Deployment
Ship Init to production. The web app targets Vercel; the database targets Vercel Postgres. Mobile, extension, and desktop each have their own pipeline.
Web App (Vercel)
1. Create the Project
Import the repo at vercel.com/new:
- Root Directory:
apps/web - Vercel detects Next.js and Turborepo automatically - no
vercel.jsonneeded
Or via CLI:
npm i -g vercel
vercel login
vercel # from apps/web2. Environment Variables
Set in the Vercel dashboard (same keys as .env.example):
# Database — the pooled connection string, plus the direct one for schema pushes
POSTGRES_URL=postgresql://...
POSTGRES_URL_NON_POOLING=postgresql://...
# Auth
BETTER_AUTH_SECRET=generate-with-openssl-rand--base64-32
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# Avatar uploads (Vercel Blob)
BLOB_READ_WRITE_TOKEN=...The auth base URL derives from Vercel's built-in VERCEL_ENV / VERCEL_PROJECT_PRODUCTION_URL / VERCEL_URL - nothing to set. Creating the database under the project's Storage tab populates POSTGRES_URL, and creating a Blob store there populates BLOB_READ_WRITE_TOKEN.
3. Preview Deployments
The oAuthProxy plugin in packages/api/src/auth/auth.ts routes OAuth callbacks through the production URL, so GitHub login works on preview deployments without registering every preview domain.
4. Custom Domain
- Add your domain in the Vercel dashboard
- Update DNS as instructed
- SSL is automatic
- Update your GitHub OAuth app's callback URL
Database (Vercel Postgres)
-
Create the database from the Vercel dashboard under Storage, which wires
POSTGRES_URLandPOSTGRES_URL_NON_POOLINGinto the project -
Push the schema:
# .env.production.local at the repo root with the production POSTGRES_URL # and POSTGRES_URL_NON_POOLING (`vercel env pull` writes both) pnpm db:push-remotedrizzle-kit pushhangs behind a transaction-mode pooler, so the config prefersPOSTGRES_URL_NON_POOLINGand falls back toPOSTGRES_URL.
Any Postgres works - Vercel Postgres is the default because of branching (below), not because anything in the code depends on it.
RLS is already on for every table with no policies - deny by default. Nothing exposes Postgres directly, so this is belt-and-braces; authorization lives in the oRPC procedures, and the server connection bypasses RLS as table owner.
Branching
Branches are copy-on-write, so a branch is a full database with production's data at a fraction of the storage. Two uses:
- Preview deployments - the Vercel integration creates a branch per preview automatically, so PRs run against real-shaped data without touching production
- Agents and experiments - a branch created from the database's dashboard gives an isolated database to migrate against or destroy freely
Delete a branch when you're done; nothing else in the stack needs to know it existed.
Avatar Storage (Vercel Blob)
Create a Blob store in the Vercel dashboard (Storage → Create → Blob) and add BLOB_READ_WRITE_TOKEN to the project. Uploads go through /api/account/avatar, which checks the better-auth session, caps size at 1MB, and derives the stored Content-Type from the file's magic bytes rather than the client-supplied MIME type.
There is no local emulator for Blob. Without the token that one route returns 501 with a pointer; the rest of the app is unaffected, so local development doesn't need a store.
Mobile (EAS)
Production builds need to know where the API lives:
// eas.json build profile env
{ "EXPO_PUBLIC_API_URL": "https://your-domain.com" }Dev builds derive the URL from Metro. Then follow the standard EAS Build flow.
Extension and Desktop
pnpm -F @repo/extension build # wxt build → .output/chrome-mv3
pnpm -F @repo/desktop build # electron-vite build; package with electron-builderUpload the extension to the Chrome Web Store; distribute desktop builds per electron-builder.yml.
CI/CD
Vercel deploys on push once the repo is connected - previews per PR, production on main. Schema pushes stay manual (pnpm db:push-remote) or go in a workflow step with the direct connection string as a secret:
- name: Push database schema
env:
POSTGRES_URL_NON_POOLING: ${{ secrets.POSTGRES_URL_NON_POOLING }}
run: pnpm -F db drizzle:kit push --forceRun checks before merging:
pnpm lint && pnpm typecheck && pnpm test && pnpm buildHealth Checks
The app ships a liveness endpoint at /api/health (apps/web/src/app/api/health/route.ts). Point your uptime monitor at it:
curl https://your-domain.com/api/health
# {"status":"ok"}Rollback
vercel ls # List deployments
vercel rollback [deployment-url] # Roll back the web appdrizzle-kit push has no down migrations - take a backup before risky schema changes:
pg_dump $POSTGRES_URL > backup_$(date +%Y%m%d_%H%M%S).sqlVercel Postgres also keeps point-in-time restore over a retention window, and a branch taken before a migration is itself a cheap rollback target.
Production Checklist
BETTER_AUTH_SECRETset and unique per environment- Separate GitHub OAuth apps for dev and prod (callback:
https://your-domain.com/api/auth/callback/github) POSTGRES_URLuses the pooled endpoint - the client is configured for it;POSTGRES_URL_NON_POOLINGis the direct one for schema pushesBLOB_READ_WRITE_TOKENset, with a Blob store created in the Vercel project- Uptime monitor on
/api/health
Next Steps
- Monitoring - Set up monitoring and observability
- Production checklist - Review production best practices