Skip to main content

Deployment

Deploy a Nuxt and Convex application in the right order, with working SSR, realtime, and authentication.

Deploy Convex and Nuxt as two services. Convex runs the database and your functions. Nuxt renders pages, serves the browser bundle, runs server routes, and runs the auth proxy.

For a step-by-step guide on one host, see Deploy to Vercel.

What you need

  • a Convex production deployment;
  • a Nuxt host that runs the Nitro server;
  • one public HTTPS origin for the Nuxt application;
  • browser access to the Convex HTTPS and WebSocket endpoints.

Authentication needs the /api/auth/* Nitro route, so a static host without a server cannot run it. An application without convex.auth can use Nuxt's static output, but test its SSR and client navigation first.

Select the production deployment

Every Convex command below uses pnpm exec better-convex convex. This wrapper reads the target deployment only from .env.local in the current folder. It ignores CONVEX_* variables from the shell and rejects target flags such as --prod.

For production, run the commands from a separate checkout of your repository with its dependencies installed. Its .env.local contains only the production deploy key:

.env.local
CONVEX_DEPLOY_KEY=prod:your-deployment|your-key

Create the key in the Convex dashboard, under the production deployment's settings. deploy refuses a development key and a plain CONVEX_DEPLOYMENT name. Check the file before every production command. Never commit it.

Deployment order

  1. Deploy the Convex schema, functions, HTTP routes, and auth component:

    bash
    pnpm exec better-convex convex deploy
  2. Set SITE_URL, BETTER_AUTH_SECRETS, and BCN_AUTH_PROXY_IP_SECRET in Convex, as described in environment variables. Convex sets its CONVEX_SITE_URL built-in itself.
  3. Create the signing key once, before the first user signs in:

    bash
    pnpm exec better-convex convex run auth:ensureSigningKey '{}'

    The action returns { created, kid }. Running it again returns the same key.

  4. Build and deploy Nuxt with its environment variables. Before you announce the release, open https://app.example.com/api/auth/jwks and check that it lists the returned kid.
  5. For MCP, create the host OAuth clients with auth.oauthOperator. See Delegated OAuth and MCP.
  6. Test sign-in on the deployed origin, then open the application to users.

Retired signing keys stay published for 21 minutes after a rotation. Schedule the internal auth:pruneSigningKeys mutation, for example as a daily Convex cron, to delete keys after that time. It deletes at most batchSize keys per call (1 to 256, default 64) and never the current key. Run it again while it returns hasMore: true.

Production checklist

  • Install the exact Better Auth and OAuth provider versions from the package manifest. See release compatibility.
  • Set an exact HTTPS SITE_URL without a path, and pass the same value as convex.auth.origin.
  • Keep BETTER_AUTH_SECRETS out of Nuxt. Keep BCN_AUTH_PROXY_IP_SECRET out of public config and logs.
  • Set NUXT_PUBLIC_CONVEX_SITE_URL to the exact HTTP Actions origin. Do not try to override the CONVEX_SITE_URL built-in in Convex.
  • Check that /api/auth/* reaches the intended .convex.site origin.
  • Allow the exact Convex HTTPS and WebSocket origins in your Content Security Policy.
  • Set convex.auth.trustedClientIpHeader to a header that your host overwrites with one client IP. A missing or invalid value stops auth requests.
  • Make sure public traffic can reach the Nuxt server only through that host. Otherwise a client can send the header itself.
  • Keep module logging off unless you investigate a specific problem.
  • Test sign-in, sign-out, SSR refresh, client navigation, and realtime reconnect.
  • Test your Convex access checks with two different users, not only the UI.
  • Test each social provider's callback, and the error page for an existing account.
  • For MCP, also test consent, PKCE, a wrong client, resource, or scope, revocation, and removing a membership.

Do not let the first sign-in create the signing key, and never delete JWKS rows by hand.

Preview deployments

Better Auth accepts requests only from the exact SITE_URL. A preview with a random URL therefore cannot sign users in against the production deployment. Give a stable preview its own Convex deployment, exact SITE_URL, and its own secrets. Do not share production secrets, cookies, or data with previews, and do not use wildcard trusted origins.

Health checks

A successful HTML response proves only that Nuxt started. Also check:

  • an anonymous Convex query;
  • a server-rendered page for a signed-in user, when auth is on;
  • a browser subscription that receives a change;
  • that the auth proxy rejects unsupported methods.

Read the security model before you open the application to the public.