docs / deployment

Deployment

Vercel and Docker guides with env checklists

Before you deploy

  • Run the full gate locally: pnpm verify plus pnpm e2e.
  • Confirm the env matrix for your target mode — key names per mode live in the env matrix module; values come from your secret manager, never from the repo.
  • Decide the mode: demo for a public sample, production-kit for your own product.

Vercel checklist

  1. Import the repository into Vercel and select the Next.js app.
  2. Set the environment variables for your mode (see the env matrix; demo requires the Supabase URL/key pair, service-role key, and admin allowlist). Configure the Google provider credentials in the Supabase dashboard, not as app env vars.
  3. Point DATABASE_URL at your Supabase Postgres and run migrations: pnpm db:setup.
  4. Seed demo data when deploying the public sample: pnpm db:seed.
  5. Update the Google OAuth client and Supabase redirect list with the deployed callback, e.g. https://demo.buildgrain.com/auth/callback.
  6. Configure CRON_SECRET and verify the tracked daily /api/cron/demo-reset schedule. The route is demo-only, deletes only inactive unprotected personal demo clones, and runs in bounded batches.
  7. Smoke-check after deploy: sign in with Google, create a project, open the admin console, run an authorized cleanup probe against an eligible fixture, and confirm the boundary chips render on the public entry.

Docker checklist

The repository tracks Dockerfile, compose.yaml, and .dockerignore. For a clean local evaluation, reset the local Compose volumes and start the full dependency graph:

docker compose down --volumes
docker compose up --build

The dedicated one-shot setup service waits for Postgres, runs pnpm db:setup, then pnpm db:seed. The app service waits for setup to exit successfully before it runs the existing pnpm --filter web start contract, so migration, seed, and start cannot race on a clean volume.

  1. Build the app image with the provided Dockerfile and run it next to Postgres via Docker Compose.
  2. Supply env vars through an env file or your orchestrator's secret store — the compose file references key names only. Next.js inlines NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY into the client bundle, so Compose passes those two public values as build args and runtime env. Empty values produce the setup blocker. SUPABASE_SERVICE_ROLE_KEY stays runtime-only and is never a build arg.
  3. Let the one-shot setup service complete migrations and seed before the app container starts.
  4. Expose port 3000 and set the app callback URL to your host, e.g. http://localhost:3000/auth/callback for local Docker.
  5. For offline development, run Postgres locally and leave the OAuth keys unset — the login screen shows the setup blocker instead of failing silently.

Database resilience (serverless)

Concurrent-runtime hosts (Vercel Fluid Compute, AWS Lambda with response streaming) serve many requests per instance, so the Postgres client keeps a small pool per instance (DB_POOL_MAX, default 10) — a single shared connection turns one slow statement into instance-wide head-of-line blocking. Pair it with server-side guards so an abandoned transaction can never hold row locks indefinitely:

alter role postgres set idle_in_transaction_session_timeout = '30s';
alter role postgres set statement_timeout = '30s';

Both settings apply to new connections of the app role. The app also registers an instrumentation guard that logs unhandled promise rejections instead of letting them kill the process and every in-flight request with it.

Test-only rollback and reset verification

Before deployment, run NODE_ENV=test APP_MODE=test pnpm db:test:reset. The command refuses DATABASE_URL, uses only disposable in-memory PGlite databases, and proves both transaction rollback and a clean migrate-and-seed reset. It is intentionally not a production rollback command and never mutates deployment data.

Demo mode boundary

The public sample must keep its no-charge boundary: entitlement state is simulated, the billing provider slot stays disabled, and mail delivers to the local preview queue only. Never expose a test auth bypass or fake Google login on a public deployment.

Provider-gated webhook boundary

The repository contains Stripe webhook verification and lifecycle evaluation code behind provider keys. It stays off in demo mode and is not a complete paid flow or a purchasable offer. No checkout ships from this page or the public sample. Do not configure live provider keys until your own offer, legal terms, fulfillment path, and production controls have been implemented and reviewed.

The legacy lifecycle adapter uses one explicit tenant reference contract: metadata.buildgrain_account_id may name an existing Account, but account-only cancel/refund events are quarantined without changing entitlements. Exact product/offer-scoped revoke is owned by the connected billing pipeline, which preserves unrelated manual, plan-sync, provider, and product grants. Email and customer fields are never tenant selectors or audit metadata.

Post-deploy verification

  • /app/* and /admin/* guards respond correctly for signed-out users.
  • Tenant isolation: a second Google account cannot see the first account's workspace.
  • The demo cleanup route, deployment schedule, secret boundary, protected-account exclusion, and bounded deletion evidence agree.
  • pnpm check:ui-copy passes against the deployed branch — no forbidden public copy.