Production
What to verify before real users arrive. Deployment gets you live; this page keeps you safe once you are.
Make It Yours
Init ships with the author's defaults baked in. Change these before launch:
apps/web/src/lib/site-config.ts- name, description, production URL, Twitter handle.siteConfig.urlfeeds metadata, sitemap, robots, andllms.txtpackages/api/src/auth/auth.ts-oAuthProxyderivesproductionURLfromVERCEL_PROJECT_PRODUCTION_URL; deploying off Vercel means setting that env var (or replacing the derivation)apps/web/src/app/(docs)/docs/layout.tsx-githubUrlpoints at the upstream repopublic/og.jpg- the OpenGraph image referenced insrc/app/layout.tsx
Secrets
BETTER_AUTH_SECRET- unique per environment,openssl rand -base64 32. Rotating it invalidates existing sessions- Separate GitHub OAuth apps for dev and prod; prod callback is
https://your-domain.com/api/auth/callback/github POSTGRES_URLandBLOB_READ_WRITE_TOKENare full-access credentials - server code only (the avatar route,apps/web/src/app/api/account/avatar/route.ts, is the token's only consumer)- Nothing secret in
NEXT_PUBLIC_*- those ship to the browser
The Authorization Model
Two layers, know both:
- RLS deny-by-default - every table is declared through
pgTable.withRLSwith no policies, so any connection that isn't the table owner gets nothing - oRPC procedures - the real authorization.
protectedProcedurerequires a session;organizationProcedure(schema)(inpackages/api/src/orpc.ts) resolves the org and proves membership before any org-scoped handler runs
Nothing in this stack puts Postgres on the network, so the oRPC procedures are the only way in and RLS is the backstop rather than the gate. Keep it that way: if you ever enable a hosted data API or hand out a lower-privileged role, deny-by-default is what stops a forgotten table from being world-readable. If you regenerate the auth schema, declare every table through pgTable.withRLS again.
What Init Doesn't Ship
Honest gaps. Each is a deliberate "add when you need it":
- Email delivery - set
RESEND_API_KEY(andEMAIL_FROM) in production. Password reset and organization invite emails send through Resend's REST API (packages/api/src/email/send-email.ts); without the key they log to the server console instead of sending - Rate limiting - better-auth's built-in limiter covers auth routes (in-memory, so per-instance on serverless), but oRPC procedures have none. Add Vercel WAF rules or a middleware backed by a shared store
- Billing - Stripe via
@better-auth/stripe. SetSTRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET, andSTRIPE_PRO_PRICE_ID, then point a Stripe webhook at/api/auth/stripe/webhook(events:checkout.session.completed,customer.subscription.created/updated/deleted). The webhook keeps thesubscriptiontable in sync;dashboard/[slug]/billinghandles checkout and the billing portal. Subscriptions are org-scoped — only owners/admins can manage them - Email verification - verification emails send on signup. Uncomment
requireEmailVerificationinpackages/api/src/auth/auth.tsto block unverified logins - Background jobs - no queue. Long work either fits in a request or needs Inngest/Trigger.dev/QStash
Database Safety
drizzle-kit pushhas no down migrations. Back up before schema changes:pg_dump $POSTGRES_URL > backup.sql- Prefer add-column → backfill → drop-column over destructive changes on live data
POSTGRES_URLshould be the pooled endpoint; the client already setsprepare: falsefor it, anddrizzle.config.tsprefersPOSTGRES_URL_NON_POOLINGso DDL runs on a direct connection
CI Gate
.github/workflows/ci.yml runs on every PR and push to main:
pnpm typecheck && pnpm lint && pnpm format && pnpm test && pnpm buildGreen CI is the merge bar. Note apps/web/next.config.ts sets typescript.ignoreBuildErrors: true - type safety is enforced by the typecheck step, not the build, so don't skip it.
Launch Checklist
-
site-config.ts(name, description, Twitter),githubUrl,og.jpgupdated - Unique
BETTER_AUTH_SECRET; separate GitHub OAuth apps - Production schema pushed; Vercel Blob store created and
BLOB_READ_WRITE_TOKENset -
RESEND_API_KEY+EMAIL_FROMset (verification, password reset, invite emails) - Stripe keys set + webhook pointed at
/api/auth/stripe/webhook - Uptime monitor on
/api/health - Error tracking installed (Monitoring)
- Database backup taken; restore path tested once
Next Steps
- Monitoring - Know when it breaks
- Analytics - Know how it's used