Skip to content

ADR 0003: Render as a preview deployment (not the Phase 9 decision) ​

Status: Accepted Date: 2026-09-14

Context ​

docforum-web's GitHub Pages UI preview (ADR-adjacent — see its own ARCHITECTURE_ESSENTIALS.md "Deployment" section) had no live backend to talk to: docforum-core only ran locally. ARCHITECTURE.md §8 lists the deployment target as an explicit, still-open non-decision.

Decision ​

Deployed docforum-core's backend/ to Render (free tier) as a preview only — this does not resolve ARCHITECTURE.md §8. It exists so the GitHub Pages UI preview has a real backend to call, nothing more.

  • Web service: Node, free plan, root dir backend/, auto-deploys on push to main, build: npm install && npx prisma generate && npm run build, start: npx prisma migrate deploy && npm start.
  • Database: Render Postgres, free plan, same region (oregon) for internal networking.
  • TRUST_PROXY=1 (Phase 8, issue #35): Render terminates TLS in front of exactly one proxy hop. Login throttling keys on the client IP, so this has to be set — unset, the backend sees only the proxy address and every user shares one throttle bucket; true would let a client spoof X-Forwarded-For into a fresh bucket per request.
  • CORS_ORIGIN set to https://docforum.github.io (Phase 8, issue #37 generalised this to a comma-separated allowlist, CORS_ORIGINS; the single-origin variable still works and is honoured first if set).
  • JWT_ACCESS_SECRET/JWT_REFRESH_SECRET: Render-generated random values, distinct from the dev placeholders in .env.example.
  • Refresh-token cookie (backend/src/modules/auth/controllers/index.ts) is now genuinely environment-aware: SameSite=None; Secure when NODE_ENV=production (set by Render), SameSite=Lax/insecure for local dev over plain HTTP. GitHub Pages and Render are different domains — this is real cross-site, not just cross-port, so Secure + SameSite=None is required for the cookie to be sent/received at all.
  • docforum-web's api-client.ts now sends credentials: 'include' (previously missing — flagged in that repo's ARCHITECTURE_ESSENTIALS.md as a known gap before this ADR).
  • Phase 8, issue #37 (follow-up): because this cookie is genuinely cross-site, /auth/refresh and /auth/logout now also require the X-CSRF-Token header, and requests are checked against the CORS origin allowlist. docforum-web must keep the csrfToken returned by signup/login/refresh in memory and send it on those two calls — see docs/api/README.md. Sessions created before that change get one forced re-login (their refresh token has no csrf claim).

Consequences ​

  • Free-tier limitations, accepted for a preview: the Postgres instance expires 30 days after creation (current date: see "Renewing the free database" below) with a 14-day grace period — it will need recreating or upgrading before then, or the preview breaks. The web service spins down after 15 minutes of inactivity (~1 minute cold-start on the next request).
  • Not staging, not production. No secrets manager, no backup policy, no monitoring, single free-tier instance. ARCHITECTURE.md §8's real deployment-target decision (Fly.io / Render / AWS / etc., for an actual production topology) is still open — this ADR doesn't answer it, it answers "how does the Pages preview reach a live backend today."
  • CORS is locked to the Pages origin specifically — a second frontend origin (e.g. a future local dev server hitting the hosted API, or a custom domain) needs CORS_ORIGIN updated or turned into a list.
  • Data on the hosted Postgres instance is disposable preview data, not subject to any retention/backup guarantee — do not point real users or real PHI at this deployment.

Renewing the free database ​

The free Postgres expires 30 days after creation. Current instance expires 2026-11-06 (renewed 2026-10-07) — update this date each time you renew. The data is disposable preview data (see above), so renewing means starting from an empty database, not migrating rows.

No manual migration step is needed: the web service's start command already runs npx prisma migrate deploy, so a fresh database gets the full schema on the next deploy.

Render allows only one free Postgres per workspace, so the old instance has to be deleted before the new one can be created. The preview backend is down between steps 2 and 4.

  1. Note the emails of any admin/demo accounts on the old database — they won't carry over.
  2. Old Postgres instance → Settings → Delete Database.
  3. New → Postgres, free plan, region oregon (same as the web service, so the internal URL works). Once it's available, copy its Internal Database URL.
  4. Web service → Environment → replace DATABASE_URL with that URL → save. Render redeploys; prisma migrate deploy runs on start.
  5. Check the deploy log shows the migrations applied, then sign up a test user through the docforum-web Pages preview to confirm the round trip (including the refresh cookie).
  6. Re-create the admin/demo accounts from step 1. POST /auth/signup refuses role: 'admin', so sign up as a patient, then promote it with the database's External Database URL: psql "<external-url>" -c "UPDATE \"User\" SET role = 'admin' WHERE email = '<email>';" A promoted account is still emailVerifiedAt = null (Phase 8, issue #34), which the payments endpoints now reject with 403 EMAIL_NOT_VERIFIED. Since no email provider is integrated yet (issue #17), mark it verified out-of-band: node backend/scripts/mark-email-verified.mjs <email> (see scripts/README.md). This whole step is superseded by the admin bootstrap CLI added in issue #39 — replace it once that lands.
  7. Update the expiry date at the top of this section (and in ROADMAP.md Phase 9).

Upgrading the existing instance to a paid plan instead keeps its data and removes the expiry entirely; do that if renewing monthly gets old before issue #22 picks a real production target.

Docs are synced from each repo's own files at build time — see docs/site/scripts/sync-docs.mjs.