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
| Requirement | Store/read from |
|---|---|
| Core identity: name, email, image | Better Auth user, through auth.getUser(ctx) |
| Small field needed in every Convex identity | Session claim from defineSessionClaims |
| Product profile or frequently changing data | Application Convex projection/table |
| Role or membership used for authorization | Convex 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.
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:
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.
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.