Full-Stack Local Development Setup
Get the complete DutyCall platform running locally in under 10 minutes.
- 👨💻 Human Developer
- 🤖 AI Agent
Quick Start (30 Seconds)
# Clone the repository
git clone git@github.com:chrisberno/dutycall.git
cd dutycall
# Backend setup
cd backend
composer install
cp .env.example .env
# Edit .env with your database credentials (see below)
php artisan key:generate
mysql -u root -e "CREATE DATABASE dutycall_local CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
php artisan migrate
php artisan db:seed --class=RoleTestUsersSeeder
# Frontend setup
cd ../frontend
npm install
cp .env.example .env.local
# Edit .env.local with backend URL (see below)
# Start everything (requires 3 terminals)
# Terminal 1: Backend server
cd backend && php artisan serve --port=8090
# Terminal 2: Ngrok tunnel + Twilio config (automated)
cd backend && ./dev-start.sh
# Terminal 3: Frontend server
cd frontend && npm run dev
You're ready! Open http://localhost:3000 and login with agent@dutycall.net / password
Architecture Context
Repository: Monorepo with tightly coupled frontend/backend
backend/- Laravel 11 API (PHP 8.2+, MySQL, Sanctum)frontend/- Next.js 15 (React, TypeScript, TailwindCSS)
Key Patterns:
- Auth: Sanctum Bearer tokens (not sessions)
- API: Frontend → Backend REST (localhost:8090)
- Twilio: WebRTC + TwiML webhooks (ngrok required)
- Environments: Local (MySQL + ngrok) vs Production (PostgreSQL + Railway)
TwiML Routing:
$url = env('NGROK_URL', env('APP_URL')) . '/api/callback';
Local: NGROK_URL → ngrok → laptop
Production: APP_URL → Railway (no NGROK_URL set)
Source Files:
backend/README.md- Technical referencebackend/CLAUDE.md- Known issues, testingfrontend/CLAUDE.md- Hooks, API integrationbackend/dev-start.sh- Automated ngrok setup
Test Accounts (password: password):
super@dutycall.net- super_adminadmin@dutycall.net- account_adminmanager@dutycall.net- dept_manageragent@dutycall.net- agent
Prerequisites
System Requirements
- 👨💻 Human Developer
- 🤖 AI Agent
Required Software:
- PHP: 8.2+ with extensions (BCMath, Ctype, Fileinfo, JSON, Mbstring, OpenSSL, PDO, Tokenizer, XML)
- Composer: 2.x
- Node.js: 18+
- MySQL: 8.0+ (or MariaDB 10.3+)
- Ngrok: Latest version
macOS Installation:
brew install php@8.2 mysql composer node ngrok
brew services start mysql
Ubuntu/Debian Installation:
sudo apt update
sudo apt install php8.2 php8.2-mysql php8.2-mbstring php8.2-xml composer nodejs npm mysql-server
snap install ngrok
sudo systemctl start mysql
Dependencies:
- PHP 8.2+ (Laravel 11 requirement)
- MySQL 8.0+ (local) / PostgreSQL (production)
- Node.js 18+ (Next.js 15 ESM support)
- Composer 2.x
- Ngrok (webhook tunneling)
Database Schema: backend/database/migrations/
users- Sanctum auth, rolescampaigns- Outbound campaignscontacts- Contact databasequeue_lists- Inbound queuetwilio_call_logs- Call history
Repository Access
This is a private repository. To clone:
- Request Access: Contact CEO for GitHub collaborator access
- Configure Auth:
- SSH (recommended): Set up SSH key
- HTTPS: Use Personal Access Token
- Clone:
git clone git@github.com:chrisberno/dutycall.git
Backend Setup (5 minutes)
- 👨💻 Human Developer
- 🤖 AI Agent
1. Install Dependencies
cd backend
composer install
2. Configure Environment
cp .env.example .env
php artisan key:generate
Edit .env with these critical values:
APP_NAME="Duty Call - Agent Workspace Platform"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8090
# Frontend URL (for CORS)
FRONTEND_URL=http://localhost:3000
# Database Configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dutycall_local
DB_USERNAME=root
DB_PASSWORD= # Leave empty if no password
# Twilio Configuration (contact project lead for credentials)
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER_DEV=+18316033889
TWILIO_PHONE_NUMBER_PROD=+16282373889
# WebRTC Configuration
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=your_api_secret_here
TWILIO_TWIML_APP_SID=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_EDGE=ashburn
# Ngrok URL (updated automatically by dev-start.sh)
# NGROK_URL=https://your-ngrok-url.ngrok-free.app
The credentials above are for the dev environment test number (+1 831 603 3889). Contact the project lead for the full Twilio auth token and API secret.
3. Set Up Database
# Create database
mysql -u root -p
CREATE DATABASE dutycall_local CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EXIT;
# Run migrations
php artisan migrate
# Seed test users
php artisan db:seed --class=RoleTestUsersSeeder
Test accounts created (all use password password):
super@dutycall.net- Full system accessadmin@dutycall.net- Account managementmanager@dutycall.net- Team oversight, campaignsagent@dutycall.net- Call handling
4. Start Backend Server
php artisan serve --port=8090
Leave this terminal running. Backend is now at http://localhost:8090
Laravel 11 + Sanctum + Twilio SDK
Environment Pattern:
// Dual-environment routing
$url = env('NGROK_URL', env('APP_URL')) . '/api/callback';
Seeder: RoleTestUsersSeeder creates 4 roles
Port: Must use 8090 (hardcoded in frontend)
Key Controllers:
TwilioWebhookController- TwiML handlersDialerController- Campaign executionVoiceTokenController- WebRTC tokensAnalyticsController- Call reporting
Frontend Setup (2 minutes)
- 👨💻 Human Developer
- 🤖 AI Agent
1. Install Dependencies
cd frontend # (from project root)
npm install
2. Configure Environment
cp .env.example .env.local
Edit .env.local:
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:8090
# Environment
NEXT_PUBLIC_APP_ENV=local
3. Start Frontend Server
npm run dev
Frontend is now at http://localhost:3000
Next.js 15 + App Router + TypeScript
API Client: src/lib/teleman-api.ts
- Sanctum Bearer tokens
- localStorage persistence
- Auto-includes
Authorizationheader
Auth Context: src/contexts/AuthContext.tsx
- Session state management
- Login/logout methods
- localStorage sync
Key Hooks:
useTwilioDevice- WebRTC lifecycleuseInboundQueue- Queue managementuseAuth- Auth context
The Magic of dev-start.sh
- 👨💻 Human Developer
- 🤖 AI Agent
DutyCall uses ngrok to tunnel localhost to the internet for Twilio webhook callbacks.
SPOK automated this with the dev-start.sh script.
Automated Setup (Recommended)
In a new terminal (keep backend running):
cd backend
./dev-start.sh
What it does automatically:
- Starts ngrok tunnel to
localhost:8090 - Extracts ngrok HTTPS URL (e.g.,
https://abc123.ngrok-free.app) - Updates
.envwithNGROK_URL - Configures Twilio dev number (
+1 831 603 3889) to point to ngrok - Sets up webhook endpoints (inbound, status, queue)
- Keeps ngrok running (press
Ctrl+Cto stop)
Expected Output:
🚀 Starting DutyCall Development Environment...
📡 Starting ngrok tunnel...
✅ Ngrok tunnel established: https://71aadcf03d46.ngrok-free.app
🔧 Configuring Twilio dev number (+1 831 603 3889)...
✅ Dev number configured
Voice URL: https://71aadcf03d46.ngrok-free.app/api/twilio/inbound
✅ Development environment ready!
📞 Dev Number: +1 831 603 3889
🌐 Ngrok URL: https://71aadcf03d46.ngrok-free.app
🖥️ Backend: http://localhost:8090
🎨 Frontend: http://localhost:3000
💡 Call +1 831 603 3889 to test inbound calls locally
Free ngrok accounts have session limits. If ngrok stops:
pkill -f ngrok
./dev-start.sh # Restart
Manual Ngrok Setup (If Script Fails)
# Start ngrok
ngrok http 8090
# Copy HTTPS URL from ngrok output
# Update .env manually:
NGROK_URL=https://abc123.ngrok-free.app
# Restart backend
pkill -f "php artisan serve"
php artisan serve --port=8090
# Configure Twilio Console:
# Phone Numbers > +1 831 603 3889 > Voice Configuration
# Voice URL: https://abc123.ngrok-free.app/api/twilio/inbound (POST)
# Status Callback: https://abc123.ngrok-free.app/api/twilio/status (POST)
Purpose: Expose localhost for Twilio webhooks
Flow:
- Ngrok:
https://random.ngrok-free.app→localhost:8090 - Script updates
.envwithNGROK_URL - Script calls Twilio API to update phone webhooks
- Twilio sends callbacks to ngrok → laptop
Script Logic (dev-start.sh):
# Extract ngrok URL
NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
# Update .env
sed -i '' "s|NGROK_URL=.*|NGROK_URL=$NGROK_URL|" .env
# Configure Twilio
curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$SID/IncomingPhoneNumbers/$NUM.json" \
--data-urlencode "VoiceUrl=$NGROK_URL/api/twilio/inbound"
Why: Without ngrok, Twilio can't reach localhost → calls fail
Testing End-to-End
- 👨💻 Human Developer
- 🤖 AI Agent
Verify your setup with the inbound call flow:
1. Login to Frontend
- Open
http://localhost:3000 - Login:
agent@dutycall.net/password - Dashboard should load
2. Navigate to Queue Dashboard
- Click "Queue" in sidebar
- Queue dashboard loads
- Verify Device Status: "Registered" (green)
3. Set Availability
- Click "Available" button
- Status → "Available" (green)
- System polls for calls every 2 seconds
4. Make Test Call
- From your phone, call
+1 831 603 3889 - Hear hold music
- Call appears in Queue Dashboard within 2 seconds
5. Accept Call
- Click "Accept Call"
- Browser connects via WebRTC (may prompt for mic)
- Within 2-3 seconds, connected
- Verify two-way audio
6. End Call
- Hangup from phone OR click "Hangup"
- Call disappears from queue
- Call logged in history
Testing Manual Dialer (Outbound)
- Navigate to "Dialer"
- Click "Manual Dialer" tab
- Enter phone number
- Click "Call"
- Verify two-way audio
If calls don't connect:
- Backend on
localhost:8090? - Ngrok active?
curl http://localhost:4040/api/tunnels .envhas correctNGROK_URL?- Device Status "Registered"?
Test Flow:
Caller → Twilio → Ngrok → /api/twilio/inbound → <Enqueue>
↓
queue_lists table
↓
Frontend polls /api/queue/calls → Agent accepts
↓
Device.connect() → /api/twilio/agent-dial-queue
↓
<Dial><Queue> → Twilio bridges calls
Endpoints:
POST /api/twilio/inbound- EnqueuePOST /api/twilio/agent-dial-queue- WebRTC joinPOST /api/twilio/dequeue- CleanupGET /api/queue/calls- Frontend pollingPOST /api/queue/accept-call- Trigger dial-in
WebRTC Events:
device.on('registered', () => console.log('Ready'));
device.on('incoming', call => call.accept());
device.on('disconnect', call => console.log('Ended'));
Three Terminals Cheat Sheet
Keep these running:
# Terminal 1: Backend Server
cd backend
php artisan serve --port=8090
# Terminal 2: Ngrok + Twilio Config
cd backend
./dev-start.sh
# Terminal 3: Frontend Server
cd frontend
npm run dev
Access Points:
- Frontend: http://localhost:3000
- Backend: http://localhost:8090
- Ngrok Dashboard: http://localhost:4040
Troubleshooting
- 👨💻 Human Developer
- 🤖 AI Agent
Port 8090 Already in Use
lsof -ti:8090 | xargs kill -9
php artisan serve --port=8090
MySQL Connection Refused
# macOS
brew services list
brew services start mysql
# Linux
sudo systemctl status mysql
sudo systemctl start mysql
# Test
mysql -u root -p -e "SELECT 1"
Frontend Can't Connect to Backend
Check:
- Backend on
localhost:8090? - Frontend
.env.local:NEXT_PUBLIC_API_URL=http://localhost:8090 - CORS in
backend/config/cors.php
Fix:
cd backend
php artisan cache:clear
php artisan config:clear
Twilio Webhooks 404
Check:
- Backend on port 8090
- Ngrok active:
curl http://localhost:4040/api/tunnels .envhasNGROK_URL- Twilio dev number configured
Fix:
pkill -f "php artisan serve"
php artisan serve --port=8090
Device Not Registering
Check:
- Browser console errors
- Microphone permissions
- Twilio credentials in
.env - Token endpoint:
http://localhost:8090/api/voice/token
Debug:
curl -X POST http://localhost:8090/api/voice/token \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"recording_enabled": false}'
Ngrok Session Expired
pkill -f ngrok
./dev-start.sh
Common Issues:
- Port conflicts:
lsof -ti:8090 | xargs kill -9 - Database: Check MySQL service
- CORS: Verify
backend/config/cors.php - Webhook 404s: Restart backend after
.envchanges - WebRTC: Check Twilio credentials
- Ngrok:
pkill -f ngrok && ./dev-start.sh
Debug Tools:
- Ngrok:
http://localhost:4040- Webhook inspector - Laravel logs:
tail -f backend/storage/logs/laravel.log - Browser: DevTools Network tab
Critical Fixes (do not overwrite):
AnalyticsController.php:691,716- Schema fixesconfig/cors.php:18- Analytics CORSTwilioWebhookController.php:304-310- Orphan prevention
Next Steps
Now that you're running locally:
- Explore Roles - Login as different users
- Test Campaigns - Create and run campaigns
- Review API - API Reference
- Production Config - Environment Configuration
Need Help?
- Backend:
backend/README.md,backend/CLAUDE.md - Frontend:
frontend/CLAUDE.md - Webhooks: Ngrok dashboard at
http://localhost:4040 - Questions: Contact project lead
Built with Laravel 11, Next.js 15, and Twilio Programmable Voice