Deployment & Operations
How iSpeaker Live is deployed, configured, monitored, backed up and released to production.
On this page
- Environments
- Infrastructure topology
- Domains & DNS
- SSL / TLS
- Server setup
- Deploying the Laravel API
- Deploying the Next.js web app
- Mobile release (Flutter)
- Environment variables
- Queue workers
- Scheduler & cron
- Reverb (WebSockets)
- Jitsi configuration
- Object storage & media
- Backups & restore
- Monitoring & logs
- Rollback
- Release checklist
Environments
iSpeaker Live runs in three long-lived environments. All three sit behind the same infrastructure pattern; only configuration differs.
| Environment | URL | Purpose | Data |
|---|---|---|---|
| Development | localhost / dev.ispeakerlive.com | Engineering iteration. Deploys on every push to develop. | Seeded fake data. Reset weekly. |
| Staging | staging.ispeakerlive.com | QA, UAT, performance, demos. The release candidate lives here. | Realistic but fake data. Refreshed on demand. |
| Production | ispeakerlive.com | Real users. | Real data. Backed up daily. |
Infrastructure topology
The stack runs on DigitalOcean. The high-level production layout:
flowchart TB
user["Users (web / Android / iOS)"]
cdn["CDN / DNS (Cloudflare)"]
nginx["Nginx Reverse Proxy + TLS"]
subgraph app["Application servers (DO Droplets)"]
api["Laravel API + Filament
(PHP-FPM)"]
web["Next.js web
(Node 20)"]
rev["Reverb WS"]
worker["Queue Workers (Horizon)"]
sched["Scheduler (cron)"]
end
subgraph data["Data plane"]
mysql[("MySQL 8 managed DB")]
redis[("Redis
cache + queue + WS state")]
spaces[("DO Spaces
S3-compatible storage")]
end
jitsi["Jitsi Meet server"]
paypal["PayPal API"]
fcm["Firebase Cloud Messaging"]
mail["SMTP / transactional email"]
user --> cdn --> nginx
nginx --> api
nginx --> web
nginx --> rev
api --> mysql
api --> redis
api --> spaces
worker --> mysql
worker --> redis
sched --> api
rev --> redis
api --> paypal
api --> fcm
api --> mail
api --> jitsi
Domains & DNS
| Hostname | Points to | Notes |
|---|---|---|
ispeakerlive.com | Next.js web app | Apex domain redirects to www. |
www.ispeakerlive.com | Next.js web app | Primary canonical. |
api.ispeakerlive.com | Laravel API droplet | Versioned at /api/v1. |
admin.ispeakerlive.com | Laravel droplet (Filament) | IP-restricted in production. |
ws.ispeakerlive.com | Reverb WebSocket server | TLS 1.3 with persistent connections. |
meet.ispeakerlive.com | Jitsi server | Self-hosted Jitsi Meet. |
documentation.ispeakerlive.com | This documentation site | Static hosting. |
staging.* | Mirror of the above for staging | Same TLS rules. |
SSL / TLS
- Certificates issued by Let's Encrypt via Certbot, renewed automatically every 60 days.
- HSTS enabled with
max-age=31536000; includeSubDomains. - TLS 1.2 and TLS 1.3 only; older protocols disabled.
- HTTP automatically redirects to HTTPS at the Nginx layer.
wss://. Mixed-content browsers will block ws:// connections from HTTPS pages.
Server setup
Production runs on Ubuntu 24.04 LTS droplets. Recommended sizing for MVP launch:
| Role | Size | Notes |
|---|---|---|
| API + Filament | 4 vCPU / 8 GB RAM | Scales horizontally behind the LB. |
| Next.js web | 2 vCPU / 4 GB RAM | Mostly SSR + static; cache on the CDN. |
| Queue worker | 2 vCPU / 4 GB RAM | Run by Horizon; separate node so it doesn't steal API CPU. |
| Reverb | 2 vCPU / 4 GB RAM | Long-lived connections; sticky sessions at the LB. |
| MySQL | Managed 4 vCPU / 8 GB / 100 GB SSD | Daily automated backups by DO. |
| Redis | Managed 1 GB | Cache + queue + WS state. |
| Jitsi | Dedicated droplet 4 vCPU / 8 GB | Public bandwidth matters more than CPU; size up if > 50 concurrent rooms. |
Base packages
sudo apt update && sudo apt -y upgrade
sudo apt -y install nginx git curl ufw fail2ban supervisor
sudo apt -y install php8.3 php8.3-fpm php8.3-mysql php8.3-redis \
php8.3-curl php8.3-mbstring php8.3-xml php8.3-zip php8.3-gd php8.3-bcmath php8.3-intl
curl -sS https://getcomposer.org/installer | php && sudo mv composer.phar /usr/local/bin/composer
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt -y install nodejs
Deploying the Laravel API
- SSH to the API host as the deploy user.
- Pull the latest release branch:
cd /var/www/ispeaker-api git fetch --all git checkout release/v1.0.x git pull - Install PHP dependencies and apply optimizations:
composer install --no-dev --optimize-autoloader --no-interaction php artisan config:cache php artisan route:cache php artisan view:cache php artisan event:cache php artisan storage:link - Run database migrations:
php artisan migrate --force - Restart PHP-FPM and Horizon:
sudo systemctl reload php8.3-fpm php artisan horizon:terminate
php artisan down --secret="..." before destructive migrations and bring the site back with php artisan up when done. Always take a database snapshot first.
Deploying the Next.js web app
- SSH to the web host.
- Pull, install, build:
cd /var/www/ispeaker-web git pull npm ci --omit=dev npm run build - Restart under PM2 (zero-downtime reload):
pm2 reload ispeaker-web - Smoke check the home page and a deep link to a course in both AR and EN.
Mobile release (Flutter)
Android
- Bump
versionNameandversionCodeinandroid/app/build.gradle. - Build a signed app bundle:
flutter build appbundle --release --dart-define=API_URL=https://api.ispeakerlive.com. - Upload to the Google Play Console (internal testing track first, then production).
iOS
- Bump
CFBundleShortVersionStringandCFBundleVersion. flutter build ipa --release --dart-define=API_URL=https://api.ispeakerlive.com.- Upload via Xcode / Transporter to App Store Connect; submit through TestFlight before public release.
Environment variables
The full .env for the Laravel API. Treat all values as secrets. They live in the deployment provider's secrets store, never in git.
APP_NAME="iSpeaker Live"
APP_ENV=production
APP_KEY=base64:...
APP_DEBUG=false
APP_URL=https://api.ispeakerlive.com
APP_LOCALE=ar
APP_FALLBACK_LOCALE=en
LOG_CHANNEL=stack
LOG_LEVEL=warning
DB_CONNECTION=mysql
DB_HOST=...
DB_PORT=3306
DB_DATABASE=ispeaker
DB_USERNAME=ispeaker
DB_PASSWORD=...
BROADCAST_CONNECTION=reverb
QUEUE_CONNECTION=redis
CACHE_STORE=redis
SESSION_DRIVER=redis
REDIS_HOST=...
REDIS_PASSWORD=...
REDIS_PORT=6379
REVERB_APP_ID=...
REVERB_APP_KEY=...
REVERB_APP_SECRET=...
REVERB_HOST=ws.ispeakerlive.com
REVERB_PORT=443
REVERB_SCHEME=https
FILESYSTEM_DISK=spaces
DO_SPACES_KEY=...
DO_SPACES_SECRET=...
DO_SPACES_REGION=fra1
DO_SPACES_BUCKET=ispeaker-prod
DO_SPACES_ENDPOINT=https://fra1.digitaloceanspaces.com
MAIL_MAILER=smtp
MAIL_HOST=...
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS=no-reply@ispeakerlive.com
MAIL_FROM_NAME="iSpeaker Live"
PAYPAL_MODE=live
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_WEBHOOK_ID=...
FCM_SERVER_KEY=...
FCM_SENDER_ID=...
JITSI_DOMAIN=meet.ispeakerlive.com
JITSI_APP_ID=...
JITSI_JWT_SECRET=...
SANCTUM_STATEFUL_DOMAINS=ispeakerlive.com,www.ispeakerlive.com
SESSION_DOMAIN=.ispeakerlive.com
The Next.js web app reads a smaller set:
NEXT_PUBLIC_API_URL=https://api.ispeakerlive.com
NEXT_PUBLIC_WS_URL=https://ws.ispeakerlive.com
NEXT_PUBLIC_JITSI_DOMAIN=meet.ispeakerlive.com
NEXTAUTH_SECRET=...
NEXTAUTH_URL=https://ispeakerlive.com
Queue workers
Background work runs on Redis-backed queues with Laravel Horizon. Typical jobs: sending emails, generating invoices, processing PayPal webhooks, FCM push delivery, video transcoding triggers, notifications fan-out, image processing for posts and avatars.
Horizon is started by Supervisor so it restarts on crash or reboot. Example config:
# /etc/supervisor/conf.d/horizon.conf
[program:horizon]
process_name=%(program_name)s
command=php /var/www/ispeaker-api/artisan horizon
autostart=true
autorestart=true
user=deploy
redirect_stderr=true
stdout_logfile=/var/log/horizon.log
stopwaitsecs=3600
After every deploy: php artisan horizon:terminate (Horizon restarts itself with the new code).
Scheduler & cron
Recurring jobs run from Laravel's scheduler. A single cron entry triggers it every minute; individual tasks define their own cadence inside the app.
# crontab -e (as the deploy user)
* * * * * cd /var/www/ispeaker-api && php artisan schedule:run >> /dev/null 2>&1
Typical scheduled jobs:
live-rooms:start-due— moves scheduled rooms toactiveand provisions Jitsi room IDs.live-rooms:complete-overdue— marks rooms whose end time has passed ascompleted.consultations:remind— reminders 1 hour before a session.consultations:mark-no-shows— marks bookings asno_showafter the window.wallets:settle-pending— moves pending balances to settled after the release period.notifications:digest— daily summary email for users who opted in.storage:cleanup-orphans— removes uploaded media not referenced by any row.
Reverb (WebSockets)
Reverb is Laravel's first-party WebSocket server. It powers real-time chat, live-room messaging, notifications, and presence indicators.
- Runs as a Supervisor service:
php artisan reverb:start --host=0.0.0.0 --port=8080. - Nginx terminates TLS at
ws.ispeakerlive.comand proxies to the Reverb port over an internal network. - Authentication uses Sanctum bearer tokens; private channels are authorized at the API layer.
- For horizontal scaling, Reverb uses Redis to share presence and broadcast state across nodes.
Jitsi configuration
- Self-hosted Jitsi Meet at
meet.ispeakerlive.comusing the standard Debian/Ubuntu install. - JWT-based authentication: the API mints a short-lived JWT signed with
JITSI_JWT_SECRETand embeds the user identity. Only authenticated tokens can join rooms. - Room IDs are generated server-side per session (
live_rooms.current_jitsi_room_id) and are unguessable. - For recording, the Jitsi
jibricomponent is enabled and uploads MP4s to DO Spaces; resulting URLs are written back tolive_rooms.recordings.
Object storage & media
- DigitalOcean Spaces (S3-compatible) holds avatars, post media, course thumbnails, book PDFs, invoice PDFs, and recordings.
- Public assets (avatars, thumbnails) are served via the Spaces CDN.
- Private assets (book PDFs, course videos) are served via pre-signed URLs with a short TTL (5-15 minutes). The client refreshes URLs as needed.
- A daily job removes orphaned uploads not referenced by any DB row.
Backups & restore
- Managed MySQL automatically performs daily snapshots retained for 7 days.
- An additional weekly logical dump (mysqldump) is uploaded to a separate Spaces bucket and retained for 90 days.
- Redis is treated as ephemeral; nothing critical is stored there beyond cache and queues.
- Spaces has versioning enabled, so accidental deletions can be recovered within 30 days.
Restore drill
- Provision a fresh MySQL database.
- Restore the latest snapshot.
- Point the staging API at the restored DB and verify a smoke run of
TC-AUTH-003,TC-FEED-001, andTC-PAY-001. - This drill is performed quarterly and documented.
Monitoring & logs
Application logs
Laravel writes to daily rotating files under storage/logs/. Sent to a central log collector and retained for 30 days.
Crash reporting
Sentry (backend + web) and Crashlytics (Flutter) capture exceptions and crashes. Releases are tagged with the build number.
Uptime
External monitor pings /api/health and the home page every minute. Alerts to #ops Slack channel on 2 consecutive failures.
Metrics
DigitalOcean built-in metrics for CPU, memory, disk, network. App-level metrics shipped to a metrics endpoint for dashboards.
Alerts
Critical alerts: 5xx error rate > 1% over 5 min, p95 latency > 1.5 s, disk usage > 80%, queue depth > 1000, failed jobs > 100.
Security
fail2ban watches SSH and Nginx logs. UFW restricts ingress to ports 22, 80, 443, plus the LB-only ports. Admin panel is IP-allowlisted.
Rollback
If a release introduces a regression that can't be fixed forward quickly:
- Identify the previous release tag (e.g.
v1.0.6). - On each API and web host:
git checkout v1.0.6, re-run the deploy commands (composer/npm install, cache rebuild, reload). - If the bad release ran migrations, do not blindly roll those back. Restore from the pre-deploy snapshot taken in the release checklist below, then redeploy
v1.0.6. - Communicate status in
#opsand to support; write a brief postmortem within 48 hours.
Release checklist
Pre-deploy
- All
TC-*Critical and High cases pass on staging. - No open S1 / S2 bugs.
- Release notes written; user-visible changes documented in both AR and EN.
- Mobile builds uploaded to internal/TestFlight tracks for smoke.
- Database snapshot taken; snapshot ID recorded.
- Feature flags (if any) configured.
- On-call engineer named for the next 24 hours.
During deploy
- Maintenance banner shown if downtime is expected.
- API deployed; smoke test the health endpoint and login.
- Web deployed; smoke test the home page in AR and EN.
- Mobile binaries promoted from testing tracks to production.
- Horizon and Reverb restarted.
Post-deploy
- Run the smoke suite:
TC-AUTH-003,TC-FEED-001,TC-CRS-003,TC-PAY-001,TC-LR-001,TC-CHAT-001. - Watch error rate and latency for the first 30 minutes.
- Maintenance banner removed.
- Stakeholders notified.
- Release tagged in the repos (e.g.
v1.0.x).