Skip to main content

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

ts
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().

vue
<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

DataSource
Name/email needed immediately during SSRuseConvexAuth().user, with the fields added by defineSessionClaims
Signed-in user inside a Nitro handlergetConvexUser(event)
Product profile, preferences, searchable display dataExplicit application projection query
Small identity fields needed in every Convex functionJWT claims
Roles and memberships used for authorizationConvex 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

ts
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.