Environment Configuration
Configure DutyCall for local development and production deployment.
- 👨💻 Human Developer
- 🤖 AI Agent
Overview
DutyCall uses environment variables to configure backend and frontend behavior. This guide covers:
- Local development configuration (
.envand.env.local) - Production deployment configuration (Railway + Vercel)
- Environment-specific differences
Architecture
Two-Environment System:
- Local: MySQL + ngrok tunneling + localhost
- Production: PostgreSQL + Railway + Vercel
Key Pattern: env('NGROK_URL', env('APP_URL'))
- Local: Uses
NGROK_URLfor Twilio callbacks - Production: Falls back to
APP_URL(noNGROK_URLset)
Backend Environment (.env)
Local Development
- 👨💻 Human Developer
- 🤖 AI Agent
Location: backend/.env (copy from backend/.env.example)
# Application
APP_NAME="Duty Call - Agent Workspace Platform"
APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:... # Generated by php artisan key:generate
APP_URL=http://localhost:8090
# Frontend (for CORS)
FRONTEND_URL=http://localhost:3000
# Database (MySQL for local)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dutycall_local
DB_USERNAME=root
DB_PASSWORD= # Your MySQL password (often empty)
# Twilio Configuration
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
TWILIO_PHONE_NUMBER_DEV=+18316033889
TWILIO_PHONE_NUMBER_PROD=+16282373889
# WebRTC (for browser-based calling)
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=your_api_secret_here
TWILIO_TWIML_APP_SID=APf62bdf0bc5c380c61e8534b43ee6479e
TWILIO_EDGE=ashburn # Optimize routing
# Ngrok (updated automatically by dev-start.sh)
NGROK_URL=https://your-ngrok-url.ngrok-free.app
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_CALLBACK_URL=http://localhost:8090/auth/google/callback
# Google Sheets API (optional, for contact imports)
GOOGLE_SERVICE_ACCOUNT_JSON=storage/app/google/service-account.json
- Never commit
.envto version control (already in.gitignore) - NGROK_URL is automatically updated by
./dev-start.sh - Twilio credentials are for dev environment - contact project lead for full values
- APP_KEY must be unique - generate with
php artisan key:generate
Critical Variables:
APP_URL: Base URL for Laravel (localhost:8090)NGROK_URL: OverridesAPP_URLfor Twilio callbacks (local only)DB_CONNECTION:mysql(local) vspgsql(production)TWILIO_*: WebRTC + TwiML configurationFRONTEND_URL: CORS whitelist
Auto-Configuration:
dev-start.sh updates NGROK_URL automatically:
sed -i '' "s|NGROK_URL=.*|NGROK_URL=$NGROK_URL|" .env
Production (Railway)
- 👨💻 Human Developer
- 🤖 AI Agent
Location: Railway Dashboard → Environment Variables
# Application
APP_ENV=production
APP_DEBUG=false # CRITICAL: Must be false in production
APP_KEY=base64:... # Generate new key for production
APP_URL=https://dutycall-production.up.railway.app
# Frontend
FRONTEND_URL=https://dutycall.com # Your production domain
# Database (PostgreSQL - provided by Railway)
DB_CONNECTION=pgsql
DB_HOST=${PGHOST} # Railway provides these
DB_PORT=${PGPORT}
DB_DATABASE=${PGDATABASE}
DB_USERNAME=${PGUSER}
DB_PASSWORD=${PGPASSWORD}
# Twilio (production credentials)
TWILIO_ACCOUNT_SID=your_prod_account_sid
TWILIO_AUTH_TOKEN=your_prod_auth_token
TWILIO_PHONE_NUMBER=+16282373889 # Production number
TWILIO_API_KEY=your_prod_api_key
TWILIO_API_SECRET=your_prod_api_secret
TWILIO_TWIML_APP_SID=your_prod_twiml_app_sid
TWILIO_EDGE=ashburn
# NO NGROK_URL in production!
# Code falls back to APP_URL automatically
- Never use local credentials in production
- Never enable
APP_DEBUG=truein production - Always use HTTPS for
APP_URLandFRONTEND_URL - Use strong, unique passwords for all services
Key Differences:
APP_DEBUG=false- Disable debug modeDB_CONNECTION=pgsql- Railway uses PostgreSQL- No
NGROK_URL- Code usesAPP_URLfallback - Production Twilio credentials
- HTTPS URLs only
Railway Auto-Provides:
PGHOST,PGPORT,PGDATABASE,PGUSER,PGPASSWORD- Use these in Railway dashboard, not hardcoded values
Frontend Environment (.env.local)
Local Development
- 👨💻 Human Developer
- 🤖 AI Agent
Location: frontend/.env.local (copy from frontend/.env.example)
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:8090
# Environment
NEXT_PUBLIC_APP_ENV=local
Frontend only needs to know where the backend API is located. All other configuration happens on the backend.
API Client: src/lib/teleman-api.ts reads NEXT_PUBLIC_API_URL
All requests prefixed with:
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8090';
Production (Vercel)
- 👨💻 Human Developer
- 🤖 AI Agent
Location: Vercel Dashboard → Environment Variables
# Backend API URL (Railway)
NEXT_PUBLIC_API_URL=https://dutycall-production.up.railway.app
# Environment
NEXT_PUBLIC_APP_ENV=production
Vercel automatically rebuilds when environment variables change. No need to manually redeploy.
Build-time Variables: NEXT_PUBLIC_* are baked into bundle at build time
Vercel Auto-Deploy: Changes to env vars trigger rebuild
The NGROK_URL Pattern
- 👨💻 Human Developer
- 🤖 AI Agent
Why NGROK_URL Exists
Twilio sends webhook callbacks to your server when calls happen. In production, your server has a public URL (Railway). In local development, your laptop doesn't have a public URL - that's where ngrok comes in.
The Problem:
- Twilio needs to reach your backend to deliver webhooks
- Your laptop (localhost:8090) is not accessible from the internet
- Twilio can't call
http://localhost:8090/api/twilio/inbound
The Solution:
- Ngrok creates a public URL that tunnels to your localhost
- Example:
https://abc123.ngrok-free.app→localhost:8090 - Twilio calls the ngrok URL, which forwards to your laptop
How It Works in Code
Backend (TwilioWebhookController.php):
// Generate callback URL
$waitUrl = env('NGROK_URL', env('APP_URL')) . '/api/twilio/queue-wait';
// Local: Uses NGROK_URL → https://abc123.ngrok-free.app/api/twilio/queue-wait
// Production: Falls back to APP_URL → https://dutycall-production.up.railway.app/api/twilio/queue-wait
This pattern appears throughout the codebase wherever Twilio callbacks are generated.
Automatic Configuration
The ./dev-start.sh script handles everything:
- Starts ngrok tunnel to localhost:8090
- Extracts the public URL from ngrok API
- Updates
.envwithNGROK_URL=https://abc123.ngrok-free.app - Configures Twilio dev number to use the ngrok URL
- Keeps ngrok running in the foreground
You never have to manually configure this!
Pattern Implementation
Code Pattern (appears in multiple controllers):
$url = env('NGROK_URL', env('APP_URL')) . '/api/endpoint';
Fallback Logic:
- Check
NGROK_URLenvironment variable - If set, use it (local development)
- If not set, fall back to
APP_URL(production)
Why This Works:
- Local:
NGROK_URLis set → callbacks go through ngrok tunnel - Production:
NGROK_URLnot set → callbacks go directly to Railway
Locations in Codebase:
TwilioWebhookController.php:304-305- Queue callbacksTwilioWebhookController.php:350-360- Inbound call TwiMLDialerController.php:120-130- Campaign callbacks
Ngrok API:
# dev-start.sh extracts URL from ngrok API
curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url'
Environment-Specific Differences
- 👨💻 Human Developer
- 🤖 AI Agent
Local vs Production Comparison
| Feature | Local | Production |
|---|---|---|
| Backend | localhost:8090 | Railway (https://dutycall-production.up.railway.app) |
| Frontend | localhost:3000 | Vercel (https://dutycall.com) |
| Database | MySQL (localhost) | PostgreSQL (Railway) |
| Twilio Number | +1 831 603 3889 (dev) | +1 628 237 3889 (prod) |
| Webhook Delivery | Ngrok tunnel | Direct to Railway |
| Debug Mode | APP_DEBUG=true | APP_DEBUG=false |
| SSL/HTTPS | Not required | Required |
| Google OAuth | localhost callback | Production domain callback |
What Changes Between Environments
Backend .env Changes:
# Local # Production
APP_ENV=local APP_ENV=production
APP_DEBUG=true APP_DEBUG=false
APP_URL=http://localhost:8090 APP_URL=https://dutycall-production.up.railway.app
DB_CONNECTION=mysql DB_CONNECTION=pgsql
NGROK_URL=https://xxx.ngrok.io # Not set in production
Frontend .env.local Changes:
# Local # Production
NEXT_PUBLIC_API_URL=http://localhost:8090 NEXT_PUBLIC_API_URL=https://dutycall-production.up.railway.app
NEXT_PUBLIC_APP_ENV=local NEXT_PUBLIC_APP_ENV=production
Technical Differences
Database:
- Local: MySQL 8.0+ (
DB_CONNECTION=mysql) - Production: PostgreSQL (
DB_CONNECTION=pgsql) - Schema identical (Laravel handles differences)
Routing:
- Local: Ngrok intercepts webhooks → tunnels to localhost
- Production: Direct HTTPS to Railway
Security:
- Local: Debug enabled, loose CORS, HTTP allowed
- Production: Debug disabled, strict CORS, HTTPS enforced
Performance:
- Local: Single-process PHP server
- Production: Multi-worker Railway containers
Twilio Configuration
- 👨💻 Human Developer
- 🤖 AI Agent
Phone Number Configuration
Twilio phone numbers need to know where to send webhook callbacks. This configuration differs between local and production.
Local Development (automated by dev-start.sh):
Phone Number: +1 831 603 3889 (dev)
Voice URL: https://YOUR-NGROK-URL.ngrok-free.app/api/twilio/inbound (POST)
Status Callback: https://YOUR-NGROK-URL.ngrok-free.app/api/twilio/status (POST)
Production (manual configuration in Twilio Console):
Phone Number: +1 628 237 3889 (prod)
Voice URL: https://dutycall-production.up.railway.app/api/twilio/inbound (POST)
Status Callback: https://dutycall-production.up.railway.app/api/twilio/status (POST)
TwiML App Configuration
For WebRTC calling, Twilio needs a TwiML App configured:
Local Development:
TwiML App: DutyCall Dev
Voice Request URL: https://YOUR-NGROK-URL.ngrok-free.app/api/twilio/agent-dial-queue (POST)
Production:
TwiML App: DutyCall Production
Voice Request URL: https://dutycall-production.up.railway.app/api/twilio/agent-dial-queue (POST)
Every time ngrok restarts, the URL changes. The dev-start.sh script automatically updates the phone number configuration, but you may need to manually update the TwiML App Voice Request URL in Twilio Console.
Environment Variables Explained
TWILIO_ACCOUNT_SID: Your Twilio account identifier
- Find: Twilio Console → Account Info
- Example:
ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN: Secret key for API authentication
- Find: Twilio Console → Account Info (click "View")
- Keep secret! Never commit to version control
TWILIO_API_KEY and TWILIO_API_SECRET: For Access Token generation
- Find: Twilio Console → Account → API Keys → Create new
- Used for WebRTC browser calling
TWILIO_TWIML_APP_SID: TwiML Application for WebRTC routing
- Find: Twilio Console → Voice → TwiML Apps
- Example:
APf62bdf0bc5c380c61e8534b43ee6479e
TWILIO_EDGE: Latency optimization (optional)
- Options:
ashburn,dublin,sydney,tokyo - Default:
ashburn(US East Coast)
Webhook Endpoints
Phone Number Webhooks (Twilio → Backend):
POST /api/twilio/inbound- Incoming call handlerPOST /api/twilio/status- Call status updates
TwiML App Webhooks (WebRTC → Backend):
POST /api/twilio/agent-dial-queue- Agent browser joins queuePOST /api/twilio/handle-call- Manual dialer call handler
Queue System Webhooks (Twilio → Backend):
POST /api/twilio/queue-wait- Hold music providerPOST /api/twilio/dequeue- Queue cleanup
Access Token Generation
VoiceTokenController.php:
$token = new AccessToken($accountSid, $apiKey, $apiSecret, 3600, $identity);
$voiceGrant = new VoiceGrant();
$voiceGrant->setOutgoingApplicationSid($twimlAppSid);
$token->addGrant($voiceGrant);
Frontend consumes:
const { token } = await api.getVoiceToken();
device = new Device(token);
await device.register();
CORS Configuration
- 👨💻 Human Developer
- 🤖 AI Agent
Understanding CORS
CORS (Cross-Origin Resource Sharing) controls which domains can access your backend API. Since the frontend and backend run on different domains (localhost:3000 and localhost:8090), CORS must be configured.
Backend (config/cors.php):
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'dashboard/*', 'auth/*', 'analytics/*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
Local Development
# Backend .env
FRONTEND_URL=http://localhost:3000
This allows requests from http://localhost:3000 to access the backend API.
Production
# Backend .env (Railway)
FRONTEND_URL=https://dutycall.com
This restricts API access to only your production frontend domain.
Never use 'allowed_origins' => ['*'] in production - this allows any website to access your API!
CORS Implementation
Laravel Config (config/cors.php):
- Whitelists frontend domain via
FRONTEND_URL - Enables credentials (cookies, auth headers)
- Allows all methods/headers for simplicity
Sanctum Integration:
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000')),
Why Credentials Matter:
- Bearer tokens in
Authorizationheader - CSRF cookies for session-based auth
supports_credentials' => truerequired
Common Configuration Issues
- 👨💻 Human Developer
- 🤖 AI Agent
Issue: Frontend Can't Connect to Backend
Symptoms: API calls fail with network errors or 404s
Check:
- Backend running on
localhost:8090? - Frontend
.env.localhasNEXT_PUBLIC_API_URL=http://localhost:8090? - CORS configured in
backend/config/cors.php?
Fix:
# Backend
cd backend
php artisan config:clear
php artisan cache:clear
# Frontend
cd frontend
rm -rf .next
npm run dev
Issue: Twilio Webhooks Failing
Symptoms: Calls fail, webhooks return 404 or 500 errors
Check:
.envhas correctNGROK_URL?- Ngrok tunnel active?
curl http://localhost:4040/api/tunnels - Backend restarted after
.envchanges? - Twilio phone number pointing to ngrok URL?
Fix:
# Restart ngrok and backend
pkill -f ngrok
pkill -f "php artisan serve"
./dev-start.sh # In one terminal
php artisan serve --port=8090 # In another terminal
Issue: Device Not Registering (WebRTC)
Symptoms: "Device not registered" errors in frontend
Check:
- Twilio credentials correct in backend
.env? - Token endpoint working?
curl http://localhost:8090/api/voice/token - Browser console showing errors?
- TwiML App configured with correct URL?
Fix:
# Test token generation
curl -X POST http://localhost:8090/api/voice/token \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"recording_enabled": false}'
Debugging Patterns
CORS Errors:
- Check
FRONTEND_URLmatches actual frontend origin - Verify
config/cors.phpincludes API paths - Clear Laravel config cache
Webhook Failures:
- Verify
.envloaded:php artisan tinker --execute="dump(env('NGROK_URL'))" - Check ngrok active:
curl -s http://localhost:4040/api/tunnels | jq - Inspect ngrok requests:
http://localhost:4040dashboard
WebRTC Issues:
- Validate credentials: Check Twilio Console
- Test token endpoint: cURL with Bearer token
- Check browser console: Device initialization errors
Best Practices
- 👨💻 Human Developer
- 🤖 AI Agent
Security
- Never commit
.envfiles to version control - Use different credentials for local and production
- Disable debug mode in production (
APP_DEBUG=false) - Use HTTPS for all production URLs
- Rotate Twilio credentials periodically
Development Workflow
- Keep
.env.exampleupdated when adding new variables - Document all environment variables in this guide
- Use
dev-start.shfor ngrok automation - Clear Laravel cache after changing
.env:php artisan config:clear - Rebuild frontend after changing
NEXT_PUBLIC_*variables
Production Deployment
- Test locally first before deploying to production
- Use Railway environment variables (not hardcoded values)
- Monitor logs after deployment for configuration errors
- Keep production credentials secret - don't share in Slack/email
Configuration Management
Version Control:
- Commit:
.env.example,config/*.php - Ignore:
.env,.env.local,.env.production
Change Detection:
- Laravel:
php artisan config:cacheafter.envchanges - Next.js: Rebuild required for
NEXT_PUBLIC_*changes
Secret Management:
- Use Railway/Vercel secret management
- Never log sensitive values
- Rotate credentials on compromise
Next Steps
- Test Your Configuration: Local Setup Guide
- Understand Architecture: Developer Overview
Need Help? Contact the project lead or check backend/README.md for detailed configuration reference.