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
| Concern | Service |
|---|---|
| Hosting | Vercel (apps/saas + apps/marketing) |
| Database | Turso (libSQL/SQLite) |
| Background jobs | Inngest Cloud |
| Resend | |
| Object storage | S3-compatible (R2, Backblaze, AWS) |
| Payments | Stripe (parked behind NEXT_PUBLIC_BILLING_ENABLED) |
| Observability | Sentry (@sentry/nextjs, DSN-gated) |
| Inference | OpenRouter (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
- Provision Turso —
turso db create brandbanta-prod, capture the URL + auth token - Provision Inngest Cloud — create app, get
INNGEST_SIGNING_KEY+INNGEST_EVENT_KEY - Configure Sentry — create project, get DSN, create Personal Access Token with
Read & Writeon Project + Release scopes - Provision Resend — verify your sending domain, get API key
- Provision S3-compatible storage — bucket name + access key/secret
- Push to Vercel — connect the repo, configure both
apps/saas+apps/marketingprojects - Set env vars on Vercel (see Env vars)
- Run migrations —
pnpm --filter @repo/database migrateagainst the prod Turso URL - Verify crons —
vercel.jsondeclares 11 cron paths; check they show up in Vercel dashboard - 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 migratereturns "no pending migrations") -
__drizzle_migrationstable populated correctly viabackfill-drizzle-migrations.mjsif the schema was pushed before migrations existed -
CRON_SECRETset and matches between Vercel env andvercel.jsonheaders -
STRIPE_WEBHOOK_SECRETset if billing is enabled -
SENTRY_DSNset 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
| Surface | Vercel project | Domain |
|---|---|---|
| Marketing site | apps/marketing | <your-domain> |
| SaaS app | apps/saas | app.<your-domain> |
| Docs site (this one!) | apps/docs | docs.<your-domain> |
| Email previews | apps/mail-preview | dev-only, not deployed |
The three production sites share one Turso database and one Inngest Cloud app.
Local dev parity
To match production locally:
- Local Turso —
turso devruns a local libSQL server. Or use a local SQLite file viaDATABASE_URL=file:./local.db - Local Inngest —
pnpm dev:inngestruns the Inngest dev server. The saas app'sinngest.config.tsauto-detects local vs cloud - No Sentry locally — leave
SENTRY_DSNunset; 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