Context Enrichment

Eliminate per-handler auth boilerplate with auth.ctx(). Set up once, and every query/mutation gets ctx.auth.userId, ctx.auth.groupId, ctx.auth.role, and ctx.auth.grants automatically.

Use this for DB-backed authorization state. For native Convex identity claims already present on the JWT, prefer ctx.auth.getUserIdentity().

Requires convex-helpers.

This is optional app code layered on top of the minimal auth setup. You do not need it for normal convex-auth installation.

Setup

Import from @robelest/convex-auth/core for your query and mutation wrappers. This keeps provider, OAuth, and crypto code out of your query bundles entirely.

// convex/auth/core.ts
import { createAuthContext } from "@robelest/convex-auth/core";
import { components } from "../_generated/api";

export const auth = createAuthContext(components.auth);
// convex/lib/functions.ts
import { customQuery, customMutation } from "convex-helpers/server/customFunctions";
import { query as rawQuery, mutation as rawMutation } from "../_generated/server";
import { auth } from "../auth/core";

const authCtx = auth.ctx();

export const query = customQuery(rawQuery, authCtx);
export const mutation = customMutation(rawMutation, authCtx);

Usage

// convex/chat.ts
import { query, mutation } from "./lib/functions";

export const list = query({
  args: {},
  handler: async (ctx) => {
    // ctx.auth.userId — authenticated user ID
    // ctx.auth.user   — full user document
    // ctx.auth.grants — resolved grant strings for the active group
    return ctx.db.query("messages").collect();
  },
});

Identity vs enrichment

Use ctx.auth.getUserIdentity() when you want the native Convex identity surface directly from the JWT:

  • subject
  • tokenIdentifier
  • email
  • name
  • pictureUrl

Use auth.ctx() when you want auth state enriched from Convex Auth tables:

  • ctx.auth.userId
  • ctx.auth.user
  • ctx.auth.groupId
  • ctx.auth.role
  • ctx.auth.grants

The canonical convex-auth integration uses:

  • convex/convex.config.ts — component registration
  • convex/auth.ts — provider config, exports signIn/signOut plus the internal store and http runtime aliases
  • convex/auth/core.ts — lightweight context for queries/mutations
  • convex/auth.config.ts — native Convex JWT trust config
  • convex/http.ts — mounts auth protocol routes with auth.http()

When to use core vs auth

Use convex/auth/core.ts anywhere you only need auth context or helper lookups inside Convex functions.

  • Queries and mutations wrapped with auth.ctx()
  • Permission checks like auth.member.require(ctx, ...)
  • Helper lookups like auth.user.get, auth.member.list, auth.group.get
  • Account and key management like auth.account.listPasskeys and auth.key.list

Use convex/auth.ts only for the full runtime surface.

  • Exporting signIn, signOut, store, and http
  • Calling auth.request.context(ctx, request)
  • Passing the full auth runtime into higher-level server helpers such as group SSO setup

In this repo, convex/comments.ts, convex/projects.ts, convex/issues.ts, convex/groups.ts, and convex/account.ts all use core because they only need ctx.auth and helper APIs. App-specific HTTP routes still import auth.ts when they need auth.request.context(ctx, request).

Optional auth (public routes)

export const publicQuery = customQuery(rawQuery, auth.ctx({ optional: true }));
// ctx.auth.userId is null and ctx.auth.grants is [] when unauthenticated

Add app-specific fields

const authCtx = auth.ctx({
  resolve: async (_ctx, user, authState) => {
    return {
      activeGroupId: authState.groupId ?? null,
      canManageMembers: authState.grants.includes("members.create"),
    };
  },
});
// ctx.auth.groupId, ctx.auth.role, ctx.auth.grants,
// and ctx.auth.canManageMembers available in all handlers

What’s on ctx.auth

PropertyTypeDescription
userIdstringAuthenticated user’s document ID
userobject \| nullFull user document
groupIdstring \| nullActive group ID
rolestring \| nullPrimary role for active group
grantsstring[]Resolved grants for active group
getUserIdentity()Promise<UserIdentity \| null>Native Convex identity from JWT claims
...extravariesWhatever resolve() returns

Testing with convex-test

You can test auth.ctx()-based functions with convex-test. Register the Convex Auth component in your test harness so component-backed user, member, and group lookups behave the same way as production.

Register the component

Make sure your convex-test harness mounts the Convex Auth component before invoking wrappers that call auth.ctx() or auth.context(...).

// convex/test.setup.ts
import { convexTest } from "convex-test";
import { register } from "@robelest/convex-auth/test";
import schema from "./schema";

export function setupTest() {
  const t = convexTest(schema);
  register(t);
  return t;
}

For handlers that only need the current identity, prefer native Convex auth in the handler instead of auth.ctx():

const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Authentication required");
const userId = identity.subject;