Skip to main content

Better Auth Setup

Configure Better Auth in Convex and connect it to Nuxt through the same-origin auth proxy.

The maintained topology is:

Browser or Nuxt SSR
  → same-origin /api/auth/*
  → Better Convex Nuxt Nitro proxy
  → Convex HTTP Actions site
  → Better Auth Convex component

This keeps the session cookie on the Nuxt application origin while Better Auth data remains in Convex.

Required files

FileResponsibility
convex/convex.config.tsRegister the Better Auth component
convex/auth.config.tsConfigure Convex JWT verification
convex/auth.tsCreate the component client and Better Auth factory
convex/http.tsRegister Better Auth HTTP routes lazily
nuxt.config.tsRegister the Nuxt module and public Convex URLs

The complete minimal versions are in Add authentication. Keep them as the baseline and add product-specific Better Auth options in createAuth.

Environment ownership

These values exist in the Convex environment. Set the application-owned values; Convex injects the deployment-owned built-in automatically:

VariableOwnershipPurpose
SITE_URLApplication-ownedExact Nuxt origin used by Better Auth and trusted origins
CONVEX_SITE_URLConvex built-inExact Convex HTTP Actions origin used as session issuer
BETTER_AUTH_SECRETSApplication-ownedVersioned Better Auth secrets, newest version first
BCN_AUTH_PROXY_IP_SECRETApplication-ownedPrivate Nuxt-to-Convex client-IP signing secret

Do not try to override CONVEX_SITE_URL. Configure Nuxt's matching public HTTP Actions origin through NUXT_PUBLIC_CONVEX_SITE_URL.

Set these for the Nuxt build/runtime:

VariablePurpose
SITE_URLExact public Nuxt origin used by the proxy
NUXT_PUBLIC_CONVEX_URLConvex deployment URL ending in .convex.cloud
NUXT_PUBLIC_CONVEX_SITE_URLConvex HTTP Actions URL; required for local/custom domains
BCN_AUTH_PROXY_IP_SECRETSame private proxy secret configured in Convex
BCN_AUTH_TRUSTED_CLIENT_IP_HEADERIngress-owned single-IP header; required outside loopback

SITE_URL must be an exact origin. Use HTTPS outside loopback development. Do not include a path, query, or fragment.

Proxy behavior

The module owns the fixed /api/auth/* path and same-origin credentials behavior. It sends one request to the configured Convex site origin and does not follow server-side redirects.

OAuth and login redirects are returned to the browser. Auth-enabled applications require a Nitro-capable deployment; a purely static host cannot provide the proxy.

Applications that expose delegated access use the separate delegated OAuth and MCP profile. It adds the official provider, verified consent UI, fixed /mcp resource, provider-owned client provisioning, and live Convex authorization without adding another auth store.

Outside exact loopback development, set auth.trustedClientIpHeader to the single header owned and overwritten by the trusted ingress. The proxy strips caller forwarding headers and signs only that canonical address. Missing, malformed, or multi-value headers fail closed before Better Auth. Never configure a header that clients can supply unchanged, and never default to a forwarding chain. The Nuxt origin must reject public traffic that bypasses the ingress, either through network restrictions or independent ingress authentication; otherwise a client can supply the chosen header directly.

The signed identity is attached consistently to proxy requests, SSR token hydration, and request-scoped serverConvex cookie exchange. There is no public raw cookie-exchange helper that can bypass the request boundary.

Convex token-exchange quota

The public GET /api/auth/convex/token exchange is normalized by Better Auth as /convex/token and has a fixed allowance of 300 requests per 10 seconds. The key is the ingress-authenticated client IP plus that normalized path, so the quota is neither per user nor per session. Better Auth stores the counter in the component's database-backed rateLimit model, making it shared by every server instance connected to the deployment. A blocked request returns 429 with an X-Retry-After value in seconds.

This higher route-specific allowance accommodates concurrent SSR, hydration, Studio, and query refreshes without weakening sign-in or OAuth token endpoints. It is intentionally not configurable. Treat repeated 429 responses as either an ingress client-IP attribution error or abnormal bootstrap traffic before considering a profile change.

The auth proxy applies fixed 1 MiB request and upstream-response bounds. These are security invariants, not application configuration. trustedClientIpHeader is the only proxy-boundary input:

ts
convex: {
  auth: {
    origin: process.env.SITE_URL ?? 'http://localhost:3000',
    trustedClientIpHeader: process.env.BCN_AUTH_TRUSTED_CLIENT_IP_HEADER,
  }
}

Production checklist

  • Use an exact HTTPS SITE_URL.
  • Generate a unique production secret.
  • Keep old BETTER_AUTH_SECRETS versions only as long as retained ciphertext needs them; test decryption before removing one.
  • Provision a separate BCN_AUTH_PROXY_IP_SECRET in both Nuxt and Convex.
  • Configure an ingress-owned trustedClientIpHeader outside exact loopback development.
  • Restrict the Nuxt origin to that ingress or independently authenticate ingress requests at the origin.
  • Configure OAuth callback and trusted origins for the public application origin.
  • Configure Nuxt to target the selected deployment's exact Convex HTTP Actions URL.
  • Provision the initial signing key through the internal operator action before admitting auth traffic.
  • Keep Nuxt auth routes uncached.
  • Test sign-up, sign-in, reload, refresh, and sign-out in the deployed topology.
  • Verify protected Convex functions reject anonymous calls directly.