iSpeaker Live iSpeaker Live / Docs
Operations

Deployment & Operations

How iSpeaker Live is deployed, configured, monitored, backed up and released to production.

On this page

Environments

iSpeaker Live runs in three long-lived environments. All three sit behind the same infrastructure pattern; only configuration differs.

EnvironmentURLPurposeData
Developmentlocalhost / dev.ispeakerlive.comEngineering iteration. Deploys on every push to develop.Seeded fake data. Reset weekly.
Stagingstaging.ispeakerlive.comQA, UAT, performance, demos. The release candidate lives here.Realistic but fake data. Refreshed on demand.
Productionispeakerlive.comReal 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

HostnamePoints toNotes
ispeakerlive.comNext.js web appApex domain redirects to www.
www.ispeakerlive.comNext.js web appPrimary canonical.
api.ispeakerlive.comLaravel API dropletVersioned at /api/v1.
admin.ispeakerlive.comLaravel droplet (Filament)IP-restricted in production.
ws.ispeakerlive.comReverb WebSocket serverTLS 1.3 with persistent connections.
meet.ispeakerlive.comJitsi serverSelf-hosted Jitsi Meet.
documentation.ispeakerlive.comThis documentation siteStatic hosting.
staging.*Mirror of the above for stagingSame 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.
ℹ️
WebSocket endpoints must use 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:

RoleSizeNotes
API + Filament4 vCPU / 8 GB RAMScales horizontally behind the LB.
Next.js web2 vCPU / 4 GB RAMMostly SSR + static; cache on the CDN.
Queue worker2 vCPU / 4 GB RAMRun by Horizon; separate node so it doesn't steal API CPU.
Reverb2 vCPU / 4 GB RAMLong-lived connections; sticky sessions at the LB.
MySQLManaged 4 vCPU / 8 GB / 100 GB SSDDaily automated backups by DO.
RedisManaged 1 GBCache + queue + WS state.
JitsiDedicated droplet 4 vCPU / 8 GBPublic 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

  1. SSH to the API host as the deploy user.
  2. Pull the latest release branch:
    cd /var/www/ispeaker-api
    git fetch --all
    git checkout release/v1.0.x
    git pull
  3. 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
  4. Run database migrations:
    php artisan migrate --force
  5. Restart PHP-FPM and Horizon:
    sudo systemctl reload php8.3-fpm
    php artisan horizon:terminate
⚠️
Run 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

  1. SSH to the web host.
  2. Pull, install, build:
    cd /var/www/ispeaker-web
    git pull
    npm ci --omit=dev
    npm run build
  3. Restart under PM2 (zero-downtime reload):
    pm2 reload ispeaker-web
  4. Smoke check the home page and a deep link to a course in both AR and EN.

Mobile release (Flutter)

Android

  1. Bump versionName and versionCode in android/app/build.gradle.
  2. Build a signed app bundle: flutter build appbundle --release --dart-define=API_URL=https://api.ispeakerlive.com.
  3. Upload to the Google Play Console (internal testing track first, then production).

iOS

  1. Bump CFBundleShortVersionString and CFBundleVersion.
  2. flutter build ipa --release --dart-define=API_URL=https://api.ispeakerlive.com.
  3. Upload via Xcode / Transporter to App Store Connect; submit through TestFlight before public release.
ℹ️
Mobile releases are gated by store review (usually 1-3 business days for iOS, 1 day for Android). Plan accordingly.

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 to active and provisions Jitsi room IDs.
  • live-rooms:complete-overdue — marks rooms whose end time has passed as completed.
  • consultations:remind — reminders 1 hour before a session.
  • consultations:mark-no-shows — marks bookings as no_show after 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.com and 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.com using the standard Debian/Ubuntu install.
  • JWT-based authentication: the API mints a short-lived JWT signed with JITSI_JWT_SECRET and 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 jibri component is enabled and uploads MP4s to DO Spaces; resulting URLs are written back to live_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

  1. Provision a fresh MySQL database.
  2. Restore the latest snapshot.
  3. Point the staging API at the restored DB and verify a smoke run of TC-AUTH-003, TC-FEED-001, and TC-PAY-001.
  4. 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:

  1. Identify the previous release tag (e.g. v1.0.6).
  2. On each API and web host: git checkout v1.0.6, re-run the deploy commands (composer/npm install, cache rebuild, reload).
  3. 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.
  4. Communicate status in #ops and to support; write a brief postmortem within 48 hours.

Release checklist

📋
Print or copy this list for every production release. Each box gets a name and a timestamp.

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).