Deployment
Production runs on Render. The whole stack (5 Go services + 3
static sites + managed Postgres + managed Redis) is described
declaratively in render.yaml at the repo root — one Blueprint apply
brings it all up.
The full playbook lives in
docs/deploy.md.
This page is the tour, not the replacement.
What Render provisions
civicos-gateway— the api-gateway, a free Web Service (port3000). Public entry point; spins down after 15 minutes idle.civicos-identity— identity-service, starter Web Service. Also runs the shared database's migrations at boot.civicos-community— community-service, starter Web Service.civicos-organization— organization-service, starter Web Service.civicos-civicai— civicai-service, starter Web Service. Holds no database of its own; it reads from the other services with the caller's token.
The four internal services are type: web, not pserv. They are reached
by the gateway over Render's private network via
fromService.hostport, so the traffic never touches the public edge —
that matters, because Render's Cloudflare-fronted edge intermittently
429s the gateway's shared egress IP. They stay web rather than becoming
private services because a service's type is immutable on Render:
converting means delete and recreate, with new hostnames and re-wiring.
They remain publicly reachable on their onrender.com URLs, and every
route enforces JWT itself.
starter also buys two things the free plan does not: instances that
never spin down, and outbound SMTP.
civicos-web— citizen web Static Site (apps/webbuild output).civicos-admin— admin console Static Site (apps/adminbuild output).- Managed Postgres, managed Redis.
Only the gateway and the two Static Sites are public. The three backend services are private — they're reachable inside Render's network from the gateway but never from the public internet.
Estimated cost at launch
~$34/mo on Render's minimum plans. Breakdown in docs/deploy.md.
First-time deploy — the short version
- Connect Render to the GitHub repo.
- In Render, New + → Blueprint → point at the repo → confirm.
- Wait ~20–30 minutes for the first Docker builds.
- Set the following env vars in the Render dashboard on each service
(Blueprint provides defaults for most, but some are secrets):
-
JWT_SECRET(32+ chars) — must be the same on gateway, identity, community, organization and civicai. The Blueprint mints one into the sharedcivicos-secretsgroup, so this is only manual if you override it. -
GEMINI_API_KEYon civicai — from Google AI Studio. The service refuses to boot without it, which surfaces as a failed deploy rather than a service that 500s on every AI call.⚠️ The free tier allows 20 requests per day across the whole project, shared by all eleven CivicAI endpoints. Put the key on a paid plan before opening the platform to real users.
-
PAYSTACK_SECRET_KEY/PAYSTACK_PUBLIC_KEYon organization — donations are disabled without them. -
SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD/SMTP_FROMon identity (once you have a Resend / Postmark account). -
APP_URLon identity — the public URL where email links land.
-
- Wait for services to go healthy (each has
/health). - Register the first user via the citizen web app, verify the email,
then bump their role to
PLATFORM_ADMINviarender psql:UPDATE users SET role='PLATFORM_ADMIN' WHERE email='<you>';
Zero-downtime deploys
Render does rolling deploys per service by default. Because each
service has /health, Render waits for the new instance to answer
200 before shifting traffic. AutoMigrate runs at startup — if you're
making an additive schema change, that's fine. If you're making a
destructive schema change, apply the SQL migration first (via
render psql) then push the code.
Environment variables — production checklist
Set on every backend service:
DATABASE_URL— the Render Postgres string (Blueprint wires this). Not set on civicai — it owns no tables.JWT_SECRET— 32+ chars, identical across the gateway and every service that validates a token (identity, community, organization, civicai). A mismatch anywhere shows up as 401s on that service alone.PORT— Render sets this; don't override.
Set on the gateway:
-
IDENTITY_SERVICE_URL,COMMUNITY_SERVICE_URL,ORGANIZATION_SERVICE_URL,CIVICAI_SERVICE_URL— Blueprint wires these to the private URLs.Each has a
localhostfallback for local development, which is a trap in production: an unset variable does not fail loudly, it silently routes to a port where nothing is listening. If one family of routes returns connection errors and everything else is fine, check this first. -
REDIS_URL— Render Redis (Blueprint wires this).
Set on identity:
SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD/SMTP_FROM— real email.APP_URL— used in email link generation.
Set on the frontends (build-time — configured in apps/*/render.yaml
section or via Static Site env):
VITE_API_URL— the gateway's public URL.
Custom domain
Once the platform is live at the Render-issued URL:
- Add the custom domain in Render on the gateway service and both Static Sites.
- Point DNS as Render instructs.
- Update
APP_URLon identity andVITE_API_URLon the frontends to the custom domain. - Redeploy the frontends so the new env is baked in.
Backups
- Render Postgres runs daily snapshots. Retention is plan-dependent — check the current plan before relying on it for compliance.
- User uploads live on the community-service Web Service's disk
(
uploads/directory). Render's ephemeral disks don't survive a redeploy on the free tier — for production you must either bind a persistent disk to the community-service or move uploads to S3 / R2 before launch.
Rollback
Each Render service keeps a small history of past deploys. Roll back via the Render dashboard on the specific service — the previous image comes back up in a couple of minutes. Roll back the schema only if you had a destructive migration; additive changes are safe to leave.