Deploy to Vercel
Run the Nuxt application on Vercel with a Convex production deployment and working authentication.
This guide deploys a Nuxt application with Better Convex Nuxt and
authentication to Vercel. Vercel runs Nuxt. Convex runs your functions and
data. The example domain is https://app.example.com.
Before you start, the application must sign in and sign out locally. Read Deployment for the general order.
1. Deploy Convex
Do not deploy Convex from the Vercel build. The better-convex convex wrapper
reads the target deployment only from .env.local, and that file does not
exist on Vercel. Deploy Convex first, from your machine or from CI:
- Create a production deploy key in the Convex dashboard.
In a separate checkout of your repository, install the dependencies and create
.env.localwith only that key:.env.local CONVEX_DEPLOY_KEY=prod:your-deployment|your-keyDeploy the functions:
bash pnpm exec better-convex convex deploy
Deploy Convex before every Vercel release that needs new or changed Convex functions.
2. Set the Convex environment variables
In the same checkout, set the auth values in the Convex production deployment. Generate the proxy secret in this shell, because Vercel needs the same value:
export BCN_AUTH_PROXY_IP_SECRET="$(openssl rand -base64 32)"
pnpm exec better-convex convex env set SITE_URL https://app.example.com
printf '1:%s' "$(openssl rand -base64 32)" | pnpm exec better-convex convex env set BETTER_AUTH_SECRETS
printf '%s' "$BCN_AUTH_PROXY_IP_SECRET" | pnpm exec better-convex convex env set BCN_AUTH_PROXY_IP_SECRETSITE_URL is your production domain, not a *.vercel.app URL. Convex sets
CONVEX_SITE_URL itself as a built-in; do not set it. Add your social
provider secrets now, if you use them. See
Social sign-in.
3. Create the signing key
Run this once, before the first user signs in:
pnpm exec better-convex convex run auth:ensureSigningKey '{}'Note the returned kid. Running the command again returns the same key.
4. Create the Vercel project
- Import the repository in Vercel. Vercel detects Nuxt and uses the Nitro
vercelpreset. You do not need avercel.jsonfile. - Set the build command to
nuxt build. Vercel passes the environment variables directly, so the build does not need--dotenv .env.local. - Add your domain,
app.example.com, under Domains. Redirect the*.vercel.appproduction domain to it, because sign-in works only onSITE_URL.
5. Set the Vercel environment variables
Add these variables for the Production environment. Vercel passes them to
the build and to the server functions. The build needs them, because
nuxt.config.ts reads SITE_URL and BCN_AUTH_TRUSTED_CLIENT_IP_HEADER.
| Variable | Value | Notes |
|---|---|---|
SITE_URL | https://app.example.com | The same value as in Convex |
NUXT_PUBLIC_CONVEX_URL | https://your-deployment.convex.cloud | From the Convex dashboard |
NUXT_PUBLIC_CONVEX_SITE_URL | https://your-deployment.convex.site | Required when you use a custom Convex domain |
BCN_AUTH_PROXY_IP_SECRET | The value from step 2 | Mark it as Sensitive |
BCN_AUTH_TRUSTED_CLIENT_IP_HEADER | x-real-ip | See the next section |
Do not add BETTER_AUTH_SECRETS to Vercel. Only Convex needs it.
Your nuxt.config.ts passes the values to the module:
export default defineNuxtConfig({
modules: ['@lupinum/better-convex-nuxt'],
convex: {
auth: {
origin: process.env.SITE_URL ?? 'http://localhost:3000',
trustedClientIpHeader: process.env.BCN_AUTH_TRUSTED_CLIENT_IP_HEADER,
},
},
})Trusted client IP header
Better Auth limits sign-in attempts per client IP. The auth proxy reads the IP
from the header in trustedClientIpHeader, signs it, and sends it to Convex.
The header must contain exactly one IP address, and clients must not be able
to set it.
On Vercel, use x-real-ip. Vercel sets it to the IP address of the client and
replaces any value that the client sends. Requests cannot reach your Vercel
functions without passing through Vercel, so clients cannot bypass it.
Warning: If you put another proxy or CDN in front of Vercel, x-real-ip
contains the address of that proxy. Then every user shares one rate limit.
Use the client IP header of that proxy instead, and block requests that do
not come through it.
Without the variable, the build fails with
auth.trustedClientIpHeader is required outside exact loopback development origins.
6. Deploy and check
- Deploy the production branch.
- Open
https://app.example.com/api/auth/jwks. Check that it lists thekidfrom step 3. - Sign up, sign in, reload, and sign out on
https://app.example.com. - Open a page with a live query in two browsers. Change data in one and check that the other updates.
- If you use MCP, connect a host as described in Connect ChatGPT and Claude.
Do not turn on ISR or swr route rules for pages that render signed-in data.
A shared cache would show one user's page to another user.
Preview deployments
Every Vercel preview gets its own URL. Better Auth accepts requests only from
the exact SITE_URL, so sign-in does not work on a preview URL that differs
from it. Choose one of these setups:
A stable staging domain with auth. Assign a domain such as
staging.example.com to a staging branch in Vercel. Create a separate
Convex project for staging and deploy it with its own production deploy key.
In Vercel, add Preview environment variables for the staging branch:
SITE_URL=https://staging.example.com, the staging Convex URLs, a separate
BCN_AUTH_PROXY_IP_SECRET, and BCN_AUTH_TRUSTED_CLIENT_IP_HEADER=x-real-ip.
In the staging Convex deployment, set the same SITE_URL, a separate
BETTER_AUTH_SECRETS, and the same proxy secret.
Other previews without sign-in. Point their Preview variables at the
staging Convex deployment and staging SITE_URL. Pages render, but sign-in
fails on those URLs. Protect them with Vercel Deployment Protection.
Never point a preview at the production Convex deployment, and never reuse
production secrets. better-convex convex deploy accepts only a production
deploy key, so use a separate Convex project for staging instead of Convex
preview deployments.