Skip to main content

Server API

Nitro helpers from @lupinum/better-convex-nuxt/server and Convex helpers from @lupinum/better-convex-nuxt/better-auth/server.

Better Convex has two server entries:

  • @lupinum/better-convex-nuxt/server runs in Nitro, the Nuxt server. Use it in server/ routes.
  • @lupinum/better-convex-nuxt/better-auth/server runs in Convex. Use it in convex/ files.

Nitro helpers

The module auto-imports serverConvex, getConvexUser, requireConvexUser, and toConvexH3Error in Nitro code. To import them explicitly, use the #convex/server alias:

ts
import { getConvexUser, requireConvexUser, serverConvex, toConvexH3Error } from '#convex/server'

Outside Nuxt aliases, import from @lupinum/better-convex-nuxt/server. That entry also exports the ServerConvexValidationError class and the ServerConvexCaller, ServerConvexOptions, and ConvexCredential types.

serverConvex

ts
serverConvex(event: H3Event, options?: ServerConvexOptions): ServerConvexCaller

Creates a caller for one request. Create a new caller in each request. Do not store it in a module variable.

options has one of three forms:

ts
type ServerConvexOptions =
  | { auth?: 'required' | 'optional' | 'none' }
  | { authToken: string }
  | { credential: ConvexCredential }

type ConvexCredential = { type: 'cookie'; value: string }
FormIdentity used
authThe Better Auth session cookie of the request. The default is 'optional'. convex.auth.defaultQueryAuth does not apply.
authTokenA Convex session token that you already have. Authentication is required.
credentialA Better Auth Cookie header string. Authentication is required.

The three forms cannot be combined. Do not add auth to the authToken or credential form. A raw Better Auth session token is not accepted as authToken.

The caller has these methods:

ts
getToken(): Promise<string | null>
query(query, ...args): Promise<Result>
mutation(mutation, ...args): Promise<Result>
action(action, ...args): Promise<Result>

getToken() returns the Convex session token for this request, or null for an anonymous request. A function without arguments needs no {} argument. A function with declared arguments needs its argument object.

Calls reject with a ConvexCallError that has functionName. A missing required user and a failed token exchange use kind authentication and code UNAUTHENTICATED. Invalid options throw ServerConvexValidationError before any network request.

A query stops after convex.server.queryTimeoutMs (default 8 seconds), a mutation after 15 seconds, and an action after 60 seconds. A response larger than convex.server.maxResponseBytes (default 1 MiB) fails. See module configuration.

The cookie form always needs the request. The package has no helper that turns a cookie into a token outside a request, because rate limiting needs the client IP of the request.

getConvexUser

ts
getConvexUser(event: H3Event): Promise<ConvexUser | null>

Returns the signed-in user of the request, or null for an anonymous request. A build without auth always returns null.

The user is read from the Convex session token. It always has id. Profile fields such as name, email, and image are present only when defineSessionClaims adds them; see Session claims. The same holds for requireConvexUser.

The module exchanges the session cookie for a Convex session token at most once per request. Server rendering, repeated getConvexUser and requireConvexUser calls, and serverConvex(event) calls that use the request cookie share that result. So the user that a route shows is the user that Convex checks.

It rejects with an H3 error when it cannot find out who the user is: 502 when the auth backend fails, and 500 when auth is on but no Convex site URL is set. The error body contains no credentials.

Use the user for routing and display. Check access to data inside Convex functions, for example through serverConvex(event, { auth: 'required' }).

requireConvexUser

ts
requireConvexUser(event: H3Event): Promise<ConvexUser>

Returns the signed-in user, or throws an H3 401 error. The response data is a serialized ConvexCallError with kind authentication and code UNAUTHENTICATED. Missing, invalid, and revoked sessions all give 401. A build without auth always throws it. Other failures reject as for getConvexUser.

server/api/account.get.ts
export default defineEventHandler(async (event) => {
  setHeader(event, 'cache-control', 'private, no-store')
  const user = await requireConvexUser(event)
  return { id: user.id }
})

toConvexH3Error

ts
toConvexH3Error(error: unknown): H3Error<SerializedConvexCallError>

Converts any thrown value with normalizeConvexError and returns an H3 error for a Nitro response. The HTTP status depends on the error kind:

KindHTTP status
authentication401, or 403 when the error has status 403
transport502
serverThe application's 4xx status, otherwise 400
unknown500

The H3 error data is error.toJSON() without functionName. It keeps the data of your ConvexError. It never contains a stack, a credential, or the Convex function path. Log functionName from the original error on the server. Map the error yourself when a route must not show the application data. In the browser, normalizeConvexError turns the FetchError from $fetch or useFetch back into the same ConvexCallError.

server/api/notes.post.ts
import { api } from '#convex/api'
import { z } from 'zod'

const bodySchema = z.object({ title: z.string().min(1) })

export default defineEventHandler(async (event) => {
  const { title } = await readValidatedBody(event, bodySchema.parse)
  try {
    return await serverConvex(event, { auth: 'required' }).mutation(api.notes.create, { title })
  } catch (error) {
    throw toConvexH3Error(error)
  }
})

The Convex argument validator and the Convex function still check the input.

Convex auth helpers

Import these helpers in convex/ files:

convex/auth.ts
import { createBetterConvexAuth } from '@lupinum/better-convex-nuxt/better-auth/server'

createBetterConvexAuth

ts
createBetterConvexAuth<DataModel>(
  component: typeof components.betterAuth,
  options?: CreateBetterConvexAuthOptions<DataModel>,
): BetterConvexAuth<DataModel>

Creates the Better Auth setup for one Convex deployment. It is the only way to configure Better Auth with Better Convex. Call it once in convex/auth.ts and export the result as auth.

OptionEffect
appNameApplication name that Better Auth shows, for example in two-factor apps
authFunctionsReferences to the trigger functions, usually internal.auth. Without it, triggers do not run.
triggersonCreate, onUpdate, and onDelete handlers for each auth model, such as user. They run in the same mutation as the change.
beforeUserCreateDecide whether a new user may sign up. Return { allowed: false } to refuse.
emailOne function that sends every auth email: verification, password reset, one-time codes, two-factor codes, and invitations
emailAndPasswordEmail and password sign-in options, or false to turn it off. passwordReset: true adds link-based password reset.
emailVerificationEmail verification options
emailOTPEmail one-time-code options, or false
twoFactorTwo-factor options, or false
organizationOrganization options, or false
socialProvidersBetter Auth social provider settings, or a function that returns them
accountaccountLinking.trustedProviders only. Linking accounts with different emails stays off.
sessionexpiresIn (1 hour to 30 days, default 7 days), updateAge (default 1 day), and cookieCache, all in seconds
defineSessionClaimsAdd claims to the Convex session token. By default it carries only the library claims; profile fields such as name and email are opt-in.
oauth{ mcp: BetterConvexMcpOptions }. Turns on OAuth for MCP hosts. See below.
oauthProviderA hand-written OAuth provider profile. Use oauth.mcp instead unless you need a different profile.

oauth.mcp accepts scopes (required, scope name to consent text), resource (default /mcp), hosts ('chatgpt', 'claude'), renewal (default true), loginPage (default /login), and consentPage (default /oauth/consent).

The returned auth object has these members. The cost column lists the database work of one call.

MemberUseCost
createAuth(ctx)Create the Better Auth instance for one Convex function. Export it from convex/auth.ts.None until you call Better Auth
registerRoutes(http)Add the /api/auth/* routes to the Convex HTTP routerNone
getUser(ctx)The signed-in user, or null. Revoked, expired, and replaced sessions give null.One component query
requireUser(ctx)Like getUser, but throws ConvexError({ code: 'UNAUTHENTICATED' })One component query
getAuth(ctx){ auth, headers } for auth.api calls as the signed-in user, from a mutation or actionOne component query
sessionHttpAction(handler)Wrap an HTTP action that must act as the caller's session. The handler gets { request, auth, headers, user, sessionId }.One component query per request
triggerFunctions()onCreate, onUpdate, and onDelete internal mutations for triggers. Export them from convex/auth.ts.Your trigger code
jwksOperatorFunctions()ensureSigningKey, rotateSigningKey, and pruneSigningKeys internal functions for signing keysRuns only when you call them
mcpissuer(), resource(), scopes(), and scopesSupported() of the oauth.mcp settingsNone
createMcpAccessVerifier(ctx, options?)The token verifier for handleMcpRequestOne component query per token
requireMcpPrincipal(ctx, principal, { scope })Check an MCP principal and its scope again inside your function. Returns { user, principal }.One component query
oauthConnections.list(ctx, { userId })Up to 100 current grants of a user, newest firstOne component query, plus one per grant to read the client name
oauthConnections.revoke(ctx, { userId, clientId })Delete the consent and the refresh tokens of one user and client. Returns { revoked }.Up to 9 component mutations
oauthOperatorcreateHostClient, createPublicClient, setClientDisabled, and deleteClient for OAuth clients. Call them from internal functions that only operators run.Several component calls

createMcpAccessVerifier and requireMcpPrincipal are the only public OAuth access checks. Every access token is bound to the consent that issued it, so a revoked consent stops the token even after the person grants the same access again. A disabled OAuth resource rejects tokens that were already issued. requireMcpPrincipal throws a ConvexError with code MCP_ACCESS_DENIED or MCP_INSUFFICIENT_SCOPE. Pass oauthConnections the ID of the signed-in user, for example from requireUser. Never take userId from client input.

A call that reads the component inside a Convex query makes that query depend on the session and grant rows. When a session or grant is revoked, the query runs again and the check fails.

mcpPrincipalValidator

A Convex validator for BetterConvexMcpPrincipal. Use it for the principal argument of internal functions that an MCP tool calls:

convex/notes.ts
export const list = internalQuery({
  args: { principal: mcpPrincipalValidator },
  handler: async (ctx, { principal }) => {
    const { user } = await auth.requireMcpPrincipal(ctx, principal, {
      scope: 'notes:read',
    })
    return await ctx.db
      .query('notes')
      .withIndex('by_owner', (q) => q.eq('ownerId', user.id))
      .take(20)
  },
})

Never use it on a public query, mutation, or action. A public function would let any caller name another user.

createUserProjectionTriggers

ts
createUserProjectionTriggers(options): { user: { onCreate, onUpdate, onDelete, rebuild } }

Keeps a copy of Better Auth users in one of your tables, for example users. Pass the user handlers to the triggers option of createBetterConvexAuth. Better Auth stays the source of the user data.

OptionRequiredEffect
tableYesYour table, for example 'users'
indexYesThe index on the Better Auth user ID, for example 'by_auth_id'
authIdFieldNoThe field that stores the Better Auth user ID. Default: 'authId'.
createDocYesBuilds the new document from { ctx, user, now }
patchDocNoBuilds a patch when the user changes. Return null to skip.
rebuildDocNoBuilds a patch for rebuild

Each handler reads one row through index and writes at most one row. rebuild(ctx, users) processes a list of users and returns { inserted, patched, skipped }. User synchronization guide.

findAccountKeyCollisions

ts
findAccountKeyCollisions(ctx, component, { pageSize?, maxPages?, cursor? }): Promise<AccountKeyCollisionReport>

Finds auth accounts that share one provider and provider account ID. Only an upgrade from a 1.0 beta can create them. Run it once after you deploy 1.0, from an internal action:

convex/authUpgrade.ts
import { findAccountKeyCollisions } from '@lupinum/better-convex-nuxt/better-auth/server'
import { v } from 'convex/values'
import { components } from './_generated/api'
import { internalAction } from './_generated/server'

export const checkAccountKeys = internalAction({
  args: { cursor: v.optional(v.union(v.string(), v.null())) },
  handler: (ctx, { cursor }) => findAccountKeyCollisions(ctx, components.betterAuth, { cursor }),
})

It returns { scannedAccounts, collisions, isDone, continueCursor }. Each collision has providerId, accountId, and accounts, a list of { id, userId }. One call reads at most maxPages queries (1 to 1000, default 100) of at most pageSize rows (1 to 100, default 100), so at most 10,000 rows by default. When isDone is false, call it again with cursor: continueCursor. Each collision is reported by exactly one call, also when its rows span two calls; together, the calls report every collision. It writes nothing. It throws instead of returning a partial report when the rows do not arrive in index order. Upgrade guide.

Configuration helpers

ExportUse
getConvexAuthProvider(env?)Returns the auth provider entry for convex/auth.config.ts. It reads CONVEX_SITE_URL.
requireAuthOrigin(name, env?)Reads SITE_URL or CONVEX_SITE_URL from the environment and checks that it is a bare origin. Throws otherwise.
defineAuthAdapterFunctions({ schema, metadata })Creates the adapter functions for a local auth component. You need it only with Better Auth plugins that change the schema. See Better Auth plugins.
convex/auth.config.ts
import { getConvexAuthProvider } from '@lupinum/better-convex-nuxt/better-auth/server'
import type { AuthConfig } from 'convex/server'

export default {
  providers: [getConvexAuthProvider()],
} satisfies AuthConfig