Skip to main content

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:

  1. Create a production deploy key in the Convex dashboard.
  2. In a separate checkout of your repository, install the dependencies and create .env.local with only that key:

    .env.local
    CONVEX_DEPLOY_KEY=prod:your-deployment|your-key
  3. Deploy 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:

bash
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_SECRET

SITE_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:

bash
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

  1. Import the repository in Vercel. Vercel detects Nuxt and uses the Nitro vercel preset. You do not need a vercel.json file.
  2. Set the build command to nuxt build. Vercel passes the environment variables directly, so the build does not need --dotenv .env.local.
  3. Add your domain, app.example.com, under Domains. Redirect the *.vercel.app production domain to it, because sign-in works only on SITE_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.

VariableValueNotes
SITE_URLhttps://app.example.comThe same value as in Convex
NUXT_PUBLIC_CONVEX_URLhttps://your-deployment.convex.cloudFrom the Convex dashboard
NUXT_PUBLIC_CONVEX_SITE_URLhttps://your-deployment.convex.siteRequired when you use a custom Convex domain
BCN_AUTH_PROXY_IP_SECRETThe value from step 2Mark it as Sensitive
BCN_AUTH_TRUSTED_CLIENT_IP_HEADERx-real-ipSee 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:

nuxt.config.ts
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

  1. Deploy the production branch.
  2. Open https://app.example.com/api/auth/jwks. Check that it lists the kid from step 3.
  3. Sign up, sign in, reload, and sign out on https://app.example.com.
  4. Open a page with a live query in two browsers. Change data in one and check that the other updates.
  5. 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.