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 tomain, 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;truewould let a client spoofX-Forwarded-Forinto a fresh bucket per request.CORS_ORIGINset tohttps://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; SecurewhenNODE_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, soSecure+SameSite=Noneis required for the cookie to be sent/received at all. docforum-web'sapi-client.tsnow sendscredentials: 'include'(previously missing — flagged in that repo'sARCHITECTURE_ESSENTIALS.mdas a known gap before this ADR).- Phase 8, issue #37 (follow-up): because this cookie is genuinely cross-site,
/auth/refreshand/auth/logoutnow also require theX-CSRF-Tokenheader, and requests are checked against the CORS origin allowlist.docforum-webmust keep thecsrfTokenreturned by signup/login/refresh in memory and send it on those two calls — seedocs/api/README.md. Sessions created before that change get one forced re-login (their refresh token has nocsrfclaim).
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_ORIGINupdated 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.
- Note the emails of any admin/demo accounts on the old database — they won't carry over.
- Old Postgres instance → Settings → Delete Database.
- 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. - Web service → Environment → replace
DATABASE_URLwith that URL → save. Render redeploys;prisma migrate deployruns on start. - Check the deploy log shows the migrations applied, then sign up a test user through the
docforum-webPages preview to confirm the round trip (including the refresh cookie). - Re-create the admin/demo accounts from step 1.
POST /auth/signuprefusesrole: '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 stillemailVerifiedAt = null(Phase 8, issue #34), which the payments endpoints now reject with403 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>(seescripts/README.md). This whole step is superseded by the admin bootstrap CLI added in issue #39 — replace it once that lands. - Update the expiry date at the top of this section (and in
ROADMAP.mdPhase 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.