Composables
Use this page when you need exact behavior for the client-side composables exposed by the module.
Quick Start
Use useUserSession() in pages and components to access the current session and session lifecycle actions.
<script setup lang="ts">
const { user, loggedIn, signOut } = useUserSession()
</script>
<template>
<button v-if="loggedIn" @click="signOut()">
Sign out {{ user?.email }}
</button>
</template>
Choosing an API
| Use case | Composable |
|---|---|
| Pages, components, and setup stores that need auth state and session lifecycle actions | useUserSession() |
| Direct Better Auth client access | useAuthClient() |
| Sign-in forms that need loading, error, and success state | useSignIn() |
| Sign-up forms that need loading, error, and success state | useSignUp() |
| Better Auth plugin/client methods that need loading, error, and success state | useAuthClientAction() |
useUserSession
The primary composable for accessing authentication state. It is safe to return directly from Pinia setup stores because it only exposes session state and session lifecycle actions.
const { loggedIn, user, session, signOut } = useUserSession()
true if the user is currently authenticated.true when initial session resolution is complete (from SSR hydration or client fetch).- Use
user,session,loggedIn,ready, andfetchSessionfromuseUserSession(). - Use
useAuthRequestFetch()oruseAuthAsyncData()for auth-bound data in pages. - Use server helpers like
serverAuth(event),getUserSession(event), orrequireUserSession(event)inserver/handlers.
Pinia setup stores
Return useUserSession() directly when you want auth state in a Pinia setup store or another shared state container.
It returns:
usersessionloggedInreadyfetchSessionwaitForSessionsignOutupdateUser
It does not return client, signIn, or signUp. Those values are runtime Better Auth client namespaces, not auth state. Keep them in components or action composables instead of storing them in hydrated state.
useUserSessionState() remains as a deprecated alias for the same store-safe shape.
export const useUserStore = defineStore('user', () => {
return useUserSession()
})
Keep sign-in and sign-up flows in components. Use useSignIn(), useSignUp(), or useAuthClientAction() when the UI needs loading, error, and success state:
<script setup lang="ts">
const signInEmail = useSignIn('email')
async function submit(email: string, password: string) {
await signInEmail.execute({ email, password })
}
</script>
useAuthClient
Returns the Vue-safe Better Auth client on the client runtime and null on the server runtime. Use this for direct client/plugin method access when you do not need action state.
const client = useAuthClient()
await client?.sendVerificationEmail({ email })
Promise Behavior
Raw Better Auth client methods return a promise that resolves when the server responds, not when local state updates.
// This awaits the server response
await client.signIn.email({ email, password })
// Local state updates asynchronously after
// Use onSuccess callback for actions that depend on updated state
await client.signIn.email(
{ email, password },
{ onSuccess: () => navigateTo('/dashboard') }
)
For form flows, prefer useSignIn() and useSignUp() because they normalize errors, sync session state, and apply configured redirects.
useSignIn / useSignUp redirects
If no onSuccess callback is provided in method options, useSignIn() will:
- navigate to
route.query.redirect(or customauth.redirectQueryKey) when it is a local path - otherwise fallback to
auth.redirects.authenticatedwhen configured and an authenticated session is established - otherwise no automatic navigation
await useSignIn('email').execute({ email, password }) // redirects to safe `?redirect=...` or auth.redirects.authenticated
useSignUp() follows the same redirect precedence:
query redirect > auth.redirects.authenticated (only when authenticated) > no auto-redirect.
signOut
Signs the user out and clears the local session state.
await signOut()
If auth.redirects.logout is configured, signOut() will navigate there automatically (client-side), unless you provide onSuccess.
Options
await signOut({
onSuccess: () => navigateTo('/'),
})
waitForSession()
Waits for session state to be ready. Resolves when user is logged in or after 5 second timeout.
await waitForSession()
// Session is now ready (or timed out)
fetchSession
Manually triggers a session refresh. Useful if you've updated user data on the server via a side channel.
await fetchSession()
Options
await fetchSession({
headers, // optional HeadersInit
force: true, // disables Better Auth cookie cache for this fetch
})
updateUser
Updates the user on the server and optimistically patches local state. Local state reverts if the server call fails.
await updateUser({ name: 'New Name' })
updateUser only patches local state since no client is available.user and session are global states using useState. Changes in one component are instantly reflected everywhere.useAuthRequestFetch
Returns Nuxt's request-scoped fetch function. On SSR it preserves request context (including cookies); on client it behaves like regular fetch.
useAuthRequestFetch() defaults to GET. For endpoints that only allow POST (or another method), pass method explicitly to preserve response type inference.
When endpoint typing is enabled, useFetch('/api/auth/...') and useLazyFetch('/api/auth/...') infer payloads directly from your Better Auth config:
const { data: customerState } = await useFetch('/api/auth/customer/state')
customerState.value?.activeSubscriptions[0]?.toUpperCase()
const { data: customerById } = await useLazyFetch('/api/auth/customer/123/state')
customerById.value?.customerId.toUpperCase()
Global $fetch keeps Nitro InternalApi response inference, but path autocomplete is best on useFetch/useLazyFetch and useAuthRequestFetch.
const requestFetch = useAuthRequestFetch()
const state = await requestFetch('/api/auth/customer/state')
const postOnly = await requestFetch('/api/auth/customer/post-only', { method: 'POST' })
Use this when you need low-level control and want to build your own data loader pattern.
useAuthAsyncData
SSR-safe helper for auth-bound data loading.
const { data: customerState, pending, error } = await useAuthAsyncData(
'customer-state',
requestFetch => requestFetch('/api/auth/customer/state'),
)
When route typing is enabled, payload types for /api/auth/* endpoints are inferred automatically.
By default:
requireAuthistrue(unauthenticated users resolve tonullwithout calling the endpoint).datadefaults tonull.- errors are exposed through
error.
const { data } = await useAuthAsyncData(
'public-profile',
requestFetch => requestFetch('/api/profile/public'),
{ requireAuth: false },
)
useAction
Creates a reusable async action handle with normalized error state.
const saveProfile = useAction(async (payload: { name: string }) => {
return await $fetch('/api/profile', { method: 'PATCH', body: payload })
})
await saveProfile.execute({ name: 'Max' })
## Related pages
- [Sessions](/core-concepts/sessions)
- [Server utilities](/api/server-utils)
- [Components](/api/components)
if (saveProfile.status.value === 'error') {
console.error(saveProfile.error.value?.message)
}
useAction returns the same handle shape as useSignIn/useSignUp:
execute() call.{ error } responses.useAuthClientAction
Wraps plugin/client methods from useAuthClient() in the same action handle pattern.
const checkout = useAuthClientAction((client) => client.checkout)
await checkout.execute({ slug: 'pro' })
if (checkout.status.value === 'error') {
console.error(checkout.error.value?.message)
}
Nested methods are supported via selector functions:
const openPortal = useAuthClientAction((client) => client.customer.portal)
await openPortal.execute()
useSignIn
Returns a keyed action handle for Better Auth signIn.*. Each method handle exposes template-friendly async state, similar to composables like useFetch.
const { execute, data, status, error } = useSignIn('email')
await execute(
{ email, password, rememberMe },
{
onSuccess: () => navigateTo('/app'),
onError: (ctx) => console.error(ctx.error),
},
)
if (status.value === 'error') {
console.error(error.value?.message)
}
For OAuth sign-in, use the social key and pass the provider in the payload:
await useSignIn('social').execute({ provider: 'github' })
Provider keys are inferred from server/auth.config.ts socialProviders keys.
When callbackURL is omitted, the module auto-fills it using the same safe redirect order as other auth flows:
- safe query redirect (
auth.redirectQueryKey) auth.redirects.authenticated- otherwise no fallback callback URL
Use renaming to avoid collisions when you use multiple methods in the same scope:
const {
execute: loginWithEmail,
status: statusEmail,
error: errorEmail,
} = useSignIn('email')
const {
execute: loginWithPasskey,
status: statusPasskey,
error: errorPasskey,
} = useSignIn('passkey')
Each method returns an action handle:
execute() call.execute() and on errors.execute()).execute() multiple times, only the latest call updates status and error.Error state and promise behavior
Use status, data, and error as your source of truth. The action handle always sets status='error' and populates normalized error when a sign-in attempt fails.
{ error } result. In both cases, the action handle updates status and error, and await execute() always resolves.Recommended flow (execute)
const { execute, data, status, error } = useSignIn('email')
await execute({ email, password })
if (status.value === 'error') {
console.error(error.value?.message)
}
if (status.value === 'success') {
console.log(data.value)
}
useUserSignIn and useUserSignUp were renamed to useSignIn and useSignUp in alpha.
The API switched from map-style access (useUserSignIn().email) to keyed access (useSignIn('email')) in alpha.
OAuth provider aliases (for example useSignIn('github')) were removed. Use useSignIn('social').execute({ provider: 'github' }).
error changed from unknown | null to AuthActionError | null in alpha.
The message alias field was removed in alpha. Use error.value?.message.
execute() changed twice in alpha:- old:
await execute()could reject - previous alpha:
await execute()resolved{ ok: true, data } | { ok: false, error } - new:
await execute()resolvesvoid, and you readstatus/data/error
error.raw.useSignUp
Same API as useSignIn, but wraps Better Auth signUp.*.
const { execute, data, status, error } = useSignUp('email')
await execute(
{ email, password, name },
{
onSuccess: () => navigateTo('/welcome'),
onError: (ctx) => console.error(ctx.error),
},
)
useSignUp returns the same action handle shape (execute, status, data, error) and follows the same error normalization semantics as useSignIn.