This library is in early development. Expect breaking changes.
API Reference

Composables

API reference for client-side composables.

Use this page when you need exact behavior for the client-side composables exposed by the module.

These composables are auto-imported in Vue files. No import statement is needed in pages, components, plugins, middleware, or stores inside your Nuxt app.

Quick Start

Use useUserSession() in pages and components to access the current session and auth methods.

pages/app.vue
<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 caseComposable
Pages and components that need auth state, auth methods, or direct Better Auth client accessuseUserSession()
Sign-in forms that need loading, error, and success stateuseSignIn()
Sign-up forms that need loading, error, and success stateuseSignUp()
Better Auth plugin/client methods that need loading, error, and success stateuseAuthClientAction()

useUserSession

The primary composable for accessing authentication state. Returns reactive user, session, and auth client.

pages/login.vue
const { loggedIn, user, session, client, signIn, signOut } = useUserSession()
loggedIn
ComputedRef<boolean>
true if the user is currently authenticated.
user
Ref<AuthUser | null>
The current user object, inferred from your config.
session
Ref<AuthSession | null>
The current session object.
ready
ComputedRef<boolean>
true when initial session resolution is complete (from SSR hydration or client fetch).
client
AuthClient | null
Direct access to the Better Auth client instance. Available on client runtime; null during SSR/server runtime.
useUserSession().client is browser-only. During SSR/server runtime it is null.For SSR-safe auth access:
  • Use user, session, loggedIn, ready, and fetchSession from useUserSession().
  • Use useAuthRequestFetch() or useAuthAsyncData() for auth-bound data in pages.
  • Use server helpers like serverAuth(event), getUserSession(event), or requireUserSession(event) in server/ handlers.

Pinia setup stores

Use useUserSessionState() when you want auth state in a Pinia setup store or another shared state container. It exposes the session state and session actions from useUserSession() without exposing the Better Auth client namespaces.

It returns:

  • user
  • session
  • loggedIn
  • ready
  • fetchSession
  • waitForSession
  • signOut
  • updateUser

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.

Return useUserSessionState() directly from a setup store when the store only needs session state and session actions:

app/stores/user.ts
export const useUserStore = defineStore('user', () => {
  return useUserSessionState()
})

Keep sign-in and sign-up flows in components. Use useSignIn(), useSignUp(), or useAuthClientAction() when the UI needs loading, error, and success state:

pages/login.vue
<script setup lang="ts">
const signInEmail = useSignIn('email')

async function submit(email: string, password: string) {
  await signInEmail.execute({ email, password })
}
</script>
If a Pinia store intentionally exposes useUserSession().client, keep it out of hydrated state with Pinia's skipHydrate() and Vue's shallowRef() / markRaw().Most stores should use useUserSessionState() instead.

Methods

signIn

Proxies Better Auth signIn.

await signIn.email({
  email: 'user@example.com',
  password: 'password'
})

Promise Behavior

Methods like signIn 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') }
)

If no onSuccess callback is provided in method options, signIn will:

  • navigate to route.query.redirect (or custom auth.redirectQueryKey) when it is a local path
  • otherwise fallback to auth.redirects.authenticated when configured and an authenticated session is established
  • otherwise no automatic navigation
await signIn.email({ email, password }) // redirects to safe `?redirect=...` or auth.redirects.authenticated

signUp

Proxies Better Auth signUp with the same onSuccess behavior as signIn.

await signUp.email({
  email: 'user@example.com',
  password: 'password'
})

Like signIn, if no callback is provided, signUp 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)
Use this when you need to ensure session state before proceeding. The function always resolves - it doesn't throw or return a value.

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' })
During SSR, updateUser only patches local state since no client is available.
Reactivity: 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:

pages/app.vue
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.

pages/app.vue
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.

pages/app.vue
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:

  • requireAuth is true (unauthenticated users resolve to null without calling the endpoint).
  • data defaults to null.
  • errors are exposed through error.
pages/app.vue
const { data } = await useAuthAsyncData(
  'public-profile',
  requestFetch => requestFetch('/api/profile/public'),
  { requireAuth: false },
)

useAction

Creates a reusable async action handle with normalized error state.

components/ProfileForm.vue
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
(...args) => Promise<void>
Executes the action and never throws.
status
'idle' | 'pending' | 'success' | 'error'
Current status of the latest execute() call.
data
any | null
Last successful result.
error
AuthActionError | null
Normalized error from thrown errors or { error } responses.

useAuthClientAction

Wraps plugin/client methods from useUserSession().client in the same action handle pattern.

pages/pricing.vue
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:

pages/app/index.vue
const openPortal = useAuthClientAction((client) => client.customer.portal)
await openPortal.execute()

useSignIn

Returns a keyed action handle for useUserSession().signIn.*. Each method handle exposes template-friendly async state, similar to composables like useFetch.

pages/login.vue
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:

pages/login.vue
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:

pages/login.vue
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
(...args) => Promise<void>
Calls the underlying Better Auth method and never throws.
status
'idle' | 'pending' | 'success' | 'error'
Current status of the latest execute() call.
data
any | null
Last successful result. Cleared on each new execute() and on errors.
error
AuthActionError | null
Normalized error for the latest call (cleared on new execute()).
Each sign-in method has independent state. When you call 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.

Better Auth methods can signal failure by throwing or by resolving to a { error } result. In both cases, the action handle updates status and error, and await execute() always resolves.
pages/login.vue
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() resolves void, and you read status / data / error
If you relied on raw payloads, use error.raw.

useSignUp

Same API as useSignIn, but wraps useUserSession().signUp.*.

pages/signup.vue
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.