Skip to content

Deployment

Runbook for shipping code to production and rolling it back when something goes wrong. Audience: developers and on-call engineers.

Architecture at a glance

  • Frontend — Static SPA built with Vite, deployed to Cloudflare Pages. Auto-deploys on push to main. Every pull request gets a preview URL.
  • BackendSupabase managed Postgres + Auth + Storage, plus Edge Functions (Deno) deployed with the supabase CLI.
  • Release orchestrationsemantic-release on main reads Conventional Commit messages, bumps the version, tags, and writes CHANGELOG.md. See .releaserc.json and .github/workflows/release.yml.

Branch and release flow

  1. Cut a feature branch: feat/<scope>, fix/<scope>, or chore/<scope> (see CLAUDE.md).
  2. Open a PR into main. Cloudflare Pages builds a preview URL; CI runs lint, tests, build, bundle-budget check, and Playwright e2e.
  3. Merge with Squash and merge. Commit subject must follow Conventional Commits so semantic-release can classify it:
    • feat:minor version bump
    • fix: / perf: / revert: / refactor:patch
    • Footer BREAKING CHANGE:major
    • docs: / chore: / ci: / style: / test: → no release
  4. After the squash lands on main:
    • CI (.github/workflows/ci.yml) runs again on main.
    • If CI succeeds, release.yml triggers via workflow_run, runs npx semantic-release, pushes a vX.Y.Z tag, updates CHANGELOG.md, and creates a GitHub Release.
    • Cloudflare Pages notices the new commit on main and publishes the build to production.

Tip

Delete merged branches (local + remote) after squash. Keeps the branch list clean and avoids stale preview deploys sitting around.

Frontend build

npm run build runs vite build. The relevant configuration is in vite.config.ts:

  • Output: dist/ (entry, route chunks, vendor chunks, all gzip + brotli pre-compressed).
  • Manual vendor splitting (React, Radix, charts, Supabase, forms, dates, xlsx, Sentry) — see rollupOptions.output.manualChunks.
  • PWA service worker emitted via vite-plugin-pwa (registerType: "prompt", injectManifest).

Required build-time env vars (CI sets these from GitHub secrets):

  • VITE_SUPABASE_URL
  • VITE_SUPABASE_PUBLISHABLE_KEY
  • VITE_SUPABASE_PROJECT_ID
  • VITE_TURNSTILE_SITE_KEY
  • VITE_GOOGLE_CLIENT_ID
  • VITE_GOOGLE_DRIVE_FOLDER_ID
  • VITE_SENTRY_DSN

The same list must be configured in Cloudflare Pages → Project → Settings → Environment variables for Production (and Preview if you want previews to talk to real Supabase).

Triggering a manual deploy

Frontend (Cloudflare Pages)

Auto-deploy usually works. When it stalls:

  1. Open Cloudflare dashboard → Workers & Pages → your project.
  2. Click Deployments → the failed build → Retry deployment.
  3. If the failure was a transient network/install issue, this is enough.
  4. If a secret or build command changed, edit it under Settings → Environment variables / Settings → Builds & deployments and retry.

To force a fresh build without a new commit:

# push an empty commit to main (only if you have the access)
git commit --allow-empty -m "chore: re-trigger deploy"
git push origin main

Edge Functions (Supabase)

Edge functions are not deployed by CI. Deploy manually from a local checkout with the Supabase CLI linked to the project (yzpfwdxpwalmfuodkxni per supabase/config.toml).

# one-time link
supabase link --project-ref yzpfwdxpwalmfuodkxni

# deploy a single function
supabase functions deploy razorpay-webhook

# deploy all functions in supabase/functions/
supabase functions deploy --all   # <!-- TODO: verify flag spelling against installed CLI version -->

Warning

Some functions in supabase/config.toml have verify_jwt = false (e.g. razorpay-webhook, push-send, whatsapp-webhook) because they authenticate the request body themselves (HMAC signature or internal bearer token). If you add a new function and forget the verify_jwt stanza, the gateway will 401 legitimate webhook calls.

Rolling back

Code-level rollback (preferred)

The cleanest rollback is a new commit that reverts the bad change.

  1. Identify the bad commit on main:
    git log --oneline -20
    
  2. Revert it:
    git revert <sha>
    git push origin main
    
  3. semantic-release classifies revert: as a patch release, cuts a new version, and Cloudflare Pages republishes. Within a few minutes production is back on the previous behaviour.

Cloudflare Pages rollback (fast path for frontend-only regressions)

Use when a revert would take too long (e.g. outage mid-incident).

  1. Cloudflare dashboard → Workers & Pages → project → Deployments.
  2. Find the last known-good production deployment.
  3. Click the ... menu → Rollback to this deployment.
  4. Confirm. Production now serves that build.
  5. Still cut the revert commit afterwards so the repo matches reality.

Danger

Cloudflare rollback only swaps the frontend. If the bad commit also shipped a database migration or Edge Function change, those are still live. You must additionally revert those — see database-migrations.md and the Edge Functions section above.

Edge Function rollback

supabase functions deploy overwrites in place; there is no built-in "previous version" button. To roll back, check out the previous commit and redeploy:

git checkout <previous-good-sha> -- supabase/functions/<function-name>
supabase functions deploy <function-name>
# then restore main and commit the revert properly

Database rollback

See database-migrations.md. Short version: write a new forward migration that undoes the bad change. Do not edit or delete already-applied migration files.

Secrets and environment variables

Canonical list of provider secrets lives in docs/secrets-setup.md. Quick map of where each class of secret belongs:

Secret class Stored in Example
Public / frontend build-time GitHub Actions secrets and Cloudflare Pages env vars VITE_SUPABASE_URL, VITE_SENTRY_DSN
Edge Function runtime Supabase project secrets (supabase secrets set) RESEND_API_KEY, RAZORPAY_WEBHOOK_SECRET, WHATSAPP_ACCESS_TOKEN
Backup workflow GitHub Actions secrets SUPABASE_DB_URL, AWS_*

Danger

Never commit secrets to the repo, even in env.example. env.example holds only public keys (anon key, Turnstile site key, Google client ID) and placeholder strings.

Monitoring a deploy

  1. Watch CI status: gh run watch or Actions tab on GitHub.
  2. Once green, watch the Cloudflare Pages Deployments tab for the new build.
  3. Hit the production URL, open devtools Console, confirm no CSP violations and no Sentry errors spiking.
  4. For backend changes: check Supabase dashboard → Edge Functions → Logs and Database → Logs for the first few minutes after deploy.