# Deployment — tools.spboucher.ai on MacLustr ## Topology ``` browser ──HTTPS──▶ BHS64 (OVH Beauharnois, 51.161.112.61) Caddy, Let's Encrypt └─ WireGuard wg0 10.67.0.1 ──▶ M2M32c wg1 10.67.0.20 : 8540 (Next.js, PM2 `tools-web`) ├─ PM2 `tools-worker` (jobs, sweeps, backups) └─ PostgreSQL 17 + pgvector (localhost:5432, db tools_spboucher) DNS: GoDaddy A tools.spboucher.ai → 51.161.112.61 (pre-existing, verified 2026-11-02) Orchestration: M1M32 `mld` (manifest ~/dispatch/apps/tools-spboucher-ai.json), registry, heal every 5 min, route management ``` ## One-time node preparation (done 2026-11-02) ```bash ssh M2M32c export PATH=/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:$PATH createdb tools_spboucher # roles with generated passwords (printed once; copied into .env) DATABASE_URL_ADMIN=postgres://localhost:5432/tools_spboucher pnpm db:roles mkdir -p ~/apps/tools-spboucher-ai ~/backups/tools-spboucher-ai && chmod 700 ~/backups/tools-spboucher-ai # .env: copy ~/Desktop/Cluster/secrets/tools-spboucher-ai/.env → ~/apps/tools-spboucher-ai/.env (mode 600) ``` Secrets in `.env`: `DATABASE_URL` (tools_app), `DATABASE_URL_PUBLIC` (tools_reader), `DATABASE_URL_ADMIN` (owner role for migrations/backups), `SESSION_SECRET`, `WEBAUTHN_RP_ID=tools.spboucher.ai`, `WEBAUTHN_ORIGIN=https://tools.spboucher.ai`, `APP_URL`, `OWNER_EMAIL`, `OWNER_INITIAL_PASSWORD` (changed at first login), `BACKUP_PASSPHRASE`, `BACKUP_DIR`, `BACKUP_REMOTE`, `AI_PROVIDER=none`. ## Manifest (`infra/mld/tools-spboucher-ai.json` → `M1M32:~/dispatch/apps/`) PM2 `tools-web` = `node node_modules/next/dist/bin/next start -p 8540 -H 0.0.0.0`; `tools-worker` = `node node_modules/tsx/dist/cli.mjs src/worker/index.ts`. Hooks (post_sync): require `.env` → `pnpm install --frozen-lockfile` → `pnpm db:migrate` + `pnpm content:import --publish` + `pnpm bootstrap` → `pnpm build` (static generation reads the database, so migrations run first). Placement pinned to M2M32c. `tunnel.domain = tools.spboucher.ai`. ## Release procedure (remote-first after first deploy) First deploy (creates the app): ```bash mld stage ~/Desktop/Cluster/projets/tools-spboucher-ai tools-spboucher-ai mld deploy tools-spboucher-ai --node M2M32c # sync, hooks, PM2, local health, Caddy route, public health mld adopt tools-spboucher-ai # node dir becomes a git clone, origin = spbgit tools-spboucher-ai.git ``` Subsequent changes (on the node): ```bash ssh M2M32c "cd ~/apps/tools-spboucher-ai && git status --short && git pull" # or edit on the node ssh M2M32c "cd ~/apps/tools-spboucher-ai && pnpm install --frozen-lockfile && pnpm build && pnpm db:migrate && pm2 restart tools-web tools-worker" curl -fsS https://tools.spboucher.ai/api/health ssh M2M32c "cd ~/apps/tools-spboucher-ai && git add -A && git commit -m '…' && git push origin main" ``` Migrations are additive and tracked by checksum; `pnpm build` happens before `pm2 restart`, so the previous build keeps serving until the switch (seconds of downtime at restart). ## Rollback ```bash ssh M2M32c "cd ~/apps/tools-spboucher-ai && git log --oneline -5" ssh M2M32c "cd ~/apps/tools-spboucher-ai && git checkout && pnpm install --frozen-lockfile && pnpm build && pm2 restart tools-web tools-worker" ``` Schema rollback: migrations never drop data columns; if a migration must be reverted, restore the latest backup into a scratch database (`pnpm restore:drill` proves the path) and copy rows back, or write a forward migration. The backup taken by `mld` before sync (`~/dispatch/stage`) and the nightly encrypted dumps are the recovery points. ## Backups - Nightly 04:20 (worker) → `~/backups/tools-spboucher-ai/tools-.dump.enc` (AES-256-CBC, PBKDF2) → rsync to `M3U96b:~/backups/tools-spboucher-ai/`; retention 14. - Manual: `pnpm backup`. Drill: `pnpm restore:drill` (creates `tools_restore_drill_*`, restores, compares counts + content hash, drops it). - Restore for real: `openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 -in .enc -out dump -pass env:TOOLS_BACKUP_PASS && pg_restore --clean --if-exists -d tools_spboucher dump`. ## Operations | Task | Command | |---|---| | Where is it / health | `mld status --live`, `curl https://tools.spboucher.ai/api/health` | | Logs | `mld logs tools-spboucher-ai` or `ssh M2M32c pm2 logs tools-web --lines 100` | | Restart | `mld restart tools-spboucher-ai` | | Move to another node | `mld move tools-spboucher-ai --to ` (needs Postgres 17 + pgvector there; restore DB from backup first) | | Route check | `mld tunnel status` (expect `tools.spboucher.ai → 10.67.0.20:8540 (M2M32c)`) | | Re-run link sweep | Admin → Jobs → Link sweep, or `pnpm links:check` on the node | | Content update | edit `content/`, `pnpm content:validate`, `pnpm content:import --publish` on the node, commit | ## Reverse-proxy trust Caddy forwards `X-Forwarded-For`/`X-Forwarded-Proto`; the app reads the first XFF hop for rate limiting and audit only (never for authorization). Cookies are `Secure` in production; `APP_URL`/`WEBAUTHN_ORIGIN` pin the canonical origin for CSRF and passkeys. Apex `spboucher.ai` is unaffected (no redirect added; other subdomains untouched).