Skip to content

Laravel Forge Deployment

Production-oriented guide for hosting SaleOS on Laravel Forge: three sites (API, SPA, Docs), environment variables, deploy scripts, daemons, scheduler, Reverb, and email.

Use this with the Production Runbook (launch blockers / smoke) and Notification System (Redis, Reverb TLS, Web Push). Local machines stay on Installation.

Create three Forge sites (same server or separate servers). Do not mix PHP API, SPA static files, and docs on one web root.

SiteDomain exampleGit repoBranchWeb directoryNode on server
APIapi.example.comDiligentCreators/SaaS-Backendmain/publicNot required
SPAapp.example.comDiligentCreators/SaaS-Frontendbuild-artifacts/ (site root)Not required
Docsdocs.example.comDiligentCreators/SaaS-Docsbuild-artifacts/ (site root)Not required
mermaid
flowchart LR
  subgraph github [GitHub]
    Bmain[SaaS-Backend main]
    Fmain[SaaS-Frontend main]
    Dmain[SaaS-Docs main]
    Fart[Frontend build-artifacts]
    Dart[Docs build-artifacts]
  end
  subgraph forge [Laravel Forge]
    API[api.example.com]
    APP[app.example.com]
    DOCS[docs.example.com]
  end
  Bmain -->|composer + migrate| API
  Fmain -->|Actions build| Fart
  Dmain -->|Actions build| Dart
  Fart -->|activate release + config.js| APP
  Dart -->|activate release| DOCS
  APP -->|HTTPS API + CORS| API
  APP -->|wss Echo| API

Rules

  • SPA and Docs: CI builds on merge to main; Forge deploys compiled build-artifacts only. Never run npm ci / vite / VitePress on the Forge server for those sites.
  • API: Forge runs Composer + artisan on each deploy. Prefer zero-downtime / quick deploy with shared .env and storage.
  • One SPA artifact serves many clients: each Forge SPA site owns its own .env and generated /config.js.

Server prerequisites (Forge)

On the API server (or shared box):

ServiceForge action
PHP 8.3+ (8.4 preferred)Site → PHP version
MySQL 8+Database → create DB + user
RedisServer → Redis (install / enable)
Nginx + TLSSite → SSL (Let's Encrypt)
ComposerBundled with Forge PHP sites
SupervisorUsed by Daemons (queue, Reverb)
SchedulerSite → Scheduler (or server cron)

Enable Redis before setting CACHE_STORE=redis / QUEUE_CONNECTION=redis. Production must not use database/file cache with tenancy.


1. API site (SaaS-Backend)

1.1 Site settings

SettingValue
RepositoryDiligentCreators/SaaS-Backend
Branchmain (or your release branch)
Project typeLaravel / PHP
Web directorypublic
PHP version8.3+ / 8.4
ComposerInstall during deploy (--no-dev)

1.2 Production .env (Forge → Environment)

Paste from backend .env.example (production-shaped template), then replace empty secrets. Shape:

env
APP_NAME="SaleOS"
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_URL=https://api.example.com
FRONTEND_URL=https://app.example.com
CORS_ALLOWED_ORIGINS=https://app.example.com

CACHE_STORE=redis
QUEUE_CONNECTION=redis
CACHE_PREFIX=saleos_production_
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=
REDIS_PORT=6379

BROADCAST_CONNECTION=reverb
REVERB_APP_ID=saleos
REVERB_APP_KEY=saleos-reverb-key
REVERB_APP_SECRET=
REVERB_HOST=reverb.example.com
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
REVERB_ALLOWED_ORIGINS=https://app.example.com

FILESYSTEM_DISK=s3
FILESYSTEM_BRANDING_DISK=public

Redis is required whenever Reverb / production cache / queue are enabled. Do not leave CACHE_STORE=database with tenancy in production.

VariableProduction note
APP_DEBUGMust be false (boot fails closed if true in production)
FRONTEND_URLAbsolute SPA origin for reset/invite links
CORS_ALLOWED_ORIGINSPin SPA origin(s); never *
REVERB_HOSTPublic WebSocket host (reverb.example.com); browsers use this via SPA VITE_REVERB_*
REVERB_SERVER_*Internal listener behind Nginx (Forge Reverb proxy)
CACHE_STORE / QUEUE_CONNECTIONredis required with Reverb and tenancy
MailPrefer Central Settings → Mail; env is fallback only
SeedersNever db:seed in production

Generate VAPID once and keep stable (rotation invalidates browser push subscriptions).

1.3 Deploy script (API)

Forge → Site → Deploy Script. Adjust paths only if your site path differs ($FORGE_SITE_PATH).

bash
$CREATE_RELEASE()

cd $FORGE_RELEASE_DIRECTORY

$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader

$FORGE_PHP artisan storage:link --force || true
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan optimize
$FORGE_PHP artisan queue:restart
$FORGE_PHP artisan reverb:restart || true

$ACTIVATE_RELEASE()

Do not add php artisan db:seed, migrate:fresh, or local:seed-demo.

After first Multi-Provider Email release on an older DB, run once (SSH or one-off):

bash
php artisan email:migrate-tenant-mail-modes

1.4 Scheduler

Forge → Site → Scheduler → enable (runs schedule:run every minute). Confirm jobs from the Production Runbook appear in routes/console.php / schedule definition.

1.5 Daemons (queue + Reverb)

Forge → Server → Daemons (or site Daemons). Use the site path Forge shows (example: /home/forge/api.example.com).

Queue worker

FieldValue
Commandphp artisan queue:work redis --queue=emails,default --sleep=1 --tries=3 --timeout=60 --max-time=3600
Userforge
Directory/home/forge/api.example.com/current or /home/forge/api.example.com (match your zero-downtime layout)
Processes2 (scale with load)

Prefer Forge’s “directory = current release” pattern so deploys + queue:restart pick up new code. If Daemons point at a fixed path, ensure it is the active release symlink.

Reverb

FieldValue
Commandphp artisan reverb:start --host=127.0.0.1 --port=8080
Userforge
DirectorySame as API release root
Processes1

Enable Forge Laravel Reverb / WebSocket proxy for the API site when available so Nginx terminates TLS and upgrades /app and /apps. Confirm:

  • Public wss://api.example.com (or dedicated ws. host) matches SPA VITE_REVERB_*
  • REVERB_ALLOWED_ORIGINS is exactly the SPA origin
  • App secret never appears in SPA env

Supervisor examples and troubleshooting: Notification System.

1.6 First-time API bootstrap (once)

On a greenfield production database (SSH as forge):

bash
cd /home/forge/api.example.com
php artisan key:generate   # if APP_KEY empty — then store in Forge Environment
php artisan migrate --force
# Create the first central admin via a controlled process — do NOT seed demo users in production
php artisan storage:link

Provision the first central user through your approved ops process (manual insert / one-off artisan), then sign in and configure Settings → Mail, payment gateways, and registration policy. Change any temporary password immediately.

1.7 Health

  • GET https://api.example.com/up → 200 (DB + Redis when configured)
  • Forge → Monitoring / uptime on /up

2. SPA site (SaaS-Frontend)

2.1 Site settings

SettingValue
RepositoryDiligentCreators/SaaS-Frontend
Branchbuild-artifacts
Web directory/ (compiled assets live at branch root)
Node.jsNot required for deploy
NginxSPA fallback to index.html for client routes

If deep links 404, add a try_files fallback to index.html (Forge “Static HTML” / custom Nginx). Do not point Web Directory at dist/ on main — that branch has no committed build.

2.2 Site .env (runtime only)

Forge Environment for the SPA site (not the API site):

env
VITE_API_URL=https://api.example.com
VITE_APP_NAME=SaleOS
VITE_API_MODE=central
VITE_REVERB_APP_KEY=<same-as-REVERB_APP_KEY>
VITE_REVERB_HOST=reverb.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https

VITE_API_URL has no trailing /api. Reverb host/port/scheme must match backend REVERB_HOST / public WebSocket endpoint.

2.3 Deploy script (SPA)

bash
$CREATE_RELEASE()

cd $FORGE_RELEASE_DIRECTORY

if [ -f ../../.env ]; then
  set -a
  source ../../.env
  set +a
fi

echo "window.env = {" > "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_API_URL: \"$VITE_API_URL\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_APP_NAME: \"${VITE_APP_NAME:-SaleOS}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_API_MODE: \"${VITE_API_MODE:-central}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_REVERB_APP_KEY: \"$VITE_REVERB_APP_KEY\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_REVERB_HOST: \"$VITE_REVERB_HOST\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_REVERB_PORT: \"$VITE_REVERB_PORT\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_REVERB_SCHEME: \"$VITE_REVERB_SCHEME\"" >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "};" >> "$FORGE_RELEASE_DIRECTORY/config.js"

$ACTIVATE_RELEASE()

No npm install, no npm run build. CI already published assets; this script only activates the release and writes /config.js.

Trigger: Forge GitHub webhook on build-artifacts, or deploy after Actions finishes. Wrong API URL → fix SPA site .env and redeploy (regenerates config.js).

Details: Frontend Build Artifacts.

PathCache
index.html, config.jsShort TTL or no-cache
/assets/* (hashed)Long-lived immutable

3. Docs site (SaaS-Docs)

3.1 Site settings

SettingValue
RepositoryDiligentCreators/SaaS-Docs
Branchbuild-artifacts
Web directory/
Deploy scriptActivate only
Node.jsNot required

3.2 Deploy script (Docs)

bash
$CREATE_RELEASE()

cd $FORGE_RELEASE_DIRECTORY

$ACTIVATE_RELEASE()

Clean URLs work on default Forge Nginx (try_files $uri $uri/ …) because CI writes directory index.html fallbacks. Optional:

nginx
location / {
    try_files $uri $uri.html $uri/ /404.html;
}

4. Email on Forge

  1. Deploy API with queue daemon running (emails,default).
  2. Sign in to Central → Settings → Mail → SMTP / Postmark / Mailgun (not Log).
  3. Set From identity; configure provider webhooks to:
    • https://api.example.com/webhooks/email/{provider}
    • Tenant custom: …/webhooks/email/{provider}/{tenant}
  4. Send test; confirm Email logs.
  5. Ensure FRONTEND_URL is the production SPA so reset links work.

Env MAIL_* is only a fallback when Settings secrets are empty. Restart queue after credential changes (queue:restart is already in the API deploy script).

See Multi-Provider Email and Authentication ops.


5. Cross-site checklist (before go-live)

Wire-up

  • [ ] API APP_URL = public API HTTPS URL
  • [ ] API FRONTEND_URL + CORS_ALLOWED_ORIGINS + REVERB_ALLOWED_ORIGINS = SPA origin
  • [ ] SPA VITE_API_URL = API origin
  • [ ] SPA VITE_REVERB_* matches public Reverb (TLS, host, port 443, key)
  • [ ] Stripe / Creem webhook URLs hit the API host; secrets non-empty for active gateways
  • [ ] BRANDED_SERVER_IPV4 (and optional CNAME) set if using custom domains

Processes

  • [ ] API scheduler enabled
  • [ ] Queue daemon running (emails,default)
  • [ ] Reverb daemon + Nginx WebSocket proxy
  • [ ] Redis up; CACHE_STORE / QUEUE_CONNECTION = redis

Deploy paths

  • [ ] API deploys from main with migrate + optimize + queue/reverb restart
  • [ ] SPA deploys from build-artifacts with config.js generation
  • [ ] Docs deploys from build-artifacts with activate-only script
  • [ ] No production seeders / migrate:fresh

Smoke

  • [ ] GET /up on API
  • [ ] Central + tenant login on SPA
  • [ ] Forgot-password email leaves the queue
  • [ ] Notification bell updates over Echo (or unread poll fallback)
  • [ ] Docs homepage and a deep link load

Full launch blockers: Production Runbook.


6. Deploy order

When shipping a release that touches all three repos:

  1. Backend — merge → Forge API deploy (migrations first)
  2. Frontend — merge → wait for Actions → build-artifacts → Forge SPA deploy
  3. Docs — merge → wait for Actions → build-artifacts → Forge Docs deploy

SPA may briefly talk to a newer API if you deploy backend first (preferred for additive APIs). Avoid deploying SPA features that require API routes before the API migrate finishes.

See Release Process.


7. Troubleshooting (Forge)

SymptomCheck
SPA blank / wrong APISite .env + redeploy so config.js regenerates; view-source /config.js
CORS errorsAPI FRONTEND_URL / CORS_ALLOWED_ORIGINS match SPA scheme+host
Echo never connectsReverb daemon; Nginx /app /apps; SPA VITE_REVERB_*; REVERB_ALLOWED_ORIGINS
Mail stuckQueue daemon; failed_jobs; Central Mail settings; not MAIL_MAILER=log
500 after deployAPP_DEBUG=false but check storage/logs; php artisan optimize / missing APP_KEY
Docs 404 on deep linksBranch is build-artifacts; Web Directory /; CI clean-url indexes present
Stale SPA/docsConfirm Forge site branch is build-artifacts, not main; redeploy after Actions

TopicDoc
Launch blockers / smokeProduction Runbook
Reverb / Redis / Web PushNotification System
SPA CI + config.jsFrontend Build Artifacts
Migrate-only upgradesUpgrade Guide
Local installInstallation
Docs Forge notesSaaS-Docs README

Official documentation for the SaleOS SaaS Platform.