Auth state and user data
Read session state from Better Auth and query application profile data from Convex.
useConvexAuth() tells you whether a user is signed in and gives you the session user. Load other profile data with an ordinary Convex query.
Auth state
const { status, pending, user, error, client, ready } = useConvexAuth()status tells you whether a user is signed in. pending tells you whether auth work is running. Keep them separate in UI logic.
Use these refs for the signed-in user. They live as long as the app. With Better Auth 1.7.6, do not mount client.useSession() in route components; remounting it can leave session state stale. client is the one integrated Better Auth client for sign-in, sign-out, and inferred plugin methods. It is never null, so you can destructure and read it anywhere. The real client exists only in the browser after the Convex runtime starts. During server rendering and before that point, calling one of its methods throws a ConvexCallError with code CLIENT_UNAVAILABLE. Call it from browser event handlers or onMounted().
<template>
<p v-if="status === 'loading'">Checking session…</p>
<p v-else-if="status === 'error'">
{{ error?.message ?? 'Authentication failed' }}
</p>
<p v-else-if="status === 'authenticated'">Welcome, {{ user?.name ?? user?.email ?? user?.id }}</p>
<SignInLink v-else-if="status === 'anonymous'" />
</template>user comes from the Convex session token. It always has id. Other fields,
such as name and email, are present only when defineSessionClaims adds
them. See Session claims.
Choose a user-data source
| Data | Source |
|---|---|
| Name/email needed immediately during SSR | useConvexAuth().user, with the fields added by defineSessionClaims |
| Signed-in user inside a Nitro handler | getConvexUser(event) |
| Product profile, preferences, searchable display data | Explicit application projection query |
| Small identity fields needed in every Convex function | JWT claims |
| Roles and memberships used for authorization | Convex data checked in each function |
Do not put frequently changing permissions only in JWT claims. Their value can remain stale until token refresh.
Query a product profile directly
const auth = useConvexAuth()
const profile = useConvexQuery(api.users.getCurrentProfile, {}, { auth: 'required' })
const displayName = computed(
() => profile.data.value?.displayName ?? auth.user.value?.name ?? 'Account',
)The session is available immediately. The profile is your own Convex data, with its own schema, indexes, and sync code. undefined means the profile query has no value yet. A null result means the query succeeded and found no profile.