Skip to main content

Custom user fields

Place user data in the session, Better Auth component, JWT, or application projection deliberately.

Choose the field's source from how it is written, read, and refreshed.

Decision table

RequirementStore/read from
Core identity: name, email, imageBetter Auth user, through auth.getUser(ctx)
Small field needed in every Convex identitySession claim from defineSessionClaims
Product profile or frequently changing dataApplication Convex projection/table
Role or membership used for authorizationConvex data checked by the function

createBetterConvexAuth controls the Better Auth user schema. It does not accept Better Auth user.additionalFields. Store product fields in an application table that is keyed by the Better Auth user ID.

Read the live user

auth.getUser(ctx) returns the live Better Auth user row, or null. It checks the session in the same Convex function, so a revoked or expired session returns null.

convex/profile.ts
import { query } from './_generated/server'
import { auth } from './auth'

export const current = query({
  args: {},
  handler: async (ctx) => {
    const user = await auth.getUser(ctx)
    if (!user) return null
    return { name: user.name, email: user.email, image: user.image ?? null }
  },
})

Session claims

By default the Convex session token carries only the claims that the library needs: sub (the Better Auth user ID), sid, token_use, and the registered JWT claims. Profile fields such as name, email, emailVerified, and image are not in the token unless you add them.

useConvexAuth().user and ctx.auth.getUserIdentity() read the token. If the client shows the user's name or email from useConvexAuth().user, return those fields from defineSessionClaims:

convex/auth.ts
export const auth = createBetterConvexAuth<DataModel>(components.betterAuth, {
  organization: {},
  defineSessionClaims: ({ user, session }) => ({
    name: user.name,
    email: user.email,
    // Display hint only. Authorize organization access from member rows.
    activeOrganizationId:
      typeof session.activeOrganizationId === 'string' ? session.activeOrganizationId : null,
  }),
})

In Convex functions, prefer auth.getUser(ctx). It reads the live user row, so it never shows a stale name or email.

The library sets the registered JWT claims, sid, and token_use. A custom claim with one of these names fails the token request. The serialized claims must not be larger than 4096 bytes; a larger payload fails with AUTH_SESSION_JWT_CLAIMS_TOO_LARGE.

A claim value stays unchanged until Better Convex issues a new Convex session token. Do not use a claim as the only source for a revocable permission. Read the live user or application data in the Convex function instead.

Application projection

Use a projection for product-specific fields, search indexes, or joins in Convex. Mark it as derived and provide a rebuild path.

ts
const profile = useConvexQuery(api.users.getCurrentProfile, {}, { auth: 'required' })

The query returns immediate reactive state. The projection does not become session truth. Better Auth deletion and update flows must synchronize or rebuild it.

useConvexAuth().user deliberately exposes only the stable identity fields needed by the auth lifecycle. Its type is ConvexUser, a root type export; getConvexUser(event) returns the same shape in Nitro. It is not an application profile and has no module-augmentation target. Read product-specific fields through a generated, typed Convex query instead.