Skip to content

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)

CheckRequirement
APP_ENVproduction
APP_DEBUGfalse (boot throws if true in production)
APP_KEYSet and backed up
FRONTEND_URL / CORS_ALLOWED_ORIGINSPin SPA origins (no *)
STRIPE_WEBHOOK_SECRET / CREEM_WEBHOOK_SECRETNon-empty for each active gateway
CachePrefer CACHE_STORE=redis. Isolation uses explicit tenant-scoped keys (CacheTenancyBootstrapper is intentionally off — do not rely on Redis tags alone)
Queue workerAlways running (queue:work --queue=emails,default or Laravel Cloud background process)
ReverbSupervised process, public TLS endpoint, SPA origin pinned
SchedulerCron/schedule:run every minute
HTTPSTLS at edge; app trusts proxies (TrustProxies); production forces https URL scheme; SESSION_SECURE_COOKIE=true
Registrationregistration_enabled intentional (defaults false)
HealthGET /up returns 200 (DB; Redis when cache/queue use Redis)
Object storageFILESYSTEM_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:

bash
cd SaaS-Frontend
npm ci
# VITE_* must be set at build time (see .env.example)
npm run build

Publish dist/ (or the build-artifacts tree) to your CDN/static host / web root.

Cache guidance:

  • index.html / config.js — short TTL or no-cache (always revalidate)
  • Hashed assets under assets/ — long-lived immutable cache
  • Use build-info.json on the artifact branch to confirm which main commit 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.

  1. HTTP / PHP-FPM (Forge site / Laravel Cloud web process)
  2. Queue workers — all ShouldQueue notifications use the dedicated emails queue (QueuesOnEmails)
    bash
    php artisan queue:work redis --queue=emails,default --sleep=1 --tries=3 --timeout=60 --max-time=3600
  3. Reverb — supervised WebSocket process behind TLS
    bash
    php artisan reverb:start --host=127.0.0.1 --port=8080
    See the Notification System runbook for environment variables, Supervisor examples, origin pinning, and health checks.
  4. Scheduler (every minute)
    bash
    * * * * * cd /path/to/SaaS-Backend && php artisan schedule:run >> /dev/null 2>&1

Scheduled commands (all use withoutOverlapping):

  • sanctum:prune-expired
  • subscriptions:expire
  • billing:run-consolidated
  • crm: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 from EMAIL_LOGS_RETAIN_DAYS / config email.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:

bash
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 up

After 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:

  1. Central Settings → Mail — choose SMTP / Postmark / Mailgun (not Log/Array in production) → configure webhook URL/events when using API providers → Send test
  2. Tenant Settings → Mail — system provider test (inherits Central) and/or custom provider test (+ tenant webhook URL when custom)
  3. 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), not artisan down.
  • System settings are applied at runtime from the database; env/config cache alone does not replace DB-backed settings.

Webhooks

EndpointPurpose
POST /stripe/webhookCashier 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

  1. Central login + dashboard
  2. Tenant login + dashboard
  3. Create lead / task / communication template (module gates)
  4. Lead WhatsApp picker opens wa.me when phone + template present
  5. Stripe test webhook (or gateway health)
  6. Forgot-password email leaves the queue
  7. Suspend a test user → token immediately unusable
  8. GET /up healthy
  9. Failed jobs empty: php artisan queue:failed

Rollback

  1. Redeploy previous release artifact / image
  2. php artisan migrate:rollback only when the release’s migrations are known-safe to reverse
  3. php artisan queue:restart
  4. Verify /up, login, and billing webhook reachability

Disaster recovery (minimum)

AssetRPO guidanceRestore
MySQL (central + tenant DBs)Point-in-time or daily dumpRestore dump; verify tenancy domains
RedisEphemeral OKClear + warm; workers will refill
Object storage (branding)DailySync from backup bucket; see object-storage.md
APP_KEYOffline secure storeRequired to decrypt encrypted attributes

Official documentation for the SaleOS SaaS Platform.