Skip to content

Deployment

Project Used by
movement-mapping-dev Every developer’s local app, the dev app on Render, and PR previews
movement-mapping-prod The production app

Both are in the EU. Migrations reach them as described in Migrations.

Some settings live at the Supabase project level, where a migration can’t reach them:

  • PostgREST’s exposed schemas. A new project exposes only public, so app and network queries fail until they’re added.
  • The Auth site_url. Auth emails link to it. A new project uses http://localhost:3000.
  • The Auth redirect allow-list. Until /auth/confirm and /auth/callback are on it, sign-in links are rejected.
  • Custom SMTP and email templates. Without custom SMTP, Supabase won’t let you edit the templates. The default templates use a link that /auth/confirm can’t handle, so confirmation and magic-link sign-in fail.

scripts/supa-configure-project.sh sets all of these. It runs automatically on a project’s first migration (see First-run setup). For dev and prod, run it once by hand when the project is created:

Terminal window
SUPABASE_PROJECT_REF=<ref> SITE_URL=<public app URL> \
RESEND_API_KEY=<key> SMTP_SENDER_EMAIL=<address> \
./scripts/supa-configure-project.sh
  • Run it from a checkout with no .env, or with .env moved aside. The script loads .env if it exists, and that overrides the values on the command line.
  • Without RESEND_API_KEY and SMTP_SENDER_EMAIL, it skips the email step and prints the template text to paste in by hand (Dashboard → Authentication → Email Templates).

render.yaml defines everything, in Frankfurt:

Service What Deploys
network-mapping-app The production app, against prod Supabase Only from a published release (autoDeployTrigger: "off")
network-mapping-app-dev The same app against dev Supabase, on the free plan. It sleeps when idle; the first request takes about a minute. Every push to main, plus a preview per PR
network-mapping-docs This site, a static build of apps/docs Every push to main
network-mapping-purge A daily cron that calls POST /api/maintenance/purge on prod On schedule (03:00)

Both app services use GET /api/health as their health check. It returns { "ok": true } and needs no session or setup.

Secrets are set in Render’s dashboard, not committed. render.yaml marks them sync: false. The Resend variables aren’t declared in render.yaml at all, so set them on each app service by hand. See Environment variables.

Docs deploy from main while the app deploys from releases. So the published docs can describe features that aren’t in the released app yet.

Terminal window
pnpm release # work out the version from conventional commits
pnpm release minor # or major / patch
pnpm release v2.0.0 # or an exact version
pnpm release --dry-run # print everything, create nothing

The script needs an authenticated gh CLI. It creates a draft release and stops. The draft lists every pending migration, because publishing applies them to production. It doesn’t create a tag or push anything; GitHub creates the tag on publish. To abandon a draft, delete it with gh release delete <tag>.

Publishing the release runs promote-prod in .github/workflows/db.yml:

  1. It applies pending migrations to movement-mapping-prod.
  2. Only if that succeeds, it calls the app’s Render deploy hook, pinned to the release’s commit with &ref=<sha>.

The job refuses to deploy a commit that isn’t on main, skips pre-releases, and queues rather than cancels when two runs overlap. If a promotion fails part-way, re-run the job. supabase db push skips migrations already applied, and the hook fires again.

A deploy-hook deploy uses the build command and environment Render already has. A release that changes render.yaml needs the blueprint synced in Render as well.

Each pull request gets a preview of the app. Render creates it when the PR opens, deletes it when the PR closes, and posts its URL on the PR.

  • Previews come from network-mapping-app-dev, so they use the dev database and dev secrets. Never preview from the production service: a preview copies its base service’s environment.
  • Previews run on the starter plan so they don’t sleep during review.
  • A preview runs against whatever schema dev has. That includes migrations someone has pushed with scripts/supa, but not the PR’s own, unless they’ve been pushed.
  • Invitation links use the preview’s own URL (RENDER_EXTERNAL_URL), since a preview inherits the dev service’s SITE_URL. Emails Supabase sends itself, such as sign-up confirmation, still use the project’s site_url.

One-time setup:

  1. Sync the blueprint in Render. Set network-mapping-app-dev’s secrets to the dev values: VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY, VITE_DOCS_URL, and SITE_URL as the dev service’s URL. Check the first preview’s environment before trusting it.
  2. In the dev Supabase project, add the dev service’s URL and a preview wildcard (such as https://network-mapping-app-dev*.onrender.com/**) to Authentication → URL Configuration → Redirect URLs.
  • .github/workflows/ci.yml runs on pull requests and pushes to main. It does a frozen-lockfile install, then biome check, the typecheck, and pnpm build (which builds every workspace, including packages/). It runs the Socket.dev scan when SOCKET_SECURITY_API_KEY is set, without failing on its findings. It doesn’t run tests.
  • .github/workflows/db.yml migrates dev on every push to main (apply-dev, in the GitHub dev environment), and runs promote-prod on release (in the prod environment).
  • .github/workflows/supabase-branch-cleanup.yml runs when a git branch is deleted, and tries to delete a matching Supabase branch. With branching disabled, there’s nothing for it to delete.