Unstack Pro Docs

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.

vercel.json
{
    "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
vercel.json
{
    "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

  1. Commit all changes:

    git add .
    git commit -m "Ready for deployment"
  2. Push to GitHub:

    git push origin HEAD   # your default branch (master or main)
  3. Verify .gitignore includes:

    • .env.local
    • .env
    • node_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
  1. Open the Convex dashboard, select your project, and note the production deployment name under Settings.

  2. Set the deployment-side variables — BETTER_AUTH_SECRET, EMAIL_FROM, RESEND_API_KEY, AUTUMN_SECRET_KEY, PASSKEY_RP_ID, NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_APP_NAME — with production values, either from Settings → Environment Variables or with the CLI:

    bunx convex env set NAME "value" --prod

    NEXT_PUBLIC_APP_URL must be the https:// production origin, and PASSKEY_RP_ID the bare domain that serves it.

  3. Push the functions so betterAuth/auth exists on the production deployment — bunx convex deploy, or let your first Vercel production deploy do it.

  4. Generate the production JWKS. BETTER_AUTH_SECRET encrypts the private key, so set that first:

    bunx convex env set JWKS "$(bunx convex run betterAuth/auth:getLatestJwks --prod)" --prod

    To 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

  1. Go to vercel.com

  2. Import your repository:

    • Click "New Project"
    • Import from GitHub
    • Select your repository
  3. Configure project:

    • Framework Preset: Next.js
    • Root Directory: ./
    • Build Command: leave it alone — vercel.json overrides 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 string
  • NEXT_PUBLIC_APP_URL — your production domain, on https://
  • NEXT_PUBLIC_APP_NAME — your application name
  • BETTER_AUTH_SECRET — a new value, not the development one
  • EMAIL_FROM — a sender on your verified Resend domain
  • RESEND_API_KEY — production key
  • AUTUMN_SECRET_KEY — production Autumn key
  • PASSKEY_RP_ID — the bare deployed domain: no scheme, no port
  • NEXT_PUBLIC_SUPPORT_EMAIL — optional
  • NEXT_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 build
  • NEXT_PUBLIC_APP_URL — your production domain, on https://
  • NEXT_PUBLIC_APP_NAME — your application name
  • NEXT_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 runtime
  • NEXT_PUBLIC_SUPPORT_EMAIL, NEXT_PUBLIC_PURCHASE_URL — optional
  • NEXT_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)

  1. In Vercel project settings:

    • Go to "Domains"
    • Add your domain
    • Follow DNS instructions
  2. 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" --prod

Redeploy afterwards — NEXT_PUBLIC_* values are inlined at build time.

DNS changes can take up to 48 hours but usually propagate within minutes.

Verify Deployment

  1. Visit your production URL

  2. Test authentication:

    • Register a new account
    • Check email verification
    • Test OTP login
    • Try passkey (if on HTTPS)
  3. 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.

  1. 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:

  1. Add domain in Resend dashboard
  2. Add DNS records provided by Resend
  3. Verify domain
  4. Update EMAIL_FROM to use your domain

Until domain is verified, emails may go to spam or fail to send.

Configure Billing

If using Autumn for payments:

  1. Set up an Autumn account and run bunx atmn push against your live account
  2. Get your production secret key (starts with am_sk_)
  3. Set AUTUMN_SECRET_KEY where the edition needs it
  4. 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 time
  • SENTRY_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 with bunx 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_URL and NEXT_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 HEAD

What 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 build type-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_URL matches your actual domain
  • Cookies are not blocked (HTTPS required)
  • BETTER_AUTH_SECRET is set in the host environment and different from dev
  • Every migration in drizzle/ has been applied to the production database
  • BETTER_AUTH_SECRET is set on the production Convex deployment and different from dev
  • JWKS is set on the production deployment

Emails Not Sending

Check:

  • RESEND_API_KEY is production key
  • Domain is verified in Resend
  • EMAIL_FROM uses 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_ID matches domain
  • Domain doesn't have port number
  • Browser supports WebAuthn

The App Can't Reach Its Data

Verify:

  • DATABASE_URL is set in the host environment and reachable from it
  • The connection string carries ?sslmode=require if 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_URL and NEXT_PUBLIC_CONVEX_SITE_URL are 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

  1. HTTPS Only: Never use HTTP in production
  2. Strong Secrets: Generate with openssl rand -base64 32
  3. Different Secrets: Dev and prod must be different
  4. Environment Variables: Never commit to git
  5. CORS: Configure appropriately for your domain
  6. Rate Limiting: Consider adding rate limiting
  7. Security Headers: Vercel sets these by default
  8. 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:

  1. Check Vercel Docs
  2. Check Better Auth Docs
  3. Review GitHub issues
  4. Contact support

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.

On this page