Skip to main content

Deployment

Where each part of Complied runs, what deploys it, and the checks to make before touching production. For engineers who merge to main or operate the environment. No secret values appear here; secrets live in the platform dashboards and repository secrets named below.

What runs where​

PartRuns onDeployed by
Web app (src/)Cloudflare Pages project compliedCloudflare's Git integration, automatically on push to main
Docs site (website/)Cloudflare Pages project complied-docs, at docs.compliednyc.comCloudflare's Git integration, automatically on push to main
Database, auth, storageSupabase production project (ref xtdawebsatjlqkmmtexk)Supabase GitHub integration applies migrations on merge to main
Edge functions (supabase/functions/)Same Supabase projectdeploy-functions.yml
Map query service (map-service/)AWS Lambda complied-map-query (us-east-2) behind a Function URL, snapshots in S3deploy-map-service.yml
Map exporter and CLI ingestionThe ingestion EC2 boxmap-service/deploy/ec2/install.sh by hand
PDF renderingCloudflare Browser Rendering (or any endpoint you host)Secrets on the Supabase project
Nightly public-data syncpg_cron inside Supabase, calling the sync-orchestrator functionA migration (ingestion)

Cloudflare Pages hosts the frontend and auto-deploys on push to main. The working cadence is: build and tests green, commit, push. The production Supabase project is separate from whatever the local CLI has linked (supabase/.temp/linked-project.json); always confirm the linked project ref before running supabase db push or functions deploy against anything meant for production.

CI​

ci.yml runs on every push and pull request on any branch. It never deploys.

JobRunsBlocking
lintnpm run lintNo (continue-on-error)
typechecknpx tsc --noEmitYes
buildnpm run buildYes
testnpm test (root vitest: src/** and supabase/functions/**)Yes
functionsnpm run check:functions (Deno type-check of every function; functions on the KNOWN_FAILING ratchet in scripts/check-functions.mjs are exempt)Yes
map-servicenpm run typecheck and npm test in map-service/ on Node 22Yes
guardrailsscripts/ci/guardrails.sh: a change that adds a SECURITY DEFINER function without a paired REVOKE ... FROM anon/PUBLIC fails; USING (true) in a new migration and un-gated verify_jwt = false functions only warnYes for the SECURITY DEFINER check
secret-scangitleaksNo (advisory)

The domain package's own suite (cd domain && npm test) and db-tests are not run by CI; run them locally (local development).

Web app​

Cloudflare Pages builds the repository root with npm run build and serves dist/. Production build-time variables are set on the complied Pages project:

VariableNotes
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEYRequired; the build's client throws at import time without them. Publishable values only.
VITE_MAP_SERVICE_URLRequired for /map. A production build has no fallback; unset means the map fails loudly.
VITE_MAPBOX_PUBLIC_TOKENMap canvas
VITE_MAPILLARY_TOKENOptional; street-level photos on an opened building

Tenants are served on subdomains (<tenant>.compliednyc.com); the tenant is read from the host. Edge-function CORS allows the apex, tenant subdomains and local dev hosts by pattern; any other origin, a Pages preview URL for example, must be named in the CORS_ALLOWED_ORIGINS function secret rather than matched by a wildcard.

Database migrations​

The Supabase GitHub integration applies supabase/migrations/ to production when a change merges to main, so merging a migration is a production database change. Before opening a PR:

  1. Rebase on main and confirm your migration's timestamp sorts last (ls supabase/migrations/*.sql | tail -3).
  2. Run it locally with npx supabase migration up --local.
  3. If it adds a SECURITY DEFINER function, pair it with a REVOKE EXECUTE ... FROM anon in the same change (security).

Production writes by hand (supabase db push, direct SQL) are for the owner to run, not for automation or agents; the pre-tool hook blocks them (conventions). Two production tables, buildings and public_events, additionally refuse deletes, truncates and drops at the database level.

Edge functions​

deploy-functions.yml runs on push to main when supabase/functions/**, domain/src/** or src/auth/permissionKeys.ts change (the functions import those by relative path), and on manual dispatch. It runs the Deno type-check job first and deploys only if it passes, then runs supabase functions deploy --project-ref xtdawebsatjlqkmmtexk for every function. It needs the repository secret SUPABASE_ACCESS_TOKEN.

Function behaviour that depends on configuration, not code:

  • supabase/config.toml marks sync-orchestrator, process-email-outbox, process-notification-digest, handle-email-suppression, handle-email-unsubscribe and request-password-reset as verify_jwt = false. Each performs its own authorization (security). All other functions keep the platform's JWT check.
  • Secrets are set per project with npx supabase secrets set NAME=value and read with Deno.env.get. The set the functions use: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN (or BROWSER_RENDER_ENDPOINT and BROWSER_RENDER_TOKEN), CRON_SECRET, NYC_OPEN_DATA_TOKEN, RESEND_API_KEY, RESEND_WEBHOOK_SECRET, EMAIL_FROM_ADDRESS, EMAIL_FROM_NAME, EMAIL_REPLY_TO, PUBLIC_APP_URL, CORS_ALLOWED_ORIGINS. SUPABASE_URL, SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY are provided by the runtime.
  • The nightly cron job sends CRON_SECRET, read from Supabase Vault (secret name sync_orchestrator_cron_secret), so the Vault value and the function secret must match.

A manual deploy of one function (npx supabase functions deploy <name>) is gated by the pre-tool hook, which requires a passing production build first.

PDF rendering​

Set the Cloudflare credentials on the production project and leave BROWSER_RENDER_ENDPOINT unset:

npx supabase secrets set CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=...

The token needs Browser Rendering: Edit (listed in Cloudflare's picker as "Browser Run"). The public report-fonts bucket must be populated in that project (node scripts/upload-report-fonts.mjs, with SEED_CONFIRM=<project-ref> for a non-local target). Workers Paid is recommended because the free plan's one-request-per-10-seconds limit slows concurrent reports; the function retries a 429 within a 90 s budget. To keep report data in-house, run any headless-Chrome service and set BROWSER_RENDER_ENDPOINT. Details and debugging: PDF reports.

Map service​

Production layout: the ingestion EC2 box exports Parquet snapshots to S3; a Lambda function loads the latest snapshot on cold start, polls latest.json, and serves the query API through a Function URL. Region us-east-2, CloudFormation stack complied-map.

Code deploys. On push to main touching map-service/**, deploy-map-service.yml runs typecheck and tests, builds the arm64 image, pushes it to ECR tagged with the commit, runs aws lambda update-function-code on complied-map-query, waits for the update, and calls /health. AWS access uses the repository secret AWS_DEPLOY_ROLE_ARN, an IAM role assumed through GitHub's OIDC provider (id-token: write), with permission only for ECR push and updating that one function. This repo uses GitHub's immutable subject claim, so the role's trust policy sub must match repo:Martial-Geek@<owner-id>/complied@<repo-id>:ref:refs/heads/main; check the live format with gh api repos/Martial-Geek/complied/actions/oidc/customization/sub. A deploy-only IAM user (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) is accepted as an alternative; with neither, the workflow runs the tests and skips the deploy with a warning.

Infrastructure changes. The workflow never touches the stack. A change to template.yaml (memory, environment, alarms) needs a manual sam deploy with the current ImageUri. The snapshot bucket is created once by an account admin (the deploy user is denied s3:CreateBucket); the stack takes its name as a parameter. CORS is handled only in the app through MAP_ALLOWED_ORIGINS; leave Function URL CORS unset, because duplicate Access-Control-Allow-Origin headers break browsers. Step-by-step first-time setup (image build, bucket, sam deploy, smoke test): map-service/deploy/README.md.

Exporter and schedulers. Versioned under map-service/deploy/ec2/ and installed with install.sh, with MAP_SNAPSHOT_ROOT=s3://<bucket>/map in /opt/complied/.env and no static AWS keys (the instance role carries the stack's exporter policy):

TimerWhen (UTC)What
complied-map-export-daily14:00Force an export; pauses backfill to free RAM
complied-map-export-hourlyHourlyexport --if-stale, a no-op when current
complied-map-export-morning08:20 to 09:55, every 5 minutesexport --if-stale during the sync's quiet window
complied-ingest-delta08:10Delta ingestion only; does not chain an export

The exporter's Postgres login should be a dedicated read-only role. Pass its password through PGPASSWORD or a secret store, never in a URL that ps can show.

Ordering when the map's contract changes. The frontend calls VITE_MAP_SERVICE_URL with no production fallback, so a frontend that needs a new service behaviour must not reach main first. Apply any migrations, let deploy-map-service.yml ship the service and confirm /health shows a snapshot, run an export if the snapshot schema changed, redeploy sync-orchestrator if it needs a new RPC, then merge the frontend. Architecture and configuration: map and map-service/AGENTS.md.

Docs site​

The documentation site is a separate Cloudflare Pages project from the app, so a docs-only change ships without touching the app.

SettingValue
Projectcomplied-docs
Domainhttps://docs.compliednyc.com
Production branchmain
Root directorywebsite
Build commandnpm ci && npm run build
Output directorybuild
Node version20

Content is canonical markdown under docs/ plus the root REQUIREMENTS.md; website/scripts/sync-docs.mjs copies it into a gitignored website/docs/ at build time, rewrites out-of-tree and source-code links to GitHub, and the build doubles as the link checker. Never author inside website/docs/.

A pull request that only changes docs/** or website/** publishes docs on merge and leaves the app project alone. Non-main branches get Pages preview builds. Manual deploy:

cd website && npm ci && npm run build
npx wrangler pages deploy build --project-name=complied-docs --branch=<name>

Do not point the app's Pages project at website/. More: website/README.md.

Pre-deploy checklist​

  • npm run build and npm run test pass locally; npm run check:functions passes if you touched a function.
  • The linked Supabase project ref is the one you mean (cat supabase/.temp/linked-project.json).
  • Any new migration sorts last and is idempotent where it needs to be.
  • A migration that adds a table has RLS enabled with permission-key policies (security).
  • Any new secret is set on the target project before the code that reads it ships.