Deployment
Deploy Unstack Pro to production
Deploy to production in minutes. Auth is already configured — set your production environment variables and go live.
How many surfaces you configure depends on which edition you run. Pick yours with the switcher below — the choice is remembered across every page in these docs.
One surface: your host's environment variables. Better Auth runs inside Next.js and talks to Postgres directly, so there is nothing else to populate.
Two surfaces: your host's environment variables for the Next.js side, and the Convex deployment environment for everything Better Auth itself reads.
Prerequisites
Before deploying, ensure you have:
- Completed local development setup
- Resend account for email sending
- Autumn account for billing
- Vercel account (free tier works)
- Domain name (optional but recommended)
- All environment variables documented
- A production PostgreSQL database, separate from your development one, and
its connection string. Any provider works — the app connects with
pg(node-postgres), not a vendor-specific driver.
- A Convex account and project with a production deployment
- A Convex production deploy key, for the build to push with
The Build Command
Both editions ship a vercel.json whose buildCommand overrides the Build
Command field in the Vercel dashboard. Typing a command into that field has no
effect — if you need to change what a deploy runs, edit vercel.json.
This is the step most first deploys get wrong. Leave the dashboard's Build
Command on its default and read what vercel.json actually does below — the
production and preview paths are deliberately different.
{
"buildCommand": "if [ \"$VERCEL_ENV\" = \"production\" ]; then bunx drizzle-kit migrate && bun run build; else bun run build; fi"
}Production deploys apply every pending migration in drizzle/ to
DATABASE_URL before building. && means a failed migration aborts the
deploy, so a broken migration never ships alongside the code that depends on it.
Preview deploys skip the migrate step entirely and build against whatever schema the database they point at already has. If previews share one database, plan your migrations to be backward compatible — a preview built from a branch with new columns will run against a database that does not have them yet.
Self-hosting (anywhere that is not Vercel) means nothing runs migrations for
you. Apply them against the production DATABASE_URL before starting the new
build:
bun run db:migrate
bun run build{
"buildCommand": "if [ \"$VERCEL_ENV\" = \"production\" ]; then bunx convex deploy --cmd 'bun run build'; else bun run build; fi"
}Production deploys run convex deploy, which pushes your functions and
schema to the production Convex deployment and then runs the Next.js build with
the production Convex URLs injected.
Preview deploys only build — they do not push anything to Convex.
convex deploy authenticates with a CONVEX_DEPLOY_KEY environment
variable, which must be set in Vercel (not on the Convex deployment).
Generate a production deploy key in the Convex dashboard under Settings → Deploy
Keys and add it to your Vercel project. Without it, the production build fails
at the deploy step — the preview builds that skip convex deploy will still
succeed, which makes this easy to miss until your first production deploy.
In both editions bun run build is tsgo --noEmit && next build --turbopack,
so a type error fails the deploy.
Deployment Steps
Prepare Your Repository
-
Commit all changes:
git add . git commit -m "Ready for deployment" -
Push to GitHub:
git push origin HEAD # your default branch (master or main) -
Verify
.gitignoreincludes:.env.local.envnode_modules/.next/
Prepare the Production Backend
Create the production database at your provider and copy its connection string —
most hosted providers require ?sslmode=require. That string becomes
DATABASE_URL in the next step.
You do not have to run the migrations by hand: on Vercel, the production build applies them for you. If you want the schema in place before the first deploy, or you are self-hosting, run them yourself against the production URL:
DATABASE_URL="postgresql://…" bunx drizzle-kit migrate-
Open the Convex dashboard, select your project, and note the production deployment name under Settings.
-
Set the deployment-side variables —
BETTER_AUTH_SECRET,EMAIL_FROM,RESEND_API_KEY,AUTUMN_SECRET_KEY,PASSKEY_RP_ID,NEXT_PUBLIC_APP_URLandNEXT_PUBLIC_APP_NAME— with production values, either from Settings → Environment Variables or with the CLI:bunx convex env set NAME "value" --prodNEXT_PUBLIC_APP_URLmust be thehttps://production origin, andPASSKEY_RP_IDthe bare domain that serves it. -
Push the functions so
betterAuth/authexists on the production deployment —bunx convex deploy, or let your first Vercel production deploy do it. -
Generate the production
JWKS.BETTER_AUTH_SECRETencrypts the private key, so set that first:bunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks --prod)" --prodTo rotate later:
bunx convex env set JWKS "$(bunx convex run betterAuth/auth:rotateKeys --prod)" --prod
Rotating JWKS logs out every existing user. Do it during low-traffic periods.
Use different secrets for production. Never reuse development secrets.
Import the Project into Vercel
-
Go to vercel.com
-
Import your repository:
- Click "New Project"
- Import from GitHub
- Select your repository
-
Configure project:
- Framework Preset: Next.js
- Root Directory:
./ - Build Command: leave it alone —
vercel.jsonoverrides it. See The Build Command. - Output Directory:
.next
Add Environment Variables
Everything goes in the Vercel project's environment variables — there is no second surface:
DATABASE_URL— the production connection stringNEXT_PUBLIC_APP_URL— your production domain, onhttps://NEXT_PUBLIC_APP_NAME— your application nameBETTER_AUTH_SECRET— a new value, not the development oneEMAIL_FROM— a sender on your verified Resend domainRESEND_API_KEY— production keyAUTUMN_SECRET_KEY— production Autumn keyPASSKEY_RP_ID— the bare deployed domain: no scheme, no portNEXT_PUBLIC_SUPPORT_EMAIL— optionalNEXT_PUBLIC_SENTRY_DSN,SENTRY_ORG,SENTRY_PROJECT,SENTRY_AUTH_TOKEN— optional, leave unset to disable Sentry
Only the Next.js side goes in Vercel; the rest was set on the Convex deployment in the previous step.
CONVEX_DEPLOY_KEY— the production deploy key, required by the buildNEXT_PUBLIC_APP_URL— your production domain, onhttps://NEXT_PUBLIC_APP_NAME— your application nameNEXT_PUBLIC_CONVEX_URL— production Convex URL (….convex.cloud)NEXT_PUBLIC_CONVEX_SITE_URL— production Convex site URL (….convex.site)AUTUMN_SECRET_KEY— also required on the Convex deployment; the Next.js billing routes read it here at runtimeNEXT_PUBLIC_SUPPORT_EMAIL,NEXT_PUBLIC_PURCHASE_URL— optionalNEXT_PUBLIC_SENTRY_DSN,SENTRY_ORG,SENTRY_PROJECT,SENTRY_AUTH_TOKEN— optional, leave unset to disable Sentry
BETTER_AUTH_SECRET, JWKS, EMAIL_FROM, RESEND_API_KEY and
PASSKEY_RP_ID live on the Convex deployment, not on Vercel.
AUTUMN_SECRET_KEY is the one variable needed in both places.
PASSKEY_RP_ID and NEXT_PUBLIC_APP_URL must agree: the former is the bare
domain, the latter the matching https:// origin.
Then Deploy, and wait for the build to finish.
Configure Custom Domain (Optional)
-
In Vercel project settings:
- Go to "Domains"
- Add your domain
- Follow DNS instructions
-
Update the app's environment variables:
NEXT_PUBLIC_APP_URL=https://yourdomain.com PASSKEY_RP_ID=yourdomain.com
Both live in Vercel. Redeploy afterwards — NEXT_PUBLIC_* values are inlined at
build time, so an edit alone does not reach the running app.
NEXT_PUBLIC_APP_URL has to be updated in Vercel and on the Convex
deployment, and PASSKEY_RP_ID on the Convex deployment, where Better Auth
reads it:
bunx convex env set NEXT_PUBLIC_APP_URL "https://yourdomain.com" --prod
bunx convex env set PASSKEY_RP_ID "yourdomain.com" --prodRedeploy afterwards — NEXT_PUBLIC_* values are inlined at build time.
DNS changes can take up to 48 hours but usually propagate within minutes.
Verify Deployment
-
Visit your production URL
-
Test authentication:
- Register a new account
- Check email verification
- Test OTP login
- Try passkey (if on HTTPS)
-
Confirm the data landed and check the logs:
Query the production database (select * from "user";) or open your provider's
table browser, and read the build log to confirm the migrate step ran.
Open the Convex dashboard, watch the user table fill in, and check the
production deployment logs.
- Test key features:
- Create organization
- Invite members
- Test 2FA
- Verify admin panel (if applicable)
Post-Deployment
Set Up Email Domain
For production emails, configure your domain in Resend:
- Add domain in Resend dashboard
- Add DNS records provided by Resend
- Verify domain
- Update
EMAIL_FROMto use your domain
Until domain is verified, emails may go to spam or fail to send.
Configure Billing
If using Autumn for payments:
- Set up an Autumn account and run
bunx atmn pushagainst your live account - Get your production secret key (starts with
am_sk_) - Set
AUTUMN_SECRET_KEYwhere the edition needs it - Test the checkout and portal flows
One place — your host's environment. The billing routes
(app/api/autumn/[...all]/route.ts) read it at runtime.
AUTUMN_SECRET_KEY must be set on both Vercel (the Next.js billing routes
read it at runtime) and the Convex deployment (the auth hooks read it for seat
syncing and the plan gate). Missing it in either place breaks billing.
Set Up Monitoring (Optional)
Sentry is environment-variable driven — no code changes are needed. Set these in Vercel to enable it:
NEXT_PUBLIC_SENTRY_DSN— your Sentry project DSN (when empty, Sentry is disabled)SENTRY_ORG,SENTRY_PROJECT— used at build timeSENTRY_AUTH_TOKEN— enables source-map upload during the build
Leave NEXT_PUBLIC_SENTRY_DSN unset to disable Sentry entirely — the SDK no-ops,
and builds skip source-map upload when SENTRY_AUTH_TOKEN is missing.
Environment Variables Checklist
Vercel (or your host's environment) — the only surface:
-
DATABASE_URL(production database) -
NEXT_PUBLIC_APP_URL(your domain,https://) -
NEXT_PUBLIC_APP_NAME(your app name) -
BETTER_AUTH_SECRET(new, secure value) -
EMAIL_FROM(verified domain) -
RESEND_API_KEY(production key) -
AUTUMN_SECRET_KEY(production key) -
PASSKEY_RP_ID(your bare domain) -
NEXT_PUBLIC_SUPPORT_EMAIL(optional) -
NEXT_PUBLIC_SENTRY_DSN+SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN(optional)
Vercel (Next.js runtime and build):
-
CONVEX_DEPLOY_KEY(production deploy key — the build fails without it) -
NEXT_PUBLIC_APP_URL(your domain,https://) -
NEXT_PUBLIC_APP_NAME(your app name) -
NEXT_PUBLIC_CONVEX_URL(production.convex.cloud) -
NEXT_PUBLIC_CONVEX_SITE_URL(production.convex.site) -
AUTUMN_SECRET_KEY(also on Convex) -
NEXT_PUBLIC_SUPPORT_EMAIL,NEXT_PUBLIC_PURCHASE_URL(optional) -
NEXT_PUBLIC_SENTRY_DSN+SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN(optional)
Convex deployment:
-
BETTER_AUTH_SECRET(new, secure value) -
JWKS(generated withbunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks --prod)" --prod) -
RESEND_API_KEY(production key) -
EMAIL_FROM(verified domain) -
AUTUMN_SECRET_KEY(also on Vercel) -
PASSKEY_RP_ID(your bare domain) -
NEXT_PUBLIC_APP_URLandNEXT_PUBLIC_APP_NAME
Continuous Deployment
Vercel redeploys automatically when you push to your default branch, and creates a preview deployment for every pull request.
git add .
git commit -m "Update feature"
git push origin HEADWhat that push runs is decided by vercel.json, and production and preview take
different paths:
A push to the default branch migrates DATABASE_URL, then builds. A pull-request
preview only builds — so a PR that adds a column runs against a database that
does not have it yet unless the migration has already been applied to whatever
database that preview points at.
A push to the default branch runs convex deploy — pushing functions and schema
to the production deployment — and then builds. A pull-request preview only
builds, against whichever Convex deployment its NEXT_PUBLIC_CONVEX_URL names.
Troubleshooting
Build Fails
Check:
- Build logs in Vercel dashboard
- All dependencies in
package.json - TypeScript errors locally —
bun run buildtype-checks before it builds - Environment variables set correctly
If the log fails before the Next.js build even starts, it is the migrate step:
DATABASE_URL is unreachable from the build, or a migration errored. The &&
is doing its job — fix the migration rather than removing it.
A production build that fails at convex deploy with an authentication error is
missing CONVEX_DEPLOY_KEY in Vercel. Preview builds skip that step, so they
keep passing while production fails.
Authentication Not Working
Verify:
NEXT_PUBLIC_APP_URLmatches your actual domain- Cookies are not blocked (HTTPS required)
BETTER_AUTH_SECRETis set in the host environment and different from dev- Every migration in
drizzle/has been applied to the production database
BETTER_AUTH_SECRETis set on the production Convex deployment and different from devJWKSis set on the production deployment
Emails Not Sending
Check:
RESEND_API_KEYis production key- Domain is verified in Resend
EMAIL_FROMuses verified domain- DNS records are configured
- Check Resend logs
In the Convex edition both variables belong on the Convex deployment — email is sent from there, not from Next.js.
Passkeys Not Working
Ensure:
- Using HTTPS (not HTTP)
PASSKEY_RP_IDmatches domain- Domain doesn't have port number
- Browser supports WebAuthn
The App Can't Reach Its Data
Verify:
DATABASE_URLis set in the host environment and reachable from it- The connection string carries
?sslmode=requireif your provider needs it - The provider's connection limit is not exhausted
- The migrations actually ran — check the build log
Verify:
- The production Convex deployment is active
NEXT_PUBLIC_CONVEX_URLandNEXT_PUBLIC_CONVEX_SITE_URLare the production URLs and point at the same deployment- Convex deployment environment variables are set
- Check the Convex dashboard logs
Performance Optimization
Optimize Images
Next.js automatically optimizes images. Use the Image component:
import Image from 'next/image';
<Image
src="/logo.png"
alt="Logo"
width={200}
height={200}
/>Security Best Practices
- HTTPS Only: Never use HTTP in production
- Strong Secrets: Generate with
openssl rand -base64 32 - Different Secrets: Dev and prod must be different
- Environment Variables: Never commit to git
- CORS: Configure appropriately for your domain
- Rate Limiting: Consider adding rate limiting
- Security Headers: Vercel sets these by default
- Regular Updates: Keep dependencies updated
Scaling
The Next.js side scales automatically on Vercel — serverless functions grow with traffic and there is no infrastructure to manage.
Your database is the part that does not auto-scale. Serverless functions open connections independently, so watch your provider's connection limit and use its pooled connection string if it offers one.
Convex scales on its own — there is nothing to size or tune.
Backup Strategy
Database Backups
Backups are your provider's feature — enable point-in-time recovery or scheduled
snapshots there. For an ad-hoc dump, pg_dump against DATABASE_URL works like
any other Postgres database.
Convex backs up your data automatically. To export a copy, open the dashboard, go to Settings → Export, and download the backup.
Code Backups
Your code is already backed up in GitHub. Ensure:
- Regular commits
- Protected main branch
- Pull request reviews
- Tagged releases
Monitoring
Vercel Analytics
Enable in Vercel dashboard for:
- Page views
- Performance metrics
- User analytics
Backend Logs
Server-side errors surface in Vercel's function logs, since auth and data access both run inside Next.js. Slow-query and connection metrics come from your database provider's dashboard.
Monitor in the Convex dashboard:
- Function calls
- Error rates
- Performance
- Data changes
Sentry (Optional)
For error tracking:
- Catches runtime errors
- Tracks error trends
- User impact analysis
- Source maps for debugging
Support
If issues persist:
- Check Vercel Docs
- Check Better Auth Docs
- Review GitHub issues
- Contact support
- Convex Docs
- kitcn — the Better Auth + Convex integration
Next Steps
After deployment:
- Test all features thoroughly
- Set up monitoring
- Configure backup schedule
- Plan for scaling
- Document custom changes
- Train team on admin panel
- Set up staging environment (optional)
Consider setting up a staging environment that mirrors production for testing changes before they go live.