Production
What to change and verify before real users arrive: your defaults, secrets, authorization, database safety, CI and a launch checklist
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/service/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.jpgandpublic/og/og.mp4- the OpenGraph image and video (siteConfig.ogImage,siteConfig.ogVideo). Both are recorded from a small HTML scene inapps/web/scripts/og-video(the logo mark arcing in on its exhaust as the tagline rises in under it, hovering, then lifting off and coming round again, drawn with@repo/ui) (no running app or database needed): editscene.tsx, runpnpm -F @repo/web og:video(needs ffmpeg and a Playwright Chromium), then updateogVideo.uploadDate. Docs and marketing pages draw their own share cards at/og/docs/<slug>/image.pngand/og/pages/<slug>/image.png
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. The
requireSessionmiddleware requires a session;requireOrganization(both inpackages/service/src/orpc.ts) resolves the org and proves membership before any org-scoped handler runs
With Supabase's Data API off (Deployment), 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 turn the Data API on 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/service/src/email/send-email.ts); without the key they log to the server console instead of sending - Rate limiting - better-auth's database-backed limiter covers auth routes, but oRPC procedures (
/api/orpcand/api/v1) 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/service/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.jpgandog/og.mp4updated - 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