Skip to main content

serverConvex

Call Convex from a Nitro request with explicit authentication policy and request-scoped state.

serverConvex(event) creates one request-scoped caller with query, mutation, and action methods.

server/api/projects.get.ts
import { api } from '#convex/api'
import { serverConvex } from '#convex/server'

export default defineEventHandler(async (event) => {
  return await serverConvex(event, { auth: 'required' }).query(api.projects.list)
})

Authentication modes

ModeCookie behavior
requiredExchange the request session; throw if no valid identity exists
optionalUse identity when valid, otherwise call anonymously
noneDo not inspect or exchange auth cookies

Without authToken or credential, the default is optional. convex.auth.defaultQueryAuth does not change this default.

Caller lifetime

A caller creates these on first use:

  • one token lookup;
  • one official ConvexHttpClient;
  • one copy of the request's auth state.

Do not store a caller in module scope or in a cache shared across requests. Create it inside the handler.

Transport bounds

SSR queries and serverConvex use the same bounded official HTTP transport. An incoming request abort cancels upstream response consumption. Query calls have an 8-second deadline by default, mutations 15 seconds, and actions 60 seconds.

Every query, mutation, and action response is capped at 1 MiB by default. A declared oversize response is rejected before its body is read; a streamed response is cancelled as soon as it crosses the same cap.

Set the query deadline and the response cap with the convex.server module options queryTimeoutMs and maxResponseBytes. They apply to every request; they are not per-call options. See server limits.

Explicit credentials

A trusted server workflow can pass a Convex JWT that it already has:

ts
const caller = serverConvex(event, {
  authToken: trustedToken,
})

Or exchange an explicit Better Auth cookie credential:

ts
const caller = serverConvex(event, {
  credential: { type: 'cookie', value: cookieHeader },
})

Pass either authToken or credential, not both. Either one requires a signed-in user, so omit auth. The TypeScript types reject the other combinations, and the runtime check rejects them for JavaScript callers.

Never take these values from a request body and forward them. A raw Better Auth session token is not accepted as a bearer credential here.

Function arguments

The caller matches Convex's generated function-reference contract. Omit the artificial {} for a function with no arguments:

ts
const projects = await serverConvex(event).query(api.projects.list)

Functions with declared arguments still require their exact generated object.

Errors

Calls reject with ConvexCallError for classifiable auth, transport, and structured Convex server failures. Every rejection carries the function path in functionName. A missing required identity and a rejected token exchange use code UNAUTHENTICATED. Invalid options throw ServerConvexValidationError before network access.

Rethrow an error with toConvexH3Error(error) to return it from a Nitro handler with a matching HTTP status. See server routes.

A rejected caller keeps its rejected token/client state. Create a new caller for an intentional retry.

Log server errors safely

Log the operation, your own request ID, and the public error fields, including functionName. Do not log the raw error:

ts
import { isConvexCallError } from '@lupinum/better-convex-nuxt/errors'

try {
  return await serverConvex(event, { auth: 'required' }).mutation(api.projects.archive, {
    projectId,
  })
} catch (error) {
  if (isConvexCallError(error)) {
    reportServerCallFailure({
      correlationId: event.context.requestId, // application-owned
      operation: 'mutation',
      functionName: error.functionName,
      kind: error.kind,
      // Add `code` only after checking it against application-owned values.
    })
  }
  throw toConvexH3Error(error)
}

Do not include function arguments, results, cookies, headers, tokens, message, data, or cause in generic telemetry. Function names reveal how your application is built, so keep them in server-only, access-controlled diagnostics. toConvexH3Error leaves functionName out of the response body, so a route never reveals which Convex function it called. Log it from the original error on the server.

The Convex client uses the same plain Error values for several client, protocol, and unstructured server failures. Those failures stay unknown; do not infer a category from message text. Throw a structured ConvexError with an application-owned, public-safe code for expected domain or operator failures.

Read the request user

Use getConvexUser(event) to read the signed-in user for routing or rendering decisions, and requireConvexUser(event) to answer 401 for an anonymous request:

server/api/me.get.ts
export default defineEventHandler(async (event) => {
  const user = await requireConvexUser(event)
  return { id: user.id }
})

Both helpers, SSR hydration, and serverConvex(event) share one cookie-to-token exchange per request. A handler that calls requireConvexUser and then serverConvex exchanges the cookie once, and both see the same user. The user always has id; profile fields such as name and email are present only when defineSessionClaims adds them. Use the returned user for display and routing only. Protected data still comes from a Convex function that checks the user. See the server API.

No client hydration

A serverConvex result returned from your API route is ordinary route data. It does not seed useConvexQuery or establish a subscription.

Use a composable directly in a page for normal SSR-to-live data. Use a server route when a real server boundary is required.