Skip to content

Laravel Forge Deployment

Production-oriented guide for hosting EloSync on Laravel Forge: four sites (API, SPA, Docs, Marketing), 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 four Forge sites (same server or separate servers). Do not mix PHP API, SPA static files, docs, and marketing on one web root.

SiteDomain exampleGit repoBranchWeb directoryNode on server
APIapi.example.comDiligentCreators/EloSync-Backendmain/publicNot required
SPAapp.example.comDiligentCreators/EloSync-Frontendbuild-artifacts/ (site root)Not required
Docsdocs.example.comDiligentCreators/EloSync-Docsbuild-artifacts/ (site root)Not required
Marketingelosync.com / www.example.comDiligentCreators/EloSync-Websitebuild-artifacts/ (site root)Not required

Rules

  • SPA, Docs, and Marketing: CI builds on merge to main; Forge deploys compiled build-artifacts only. Never run npm ci / vite / VitePress / next build 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.
  • Marketing is a static Next.js export (out/) — no runtime Node; Forge writes /config.js for API URL and marketing pixels.

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 (EloSync-Backend)

1.1 Site settings

SettingValue
RepositoryDiligentCreators/EloSync-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="EloSync"
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=elosync_production_
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=
REDIS_PORT=6379

BROADCAST_CONNECTION=reverb
REVERB_APP_ID=elosync
REVERB_APP_KEY=elosync-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
# Avatars always use public local storage (default). Keep storage/ shared across zero-downtime releases.
# FILESYSTEM_AVATAR_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

FCM (required for closed-browser alerts): set API FCM_PROJECT_ID / FCM_CLIENT_EMAIL / FCM_PRIVATE_KEY (or FCM_CREDENTIALS) plus SPA VITE_FIREBASE_* in /config.js (including the Firebase Web Push certificate key). Queue daemon must consume emails,default — FCM rides the same notification jobs. See Notification System.

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

# storage/ is shared across zero-downtime releases — branding + avatars live under storage/app/public
$FORGE_PHP artisan storage:link --force || true
$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan optimize
$FORGE_PHP artisan reverb:restart || true
$FORGE_PHP artisan pulse:restart || true

$ACTIVATE_RELEASE()

$FORGE_PHP artisan horizon:terminate

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

After Horizon is enabled, remove Forge queue:work daemons and run a single php artisan horizon daemon instead. Do not run both — they compete for the same Redis queues.

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 (Horizon, Pulse, Reverb)

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

Horizon (Redis queue workers — replaces all queue:work daemons)

FieldValue
Commandphp artisan horizon
Userforge
Directory/home/forge/api.example.com/current or /home/forge/api.example.com (match your zero-downtime layout)
Processes1 (Horizon spawns its own worker children)

Worker queues and process limits are defined in config/horizon.php:

  • supervisor-generalautomations, whatsapp-inbound, whatsapp-outbound, webhooks, emails, lead-ingest, imports, default with balance => false so maxProcesses is a real cap (production 2 processes, 90s timeout). Do not use Horizon auto balancing with minProcesses: 1 on this many queues — that spawns ≥1 worker per queue (~8+) and thrashs small VPS RAM.
  • supervisor-email-syncemail-sync, help-desk-ingest (production max 1 process, 300s timeout — IMAP sync for personal Email and Help Desk shared mailboxes)

Include whatsapp-inbound and whatsapp-outbound when the WhatsApp Cloud module is enabled (WhatsApp Cloud deployment). On small servers (≈4 GB RAM), keep total Horizon children at ≤3 (2 general + 1 email-sync) before scaling up.

Delete legacy Forge daemons that run php artisan queue:work redis --queue=... once Horizon is live.

Laravel Pulse (pulse:check)

FieldValue
Commandphp artisan pulse:check
Userforge
DirectorySame as API release root
Processes1

Required for the Pulse Servers card. Does not replace Horizon.

Laravel Pulse ingest (pulse:work) — required when PULSE_INGEST_DRIVER=redis

FieldValue
Commandphp artisan pulse:work
Userforge
DirectorySame as API release root
Processes1

Production should set PULSE_INGEST_DRIVER=redis (and ideally PULSE_REDIS_CONNECTION on a Redis DB separate from Horizon queues) so HTTP/queue workers do not write Pulse rows on the request path. Without pulse:work, the dashboard stays empty under Redis ingest. Deploy scripts should call pulse:restart alongside horizon:terminate.

Nightwatch — keep your existing nightwatch:agent (or nightwatch:run) daemon if enabled.

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

Requires PHP ext-imap on the server for the Email module. Details: Email deployment.

Prefer Forge’s “directory = current release” pattern so deploys + horizon:terminate pick up new code. If Daemons point at a fixed path, ensure it is the active release symlink. Supervisor stopwaitsecs for Horizon must exceed the longest job timeout (300s for email-sync).

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 (EloSync-Frontend)

2.1 Site settings

SettingValue
RepositoryDiligentCreators/EloSync-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=EloSync
VITE_API_MODE=central
# VITE_CENTRAL_PATH_PREFIX=dc-s87s
VITE_REVERB_APP_KEY=<same-as-REVERB_APP_KEY>
VITE_REVERB_HOST=reverb.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https
# Optional marketing pixels — see Marketing Pixels guide
# VITE_GTM_ID=GTM-XXXXXXX
# VITE_META_PIXEL_ID=123456789012345
# VITE_LINKEDIN_PARTNER_ID=1234567
# VITE_X_PIXEL_ID=o1234

VITE_API_URL has no trailing /api. Reverb host/port/scheme must match backend REVERB_HOST / public WebSocket endpoint. Keep VITE_CENTRAL_PATH_PREFIX in sync with the API site’s CENTRAL_PATH_PREFIX when customizing the Central login path. Optional marketing pixel keys: Marketing pixels.

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:-EloSync}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_API_MODE: \"${VITE_API_MODE:-central}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_CENTRAL_PATH_PREFIX: \"${VITE_CENTRAL_PATH_PREFIX:-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 "  VITE_GTM_ID: \"${VITE_GTM_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_META_PIXEL_ID: \"${VITE_META_PIXEL_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_LINKEDIN_PARTNER_ID: \"${VITE_LINKEDIN_PARTNER_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  VITE_X_PIXEL_ID: \"${VITE_X_PIXEL_ID:-}\"" >> "$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 (EloSync-Docs)

3.1 Site settings

SettingValue
RepositoryDiligentCreators/EloSync-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. Marketing site (EloSync-Website)

4.1 Site settings

SettingValue
RepositoryDiligentCreators/EloSync-Website
Branchbuild-artifacts
Web directory/
Deploy scriptActivate only
Node.jsNot required

4.2 Deploy script (Marketing)

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 "  NEXT_PUBLIC_API_URL: \"${NEXT_PUBLIC_API_URL:-https://api.elosync.com}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  NEXT_PUBLIC_GTM_ID: \"${NEXT_PUBLIC_GTM_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  NEXT_PUBLIC_META_PIXEL_ID: \"${NEXT_PUBLIC_META_PIXEL_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  NEXT_PUBLIC_LINKEDIN_PARTNER_ID: \"${NEXT_PUBLIC_LINKEDIN_PARTNER_ID:-}\"," >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "  NEXT_PUBLIC_X_PIXEL_ID: \"${NEXT_PUBLIC_X_PIXEL_ID:-}\"" >> "$FORGE_RELEASE_DIRECTORY/config.js"
echo "};" >> "$FORGE_RELEASE_DIRECTORY/config.js"

$ACTIVATE_RELEASE()

CI (Website production build) runs next build with output: "export" and publishes the out/ directory to build-artifacts. Forge activates the release and writes /config.js from the marketing site .env — never run npm / next on the server.

Optional Nginx 404 fallback:

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

Details: EloSync-Website README and repo docs/ci-cd/website-build-artifacts.md.

4.3 Environment (runtime)

Marketing site reads NEXT_PUBLIC_* keys from Forge /config.js (window.env) at runtime — same model as the SPA. Local dev uses .env.local. Guide: Marketing pixels.


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


6. 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 (includes horizon:snapshot every five minutes when Horizon is installed)
  • [ ] Horizon daemon running (php artisan horizon) — replaces all queue:work daemons
  • [ ] Pulse daemon running (php artisan pulse:check) when Pulse Servers card is used
  • [ ] When PULSE_INGEST_DRIVER=redis: php artisan pulse:work daemon running (and deploy calls pulse:restart)
  • [ ] Reverb daemon + Nginx WebSocket proxy
  • [ ] Redis up; CACHE_STORE / QUEUE_CONNECTION = redis
  • [ ] FCM_PROJECT_ID / FCM_CLIENT_EMAIL / FCM_PRIVATE_KEY (or FCM_CREDENTIALS) set on the API
  • [ ] SPA VITE_FIREBASE_* complete in /config.js (including Web Push certificate key)

Deploy paths

  • [ ] API deploys from main with migrate + optimize + horizon:terminate + reverb restart
  • [ ] SPA deploys from build-artifacts with config.js generation
  • [ ] Docs deploys from build-artifacts with activate-only script
  • [ ] Marketing 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
  • [ ] Marketing homepage loads (elosync.com or configured domain)

Full launch blockers: Production Runbook.


7. Deploy order

When shipping a release that touches product 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
  4. Marketing — merge → wait for Actions → build-artifacts → Forge Marketing deploy (independent of API)

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.


8. 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
Marketing 404 / blankBranch is build-artifacts (not main); Web Directory /; Actions Website production build green
Stale SPA/docs/marketingConfirm 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 notesEloSync-Docs README
Marketing CIEloSync-Website docs/ci-cd/website-build-artifacts.md

Official documentation for the EloSync SaaS Platform.