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 componentThis keeps the session cookie on the Nuxt application origin while Better Auth data remains in Convex.
Required files
| File | Responsibility |
|---|---|
convex/convex.config.ts | Register the Better Auth component |
convex/auth.config.ts | Configure Convex JWT verification |
convex/auth.ts | Create the component client and Better Auth factory |
convex/http.ts | Register Better Auth HTTP routes lazily |
nuxt.config.ts | Register 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:
| Variable | Ownership | Purpose |
|---|---|---|
SITE_URL | Application-owned | Exact Nuxt origin used by Better Auth and trusted origins |
CONVEX_SITE_URL | Convex built-in | Exact Convex HTTP Actions origin used as session issuer |
BETTER_AUTH_SECRETS | Application-owned | Versioned Better Auth secrets, newest version first |
BCN_AUTH_PROXY_IP_SECRET | Application-owned | Private 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:
| Variable | Purpose |
|---|---|
SITE_URL | Exact public Nuxt origin used by the proxy |
NUXT_PUBLIC_CONVEX_URL | Convex deployment URL ending in .convex.cloud |
NUXT_PUBLIC_CONVEX_SITE_URL | Convex HTTP Actions URL; required for local/custom domains |
BCN_AUTH_PROXY_IP_SECRET | Same private proxy secret configured in Convex |
BCN_AUTH_TRUSTED_CLIENT_IP_HEADER | Ingress-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:
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_SECRETSversions only as long as retained ciphertext needs them; test decryption before removing one. - Provision a separate
BCN_AUTH_PROXY_IP_SECRETin both Nuxt and Convex. - Configure an ingress-owned
trustedClientIpHeaderoutside 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.