Operators

Deploying BrandBanta

This is the high-level deploy path. For the full step-by-step with exact commands, see DEPLOY.md in the repo root — it's the authoritative checklist and gets updated as we tighten the deploy story.

Stack overview

ConcernService
HostingVercel (apps/saas + apps/marketing)
DatabaseTurso (libSQL/SQLite)
Background jobsInngest Cloud
EmailResend
Object storageS3-compatible (R2, Backblaze, AWS)
PaymentsStripe (parked behind NEXT_PUBLIC_BILLING_ENABLED)
ObservabilitySentry (@sentry/nextjs, DSN-gated)
InferenceOpenRouter (BYOK per workspace)

See the root COST.md (in the repo) for daily cost inspection commands, OpenRouter reconciliation, and how the Settings → Cost page + ?day= views work. | Brand grounding | Tavily (optional, for AI alias suggester) |

Each is loosely coupled — you can swap providers later, but this stack is what's wired today.

High-level deploy order

  1. Provision Tursoturso db create brandbanta-prod, capture the URL + auth token
  2. Provision Inngest Cloud — create app, get INNGEST_SIGNING_KEY + INNGEST_EVENT_KEY
  3. Configure Sentry — create project, get DSN, create Personal Access Token with Read & Write on Project + Release scopes
  4. Provision Resend — verify your sending domain, get API key
  5. Provision S3-compatible storage — bucket name + access key/secret
  6. Push to Vercel — connect the repo, configure both apps/saas + apps/marketing projects
  7. Set env vars on Vercel (see Env vars)
  8. Run migrationspnpm --filter @repo/database migrate against the prod Turso URL
  9. Verify cronsvercel.json declares 11 cron paths; check they show up in Vercel dashboard
  10. Smoke test — sign up, run a scan, view insights, check Sentry receives a synthetic event

Production-ready checklist

Before flipping DNS to the production URL:

  • All migrations applied (drizzle-kit migrate returns "no pending migrations")
  • __drizzle_migrations table populated correctly via backfill-drizzle-migrations.mjs if the schema was pushed before migrations existed
  • CRON_SECRET set and matches between Vercel env and vercel.json headers
  • STRIPE_WEBHOOK_SECRET set if billing is enabled
  • SENTRY_DSN set on Vercel — verify with a test error in browser console
  • At least one cron has fired successfully (check Vercel cron logs)
  • Inngest dashboard shows the registered functions matching your codebase
  • Resend domain verified + at least one test email landed in inbox (not spam)
  • DNS configured: app.<your-domain> → saas project, <your-domain> → marketing project
  • HTTPS enforced (Vercel auto-issues via Let's Encrypt; no opt-in needed)

What runs where

SurfaceVercel projectDomain
Marketing siteapps/marketing<your-domain>
SaaS appapps/saasapp.<your-domain>
Docs site (this one!)apps/docsdocs.<your-domain>
Email previewsapps/mail-previewdev-only, not deployed

The three production sites share one Turso database and one Inngest Cloud app.

Local dev parity

To match production locally:

  • Local Tursoturso dev runs a local libSQL server. Or use a local SQLite file via DATABASE_URL=file:./local.db
  • Local Inngestpnpm dev:inngest runs the Inngest dev server. The saas app's inngest.config.ts auto-detects local vs cloud
  • No Sentry locally — leave SENTRY_DSN unset; the SDK becomes a no-op
  • No Stripe locally — leave NEXT_PUBLIC_BILLING_ENABLED=false (default)

What's deliberately not in this guide

  • Domain setup — varies by registrar; Vercel's docs are the source of truth
  • Stripe products + price IDs — set up in the provider dashboard; map them with the PRICE_ID_* environment variables after products exist
  • Custom email templates — under packages/mail/emails/; out of scope for first deploy

See also

  • Env vars — every variable, required vs optional
  • Cron jobs — what each cron does, schedule, auth
  • DEPLOY.md — authoritative step-by-step (in the repo root)

On this page