Platform Production Deployment Runbook
This is the single ops checklist for launching the SaleOS platform (Central + Tenant applications) to paying customers.
Hosting on Laravel Forge? Start with the step-by-step Laravel Forge Deployment guide (three sites, deploy scripts, daemons, Reverb, email), then use this page for launch blockers and smoke tests.
Domain production guides still apply for auth, billing, RBAC, modules, Leads, and Tasks. This document covers platform-wide process requirements.
RC notes: releases/rc1-production-readiness.md.
Launch blockers (must be green)
| Check | Requirement |
|---|---|
APP_ENV | production |
APP_DEBUG | false (boot throws if true in production) |
APP_KEY | Set and backed up |
FRONTEND_URL / CORS_ALLOWED_ORIGINS | Pin SPA origins (no *) |
STRIPE_WEBHOOK_SECRET / CREEM_WEBHOOK_SECRET | Non-empty for each active gateway |
| Cache | Prefer CACHE_STORE=redis. Isolation uses explicit tenant-scoped keys (CacheTenancyBootstrapper is intentionally off — do not rely on Redis tags alone) |
| Queue worker | Always running (queue:work --queue=emails,default or Laravel Cloud background process) |
| Reverb | Supervised process, public TLS endpoint, SPA origin pinned |
| Scheduler | Cron/schedule:run every minute |
| HTTPS | TLS at edge; app trusts proxies (TrustProxies); production forces https URL scheme; SESSION_SECURE_COOKIE=true |
| Registration | registration_enabled intentional (defaults false) |
| Health | GET /up returns 200 (DB; Redis when cache/queue use Redis) |
| Object storage | FILESYSTEM_DISK=s3 + AWS_* configured; migrate local assets with php artisan storage:migrate-to-s3 (object-storage.md) |
Frontend SPA deploy
Production builds are automated on merge to main. See frontend-build-artifacts.md.
Preferred (Laravel Forge): deploy from the build-artifacts branch. Generate /config.js (window.env) from the SPA site .env in the deploy script (VITE_API_URL, VITE_APP_NAME, VITE_API_MODE, VITE_REVERB_*). Do not bake the API URL into CI. Full scripts and site settings: Laravel Forge Deployment.
Manual fallback:
cd SaaS-Frontend
npm ci
# VITE_* must be set at build time (see .env.example)
npm run buildPublish dist/ (or the build-artifacts tree) to your CDN/static host / web root.
Cache guidance:
index.html/config.js— short TTL orno-cache(always revalidate)- Hashed assets under
assets/— long-lived immutable cache - Use
build-info.jsonon the artifact branch to confirm whichmaincommit was built
Point FRONTEND_URL / CORS_ALLOWED_ORIGINS on the API at the SPA origin(s).
Required background processes
On Forge, configure these as Scheduler + Daemons on the API site — see Laravel Forge Deployment and Daemons.
- HTTP / PHP-FPM (Forge site / Laravel Cloud web process)
- Queue workers — all
ShouldQueuenotifications use the dedicatedemailsqueue (QueuesOnEmails)bashphp artisan queue:work redis --queue=emails,default --sleep=1 --tries=3 --timeout=60 --max-time=3600 - Reverb — supervised WebSocket process behind TLSbashSee the Notification System runbook for environment variables, Supervisor examples, origin pinning, and health checks.
php artisan reverb:start --host=127.0.0.1 --port=8080 - Scheduler (every minute)bash
* * * * * cd /path/to/SaaS-Backend && php artisan schedule:run >> /dev/null 2>&1
Scheduled commands (all use withoutOverlapping):
sanctum:prune-expiredsubscriptions:expirebilling:run-consolidatedcrm:send-due-notifications(every 5 minutes,onOneServer— task digests + lead follow-up due alerts; requires shared cache)notifications:prune --days=90(weekly)email-logs:prune(weekly — retention fromEMAIL_LOGS_RETAIN_DAYS/ configemail.logs_retain_days)
Deploy sequence
Forge API sites should use the deploy script in Laravel Forge Deployment ($CREATE_RELEASE / Composer / migrate / optimize / queue+reverb restart / $ACTIVATE_RELEASE). Equivalent manual sequence:
php artisan down --retry=60 # optional platform-wide maintenance
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
# Or the equivalent discrete caches:
# php artisan config:cache && php artisan route:cache && php artisan event:cache && php artisan view:cache
php artisan queue:restart
php artisan reverb:restart
php artisan upAfter a Multi-Provider Email release, also run php artisan email:migrate-tenant-mail-modes once (see Upgrade Guide). Restart queue workers whenever Central/Tenant mail credentials change in Settings.
Go-live smoke for mail:
- Central Settings → Mail — choose SMTP / Postmark / Mailgun (not Log/Array in production) → configure webhook URL/events when using API providers → Send test
- Tenant Settings → Mail — system provider test (inherits Central) and/or custom provider test (+ tenant webhook URL when custom)
- Open Email logs (Central + Tenant) — confirm test rows, body preview, and optional Resend
Migration-driven releases (no production seeders)
Default modules and tenant permissions ship through data migrations that run with php artisan migrate --force.
- Do not run
php artisan db:seed/CatalogSeeder/ role permission seeders in production to register modules or repair RBAC - New default-included modules use
DefaultModuleRegistrar(insert-only catalog + missing-workspace installs) - New permissions use
TenantPermissionSynchronizer(additive grants only) - Authentication has no authorization side effects; incomplete deploys are not silently “fixed” on login
See module-development.md and communication-templates.md.
Notes:
- Tenant-only maintenance must use Tenant Settings (
maintenance_mode), notartisan down. - System settings are applied at runtime from the database; env/config cache alone does not replace DB-backed settings.
Webhooks
| Endpoint | Purpose |
|---|---|
POST /stripe/webhook | Cashier subscription mirror (+ BillingEngine when applicable) |
POST /webhooks/gateways/{code} | Billing engine (all gateways) |
POST /webhooks/email/{provider} | Email delivery status (Central Postmark/Mailgun) |
POST /webhooks/email/{provider}/{tenant} | Email delivery status (Tenant custom mail) |
Payment webhooks are CSRF-exempt and rate-limited (throttle:webhooks). Invalid Stripe/Creem signatures are rejected before full payload persistence. Both payment paths claim webhook_logs by (payment_gateway_id, provider_event_id) so the same provider event cannot double-settle.
Email webhooks use SupportsWebhooks drivers + mail_webhook_secret / mail_webhook_events from settings (EMAIL_WEBHOOKS_ENABLED).
Failed processing (payments): if handling throws after the log row is created, status is failed and a provider retry reprocesses that event (reclaims the failed row). Successful/ignored events still return “already handled”.
Prefer configuring Stripe to deliver business events to /webhooks/gateways/stripe and keep Cashier URL for subscription sync as documented in your Stripe dashboard. Creem: POST /webhooks/gateways/creem with creem-signature.
Module cancel
Cancelling a purchased module subscription calls the payment gateway (cancelSubscription) before marking the local row cancelled and clearing entitlements. If the provider call fails, the local subscription stays active and the API returns a validation error so billing does not silently continue while access is revoked.
Post-deploy smoke
- Central login + dashboard
- Tenant login + dashboard
- Create lead / task / communication template (module gates)
- Lead WhatsApp picker opens
wa.mewhen phone + template present - Stripe test webhook (or gateway health)
- Forgot-password email leaves the queue
- Suspend a test user → token immediately unusable
GET /uphealthy- Failed jobs empty:
php artisan queue:failed
Rollback
- Redeploy previous release artifact / image
php artisan migrate:rollbackonly when the release’s migrations are known-safe to reversephp artisan queue:restart- Verify
/up, login, and billing webhook reachability
Disaster recovery (minimum)
| Asset | RPO guidance | Restore |
|---|---|---|
| MySQL (central + tenant DBs) | Point-in-time or daily dump | Restore dump; verify tenancy domains |
| Redis | Ephemeral OK | Clear + warm; workers will refill |
| Object storage (branding) | Daily | Sync from backup bucket; see object-storage.md |
APP_KEY | Offline secure store | Required to decrypt encrypted attributes |