PolyLLM — cluster deployment (MacLustr)
Production runs on the owner's private Apple Silicon cluster and is exposed through the MacLustr Tunnel
(WireGuard + Caddy on the OVH gateway BHS64, since 2026-09-10 — ngrok is gone) at https://www.polyllm.io.
Everything is orchestrated by mld (maclustr-dispatch) from the gateway node M1M32.
Internet → www.polyllm.io (A → 51.161.112.61, BHS64) → Caddy TLS → WireGuard wg1 → M3U96a:8240 Next.js `next start` → PostgreSQL 17 (localhost)| Item | Value |
|---|---|
| Node | M3U96a (pinned: local PostgreSQL 17, same host as fetcha / spinza / rareindex) |
| Directory | ~/apps/polyllm (rsync'd by mld from the laptop staging area) |
| Processes | PM2 polyllm-web (Next 16, port 8240, binds 0.0.0.0). Public route = Caddy on BHS64 (mld tunnel route polyllm), repointed automatically on deploy/move |
| Database | postgres://localhost:5432/polyllm (created by the post-sync hook if missing) |
| Manifest | M1M32:~/dispatch/apps/polyllm.json — the only place secrets live (gitignored copy deploy/polyllm.mld.json) |
| Health | GET /api/health (liveness), GET /api/health/ready (DB ping; used by mld) |
| Registry | mld status / mld status --live |
Environment variables (production)
See .env.example. Required: NODE_ENV=production, PORT=8240, PUBLIC_APP_URL=https://www.polyllm.io,
DATABASE_URL, AUTH_SECRET (32+ bytes), API_KEY_ENCRYPTION_SECRET (32+ bytes — rotating it invalidates every
stored user key), RESEND_API_KEY, RESEND_FROM_EMAIL, ADMIN_EMAILS. Optional owner keys
(OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GEMINI_API_KEY, XAI_API_KEY) are used only for the scheduled
model-registry refresh and the admin diagnostics; user requests always use the user's own encrypted key.
Build & deploy
# laptop
cd ~/Desktop/Projets/apps-web/polyllm
pnpm typecheck && pnpm lint && pnpm test # gates
scp deploy/polyllm.mld.json M1M32:~/dispatch/apps/polyllm.json # first time / when secrets change
~/Desktop/cluster-skill/mld stage . polyllm # laptop → M1M32 staging
~/Desktop/cluster-skill/mld deploy polyllm --node M3U96amld deploy runs the manifest hooks on the node: pnpm install --frozen-lockfile → createdb polyllm (if needed) +
pnpm db:migrate (Drizzle migrations in drizzle/, version-controlled) → pnpm build → PM2 start/reload of
polyllm-web → local health check (/api/health/ready) → tunnel route https://www.polyllm.io → M3U96a:8240 → public health check → registry update.
Restart / logs on the node:
ssh M3U96a 'pm2 restart polyllm-web; pm2 logs polyllm-web --lines 100 --nostream'
~/Desktop/cluster-skill/mld tunnel status | grep polyllmMigrations are applied by the hook before the new build starts; they must stay backward compatible with the running version (add columns, never drop in the same release).
Domain / TLS (MacLustr Tunnel)
- DNS (GoDaddy):
www.polyllm.ioA →51.161.112.61(BHS64). The apexpolyllm.ioredirects tohttps://www.polyllm.io. - TLS terminates on Caddy (Let's Encrypt, automatic). The app sees
X-Forwarded-For/X-Forwarded-Proto; Better Auth is configured withbaseURL = PUBLIC_APP_URLand secure cookies in production. - Streaming: SSE responses send a
: pingheartbeat every 15 s so idle long reasoning turns are never closed by proxies. mld heal(every 5 min on M1M32) re-creates the wg1 peer / Caddy route if they disappear after an outage.
Process management
PM2 with pm2 startup (LaunchAgent) on the node → processes survive terminal closure and reboots; autorestart: true,
max_memory_restart: 2G. The model-registry scheduler runs inside the web process (src/instrumentation.ts, every
MODEL_SYNC_INTERVAL_MINUTES, default 360) plus a startup sync.
Backups
- Daily
pg_dumpat 03:45 via launchddev.polyllm.backupon M3U96a →~/backups/polyllm/polyllm-YYYYMMDD.sql.gz, 14-day retention (find -mtime +14 -delete). Install withdeploy/backup/install.sh(see that file). - Restore:
gunzip -c ~/backups/polyllm/polyllm-YYYYMMDD.sql.gz | psql polyllm_restoreaftercreatedb polyllm_restore; swap withALTER DATABASE … RENAMEduring a maintenance window. Test restore on the 1st of each month into a scratch DB and runselect count(*) from users;to validate. - Encrypted user keys are only decryptable with the same
API_KEY_ENCRYPTION_SECRET: back up the manifest too (M1M32:~/dispatch/apps/polyllm.jsonis included in the spbgit gateway backup).
Rollback
ssh M3U96a 'cd ~/apps/polyllm && git log --oneline -5' # deployed commit
# redeploy the previous tag from the laptop:
git checkout <previous-tag> && ~/Desktop/cluster-skill/mld stage . polyllm && ~/Desktop/cluster-skill/mld deploy polyllm --node M3U96aIf a migration must be reverted, restore last night's dump (see Backups) — Drizzle migrations are forward-only.
Moving to another node
mld move polyllm --to <node> handles rsync, PM2 and the tunnel route, but the database is local: dump on M3U96a, restore on the
target (PostgreSQL 17 via Homebrew), then update DATABASE_URL in the manifest before the move.