{"id":"deployment","relativePath":"deployment.md","title":"Deployment — Vercel + Render","markdown":"# Deployment — Vercel + Render\n\nHow the Meta Museum system maps onto hosted infrastructure.\n\n| Piece | Host | Config |\n|---|---|---|\n| Next.js 16 web app (29 pages, 125 API routes) | **Vercel** | `vercel.json` |\n| Postgres (system of record) | **Neon** or Vercel Postgres | `DATABASE_URL` |\n| Validation service (pySHACL) | **Render** | `render.yaml` |\n| Reconciliation service (+ Redis) | **Render** | `render.yaml` |\n| AG2 worker (optional, disabled by default) | **Render** | `render.yaml` |\n| Background workers (outbox / publish queue) | TBD — see [Part C](#part-c--background-workers) | — |\n| Solr / GraphDB (Era C search + graph) | Off for this deploy | gated by `OUTBOX_PROJECT_TO_*` |\n\nA read-only public demo needs only **Vercel + Neon**. The Render services add validation/reconciliation; the workers add async projection/publishing.\n\n---\n\n## Part A — Next.js on Vercel\n\n### One-time setup\n1. Import the repo in Vercel. Framework auto-detects as **Next.js**.\n2. `vercel.json` already overrides the build:\n   - `buildCommand: next build` — **critical.** The repo's `pnpm build` runs `session:closeout:check` first, which fails 24h after the last close-out and would break every deploy. `next build` skips that guard. Local `pnpm build` is unchanged and still enforces the guard.\n3. Provision **Neon Postgres** (or Vercel Postgres) and copy the **pooled** connection string.\n4. Set the env vars below (Project → Settings → Environment Variables), then deploy.\n\n### Vercel environment variables\n| Var | Required | Value |\n|---|---|---|\n| `DATABASE_URL` | ✅ | Neon **pooled** connection string |\n| `METAMUSEUM_STORAGE_MODE` | ✅ | `postgres` |\n| `AUTH_SECRET` | ✅ | `openssl rand -base64 32` |\n| `BASE_URL` | ✅ | `https://<your-app>.vercel.app` |\n| `METAMUSEUM_PUBLIC_READ_BASE_URL` | ✅ | same as `BASE_URL` |\n| `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` | ⛔ optional | enables GitHub sign-in; omit for public read-only |\n| `VALIDATION_SERVICE_URL` | ⛔ optional | `https://metamuseum-validation.onrender.com/validate` (Part B) |\n| `METAMUSEUM_AG2_BRIDGE_ENABLED` | ⛔ optional | leave unset (bridge stays off) |\n\nNotes:\n- **Storage self-heals on a fresh DB.** Core stores (`records.ts`) catch a missing Postgres document and seed an empty `[]`, so an empty Neon DB won't crash the app.\n- **Runtime is Node, not Edge** for API routes (required by `pg` + Auth.js v5) — this is the App Router default; no action needed.\n- **Read-only FS caveat:** the 16 managed documents go to Postgres, but a route that writes a *non-managed* JSON file would hit Vercel's read-only filesystem. Core browse/explore/records flows are managed-doc only.\n\n---\n\n## Part B — Python services on Render\n\n`render.yaml` is a Blueprint defining three FastAPI web services + a Redis cache.\n\n### Setup\n1. In Render: **New → Blueprint**, point at this repo. It reads `render.yaml`.\n2. Each service: own `rootDir`, `pip install -r requirements.txt`, and\n   `uvicorn main:APP --host 0.0.0.0 --port $PORT` (bypasses the hardcoded\n   `127.0.0.1` in each `__main__`). Health check: `/health`.\n3. Redis (`metamuseum-reconcile-cache`) is wired into the reconciliation\n   service via `RECONCILIATION_REDIS_URL` automatically.\n4. **AG2 worker is optional** — delete that block from `render.yaml` unless you\n   plan to enable the bridge.\n\n### Wire services back to Vercel\nAfter Render assigns URLs, set on Vercel:\n- `VALIDATION_SERVICE_URL = https://metamuseum-validation.onrender.com/validate`\n\nThe reconciliation service is invoked by operator scripts/ops tooling rather\nthan the Next runtime; point those at `https://metamuseum-reconciliation.onrender.com`.\n\n> **Free-tier caveat:** Render free web services sleep after inactivity and\n> cold-start in ~30–60s. Fine for a demo; upgrade to paid for pilots.\n\n---\n\n## Part C — Background workers\n\nTwo long-running drains exist as `tsx` scripts; neither runs on Vercel\n(no persistent processes):\n\n| Worker | Script | Needed when |\n|---|---|---|\n| Outbox projector | `pnpm outbox:projector` | Solr/GraphDB projection is enabled (off in this deploy) |\n| Publish queue worker | `pnpm publish:queue:worker` | Wiki/publication publishing is used |\n\n**For the read-only demo: neither is required.** Decide before enabling\nprojection or publishing. Options:\n\n1. **Vercel Cron → drain-once endpoint** (recommended for demo→beta).\n   Add `vercel.json` cron entries hitting thin API routes that call the\n   `--once` drain path, e.g. every 5 min. Serverless-native, no extra host.\n   Requires adding small `/api/cron/outbox` + `/api/cron/publish` routes.\n2. **Render Cron Job / Background Worker** (recommended if projection is heavy).\n   Add a Node-runtime cron service to `render.yaml` running\n   `pnpm outbox:projector:once`. Keeps long/heavy drains off the request path.\n3. **External scheduler** (GitHub Actions on a schedule) — simplest, lowest\n   throughput; good for low-volume publishing.\n\n**Recommendation:** ship the demo with workers off; when projection/publishing\nis turned on, use **Vercel Cron (option 1)** first and graduate to **Render\n(option 2)** only if drain volume outgrows serverless timeouts.\n\n---\n\n## Quick path to a live demo\n1. Neon DB → copy pooled `DATABASE_URL`.\n2. Vercel import → set `DATABASE_URL`, `METAMUSEUM_STORAGE_MODE=postgres`,\n   `AUTH_SECRET`, `BASE_URL`, `METAMUSEUM_PUBLIC_READ_BASE_URL` → deploy.\n3. (Optional) Render Blueprint → set `VALIDATION_SERVICE_URL` on Vercel → redeploy.\n4. Workers stay off until projection/publishing is needed.\n","sections":[{"level":2,"heading":"Part A — Next.js on Vercel","anchor":"part-a-next-js-on-vercel"},{"level":3,"heading":"One-time setup","anchor":"one-time-setup"},{"level":3,"heading":"Vercel environment variables","anchor":"vercel-environment-variables"},{"level":2,"heading":"Part B — Python services on Render","anchor":"part-b-python-services-on-render"},{"level":3,"heading":"Setup","anchor":"setup"},{"level":3,"heading":"Wire services back to Vercel","anchor":"wire-services-back-to-vercel"},{"level":2,"heading":"Part C — Background workers","anchor":"part-c-background-workers"},{"level":2,"heading":"Quick path to a live demo","anchor":"quick-path-to-a-live-demo"}],"html":"<h1 id=\"deployment-vercel-render\">Deployment — Vercel + Render</h1>\n<p>How the Meta Museum system maps onto hosted infrastructure.</p>\n<p>| Piece | Host | Config |</p>\n<p>|---|---|---|</p>\n<p>| Next.js 16 web app (29 pages, 125 API routes) | <strong>Vercel</strong> | `vercel.json` |</p>\n<p>| Postgres (system of record) | <strong>Neon</strong> or Vercel Postgres | `DATABASE_URL` |</p>\n<p>| Validation service (pySHACL) | <strong>Render</strong> | `render.yaml` |</p>\n<p>| Reconciliation service (+ Redis) | <strong>Render</strong> | `render.yaml` |</p>\n<p>| AG2 worker (optional, disabled by default) | <strong>Render</strong> | `render.yaml` |</p>\n<p>| Background workers (outbox / publish queue) | TBD — see Part C(#part-c--background-workers) | — |</p>\n<p>| Solr / GraphDB (Era C search + graph) | Off for this deploy | gated by `OUTBOX_PROJECT_TO_*` |</p>\n<p>A read-only public demo needs only <strong>Vercel + Neon</strong>. The Render services add validation/reconciliation; the workers add async projection/publishing.</p>\n<p>---</p>\n<h2 id=\"part-a-next-js-on-vercel\">Part A — Next.js on Vercel</h2>\n<h3 id=\"one-time-setup\">One-time setup</h3>\n<ol><li>Import the repo in Vercel. Framework auto-detects as <strong>Next.js</strong>.</li></ol>\n<ol><li>`vercel.json` already overrides the build:</li></ol>\n<ol><li>Provision <strong>Neon Postgres</strong> (or Vercel Postgres) and copy the <strong>pooled</strong> connection string.</li></ol>\n<ol><li>Set the env vars below (Project → Settings → Environment Variables), then deploy.</li></ol>\n<ul><li>`buildCommand: next build` — <strong>critical.</strong> The repo&#39;s `pnpm build` runs `session:closeout:check` first, which fails 24h after the last close-out and would break every deploy. `next build` skips that guard. Local `pnpm build` is unchanged and still enforces the guard.</li></ul>\n<h3 id=\"vercel-environment-variables\">Vercel environment variables</h3>\n<p>| Var | Required | Value |</p>\n<p>|---|---|---|</p>\n<p>| `DATABASE_URL` | ✅ | Neon <strong>pooled</strong> connection string |</p>\n<p>| `METAMUSEUM_STORAGE_MODE` | ✅ | `postgres` |</p>\n<p>| `AUTH_SECRET` | ✅ | `openssl rand -base64 32` |</p>\n<p>| `BASE_URL` | ✅ | `https://&lt;your-app&gt;.vercel.app` |</p>\n<p>| `METAMUSEUM_PUBLIC_READ_BASE_URL` | ✅ | same as `BASE_URL` |</p>\n<p>| `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` | ⛔ optional | enables GitHub sign-in; omit for public read-only |</p>\n<p>| `VALIDATION_SERVICE_URL` | ⛔ optional | `https://metamuseum-validation.onrender.com/validate` (Part B) |</p>\n<p>| `METAMUSEUM_AG2_BRIDGE_ENABLED` | ⛔ optional | leave unset (bridge stays off) |</p>\n<p>Notes:</p>\n<ul><li><strong>Storage self-heals on a fresh DB.</strong> Core stores (`records.ts`) catch a missing Postgres document and seed an empty `[]`, so an empty Neon DB won&#39;t crash the app.</li><li><strong>Runtime is Node, not Edge</strong> for API routes (required by `pg` + Auth.js v5) — this is the App Router default; no action needed.</li><li><strong>Read-only FS caveat:</strong> the 16 managed documents go to Postgres, but a route that writes a <em>non-managed</em> JSON file would hit Vercel&#39;s read-only filesystem. Core browse/explore/records flows are managed-doc only.</li></ul>\n<p>---</p>\n<h2 id=\"part-b-python-services-on-render\">Part B — Python services on Render</h2>\n<p>`render.yaml` is a Blueprint defining three FastAPI web services + a Redis cache.</p>\n<h3 id=\"setup\">Setup</h3>\n<ol><li>In Render: <strong>New → Blueprint</strong>, point at this repo. It reads `render.yaml`.</li></ol>\n<ol><li>Each service: own `rootDir`, `pip install -r requirements.txt`, and</li></ol>\n<p>   `uvicorn main:APP --host 0.0.0.0 --port $PORT` (bypasses the hardcoded</p>\n<p>   `127.0.0.1` in each `__main__`). Health check: `/health`.</p>\n<ol><li>Redis (`metamuseum-reconcile-cache`) is wired into the reconciliation</li></ol>\n<p>   service via `RECONCILIATION_REDIS_URL` automatically.</p>\n<ol><li><strong>AG2 worker is optional</strong> — delete that block from `render.yaml` unless you</li></ol>\n<p>   plan to enable the bridge.</p>\n<h3 id=\"wire-services-back-to-vercel\">Wire services back to Vercel</h3>\n<p>After Render assigns URLs, set on Vercel:</p>\n<ul><li>`VALIDATION_SERVICE_URL = https://metamuseum-validation.onrender.com/validate`</li></ul>\n<p>The reconciliation service is invoked by operator scripts/ops tooling rather</p>\n<p>than the Next runtime; point those at `https://metamuseum-reconciliation.onrender.com`.</p>\n<blockquote><strong>Free-tier caveat:</strong> Render free web services sleep after inactivity and</blockquote>\n<blockquote>cold-start in ~30–60s. Fine for a demo; upgrade to paid for pilots.</blockquote>\n<p>---</p>\n<h2 id=\"part-c-background-workers\">Part C — Background workers</h2>\n<p>Two long-running drains exist as `tsx` scripts; neither runs on Vercel</p>\n<p>(no persistent processes):</p>\n<p>| Worker | Script | Needed when |</p>\n<p>|---|---|---|</p>\n<p>| Outbox projector | `pnpm outbox:projector` | Solr/GraphDB projection is enabled (off in this deploy) |</p>\n<p>| Publish queue worker | `pnpm publish:queue:worker` | Wiki/publication publishing is used |</p>\n<p><strong>For the read-only demo: neither is required.</strong> Decide before enabling</p>\n<p>projection or publishing. Options:</p>\n<ol><li><strong>Vercel Cron → drain-once endpoint</strong> (recommended for demo→beta).</li></ol>\n<p>   Add `vercel.json` cron entries hitting thin API routes that call the</p>\n<p>   `--once` drain path, e.g. every 5 min. Serverless-native, no extra host.</p>\n<p>   Requires adding small `/api/cron/outbox` + `/api/cron/publish` routes.</p>\n<ol><li><strong>Render Cron Job / Background Worker</strong> (recommended if projection is heavy).</li></ol>\n<p>   Add a Node-runtime cron service to `render.yaml` running</p>\n<p>   `pnpm outbox:projector:once`. Keeps long/heavy drains off the request path.</p>\n<ol><li><strong>External scheduler</strong> (GitHub Actions on a schedule) — simplest, lowest</li></ol>\n<p>   throughput; good for low-volume publishing.</p>\n<p><strong>Recommendation:</strong> ship the demo with workers off; when projection/publishing</p>\n<p>is turned on, use <strong>Vercel Cron (option 1)</strong> first and graduate to **Render</p>\n<p>(option 2)** only if drain volume outgrows serverless timeouts.</p>\n<p>---</p>\n<h2 id=\"quick-path-to-a-live-demo\">Quick path to a live demo</h2>\n<ol><li>Neon DB → copy pooled `DATABASE_URL`.</li></ol>\n<ol><li>Vercel import → set `DATABASE_URL`, `METAMUSEUM_STORAGE_MODE=postgres`,</li></ol>\n<p>   `AUTH_SECRET`, `BASE_URL`, `METAMUSEUM_PUBLIC_READ_BASE_URL` → deploy.</p>\n<ol><li>(Optional) Render Blueprint → set `VALIDATION_SERVICE_URL` on Vercel → redeploy.</li></ol>\n<ol><li>Workers stay off until projection/publishing is needed.</li></ol>","updatedAt":"2018-10-20T01:46:40.000Z","checksum":"4911b1f459b5c331dc05f45b1d453bee478c0b43f5485b0e90293e03b51289ac","checksumPrefix":"4911b1f459b5","anchorCount":8,"lineCount":108,"rawUrl":"/api/docs/content?path=deployment.md","htmlUrl":"/docs?doc=deployment.md","apiUrl":"/api/docs/content?path=deployment.md"}