Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/app/components/content/landing/hero.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ tabs:
- name: pages/login.vue
code: |
<script setup lang="ts">
const { signIn } = useUserSession()
const signInSocial = useSignIn('social')
</script>

<template>
<button @click="signIn.social({ provider: 'github' })">
<button @click="signInSocial.execute({ provider: 'github' })">
Sign in with GitHub
</button>
</template>
9 changes: 5 additions & 4 deletions docs/content/1.getting-started/3.client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Set up the client auth config for @onmax/nuxt-better-auth.
- Object syntax: `defineClientAuth({})` or function syntax: `defineClientAuth(({ siteUrl }) => ({}))`
- Add client plugin equivalents for every server plugin (e.g. `adminClient()` from `better-auth/client/plugins`)
- The module calls the factory with the correct `baseURL` at runtime
- `useUserSession()` is auto-imported and provides `user`, `session`, `loggedIn`, `ready`, `signIn`, `signUp`, `signOut`
- `useUserSession()` is auto-imported and provides `user`, `session`, `loggedIn`, `ready`, `signOut`
- `useSignIn()`, `useSignUp()`, and `useAuthClient()` are auto-imported for auth actions and direct client access
```

::
Expand Down Expand Up @@ -80,10 +81,10 @@ export default defineClientAuth({
<script setup lang="ts">
definePageMeta({ auth: 'guest' })

const { signIn } = useUserSession()
const signInEmail = useSignIn('email')

async function login(email: string, password: string) {
await signIn.email(
await signInEmail.execute(
{ email, password },
{ onSuccess: () => navigateTo('/app') },
)
Expand All @@ -96,6 +97,6 @@ async function login(email: string, password: string) {
Confirm all of the following:

- `useUserSession()` is available without a manual import
- `client` is available on the browser after hydration
- `useAuthClient()` returns the Better Auth client on the browser after hydration
- client plugins are registered on both server and client when required by Better Auth
- sign-in and sign-out update `user`, `session`, and `loggedIn`
8 changes: 3 additions & 5 deletions docs/content/2.core-concepts/2.sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ description: Access reactive session state with SSR support using `useUserSessio
```txt [Prompt]
Handle sessions with @onmax/nuxt-better-auth using useUserSession().

- `useUserSession()` is auto-imported and returns: user, session, loggedIn, ready, signIn, signUp, signOut, fetchSession
- `useUserSession()` is auto-imported and returns: user, session, loggedIn, ready, signOut, fetchSession
- SSR: session is fetched server-side via cookies and hydrated — `ready` is `true` after hydration
- Client: `.client` is `null` during SSR, available after hydration
- Client: use `useSignIn()`, `useSignUp()`, or `useAuthClient()` for Better Auth client methods
- Split model: use `useUserSession()` in pages/components, use `requireUserSession(event)` in server handlers
- Configure session lifetime in `server/auth.config.ts` via `session.expiresIn` and `session.cookieCache`
- Use `<BetterAuthState>` or `ready` ref to avoid loading flashes
Expand All @@ -27,8 +27,6 @@ const {
session,
loggedIn,
ready,
signIn,
signUp,
signOut,
fetchSession,
} = useUserSession()
Expand All @@ -39,7 +37,7 @@ const {
During server-side rendering (SSR), the module fetches the session using incoming request cookies and populates state before rendering. A client plugin then keeps the state in sync after hydration.

- **Server render:** `user` and `session` are set when a valid session cookie exists, and `ready` is `true`.
- **Server runtime client access:** `useUserSession().client` is `null` during SSR.
- **Server runtime client access:** `useAuthClient()` returns `null` during SSR.
- **After hydration:** State stays in sync with client-side updates.

Use a split model:
Expand Down
5 changes: 3 additions & 2 deletions docs/content/2.core-concepts/3.route-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ When preserving the original URL for post-login redirects, validate it to preven

```ts [pages/login.vue]
const route = useRoute()
const signInEmail = useSignIn('email')

function getSafeRedirect() {
const redirect = route.query.redirect as string
Expand All @@ -206,7 +207,7 @@ function getSafeRedirect() {
}

async function login(email: string, password: string) {
await signIn.email(
await signInEmail.execute(
{ email, password },
{ onSuccess: () => navigateTo(getSafeRedirect()) },
)
Expand Down Expand Up @@ -248,7 +249,7 @@ Better Auth includes CSRF protection by default. Always use the auth client meth

```ts
// ✓ Correct: uses auth client
await signIn.email({ email, password })
await useSignIn('email').execute({ email, password })

// ✗ Incorrect: bypasses CSRF protection
await fetch('/api/auth/sign-in/email', { method: 'POST', body: JSON.stringify({ email, password }) })
Expand Down
4 changes: 3 additions & 1 deletion docs/content/2.core-concepts/4.auto-imports-aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: What the module registers for you.
```txt [Prompt]
Use auto-imported utilities from @onmax/nuxt-better-auth.

- Client (auto-imported in Vue): `useUserSession()`, `useUserSessionState()`, `useSignIn()`, `useSignUp()`
- Client (auto-imported in Vue): `useUserSession()`, `useUserSessionState()`, `useAuthClient()`, `useSignIn()`, `useSignUp()`, `useAuthClientAction()`
- Server (auto-imported in `server/`): `serverAuth(event?)`, `getRequestSession(event)`, `getUserSession(event)`, `createSession(event, userId)`, `setSessionCookie(event, token)`, `requireUserSession(event, options?)`
- Component: `<BetterAuthState>` for auth-ready rendering
- Alias `#nuxt-better-auth` exports types: `AuthUser`, `AuthSession`, `AuthSocialProviderId`
Expand All @@ -25,8 +25,10 @@ Use this page when you want a quick inventory of what the module registers for y

- `useUserSession()`
- `useUserSessionState()`
- `useAuthClient()`
- `useSignIn()`
- `useSignUp()`
- `useAuthClientAction()`

**Server**

Expand Down
10 changes: 5 additions & 5 deletions docs/content/3.guides/2.oauth-providers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: OAuth Providers
description: Configure OAuth providers and sign in with `signIn.social()`.
description: Configure OAuth providers and sign in with `useSignIn('social')`.
---

Use this guide when you want to add a social provider to a full-mode Nuxt Better Auth setup.
Expand Down Expand Up @@ -50,21 +50,21 @@ export default defineServerAuth({
```vue [pages/login.vue]
<script setup lang="ts">
definePageMeta({ auth: 'guest' })
const { signIn } = useUserSession()
const signInSocial = useSignIn('social')
</script>

<template>
<button
type="button"
@click="signIn.social({ provider: 'google' })"
@click="signInSocial.execute({ provider: 'google' })"
>
Continue with Google
</button>
</template>
```

::tip
For OAuth flows (`signIn.social`), Better Auth handles the browser redirect to the provider.
For OAuth flows (`useSignIn('social')`), Better Auth handles the browser redirect to the provider.
No manual `fetchSession`/`waitForSession` step is needed before that redirect.
`callbackURL` is optional. When your app configures `auth.redirects.authenticated` (or has a safe `?redirect=` query), the module can use that as the fallback callback URL.
::
Expand All @@ -87,6 +87,6 @@ Better Auth supports 20+ providers (Apple, Discord, Facebook, etc). The pattern
1. Create OAuth app, get client ID/secret
2. Add `{PROVIDER}_CLIENT_ID` and `{PROVIDER}_CLIENT_SECRET` to `.env`
3. Reference credentials in `server/auth.config.ts`
4. Call `signIn.social({ provider: 'providerId' })`
4. Call `useSignIn('social').execute({ provider: 'providerId' })`

:read-more{to="https://www.better-auth.com/docs/authentication/other-social-providers" title="Full provider list"}
4 changes: 3 additions & 1 deletion docs/content/3.guides/6.migrate-from-nuxt-auth-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ This module is a **Nuxt wrapper** around [Better Auth](https://www.better-auth.c
const { user, loggedIn, ready, fetch, clear } = useUserSession()

// nuxt-better-auth
const { user, session, loggedIn, ready, signIn, signUp, signOut, fetchSession, client } = useUserSession()
const { user, session, loggedIn, ready, signOut, fetchSession } = useUserSession()
const signInEmail = useSignIn('email')
const client = useAuthClient()
```

### OAuth Handling
Expand Down
2 changes: 1 addition & 1 deletion docs/content/3.guides/7.two-factor-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default defineClientAuth({
```ts [pages/two-factor.vue]
<script setup lang="ts">
definePageMeta({ auth: 'user' })
const { client } = useUserSession()
const client = useAuthClient()
const password = ref('')
const qr = ref<string | null>(null)
const backupCodes = ref<string[] | null>(null)
Expand Down
70 changes: 26 additions & 44 deletions docs/content/5.api/1.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ These composables are auto-imported in Vue files. No import statement is needed

## Quick Start

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

```vue [pages/app.vue]
<script setup lang="ts">
Expand All @@ -29,17 +29,18 @@ const { user, loggedIn, signOut } = useUserSession()

| Use case | Composable |
|----------|------------|
| Pages and components that need auth state, auth methods, or direct Better Auth client access | `useUserSession()` |
| 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. Returns reactive user, session, and auth client.
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.

```ts [pages/login.vue]
const { loggedIn, user, session, client, signIn, signOut } = useUserSession()
const { loggedIn, user, session, signOut } = useUserSession()
```

::field-group
Expand All @@ -55,14 +56,12 @@ const { loggedIn, user, session, client, signIn, signOut } = useUserSession()
::field{name="ready" type="ComputedRef<boolean>"}
`true` when initial session resolution is complete (from SSR hydration or client fetch).
::
::field{name="client" type="AuthClient | null"}
Direct access to the Better Auth client instance. Available on client runtime; `null` during SSR/server runtime.
::field{name="signOut" type="(options?: SignOutOptions) => Promise<void>"}
Signs out and clears local session state.
::
::

::note
`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.
Expand All @@ -71,7 +70,7 @@ For SSR-safe auth access:

## 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.
Return `useUserSession()` directly when you want auth state in a Pinia setup store or another shared state container.

It returns:
- `user`
Expand All @@ -85,11 +84,11 @@ It returns:

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:
`useUserSessionState()` remains as a deprecated alias for the same store-safe shape.

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

Expand All @@ -105,28 +104,18 @@ async function submit(email: string, password: string) {
</script>
```

::note
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`
## useAuthClient

Proxies Better Auth `signIn`.
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.

```ts
await signIn.email({
email: 'user@example.com',
password: 'password'
})
const client = useAuthClient()
await client?.sendVerificationEmail({ email })
```

### Promise Behavior

Methods like `signIn` return a promise that resolves when the **server responds**, not when local state updates.
Raw Better Auth client methods return a promise that resolves when the **server responds**, not when local state updates.

```ts
// This awaits the server response
Expand All @@ -140,30 +129,23 @@ await client.signIn.email(
)
```

If no `onSuccess` callback is provided in method options, `signIn` will:
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 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

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

```ts
await signUp.email({
email: 'user@example.com',
password: 'password'
})
await useSignIn('email').execute({ email, password }) // redirects to safe `?redirect=...` or auth.redirects.authenticated
```

Like `signIn`, if no callback is provided, `signUp` follows the same redirect precedence:
`useSignUp()` follows the same redirect precedence:
query redirect > `auth.redirects.authenticated` (only when authenticated) > no auto-redirect.

#### `signOut`
### `signOut`

Signs the user out and clears the local session state.

Expand Down Expand Up @@ -322,7 +304,7 @@ if (saveProfile.status.value === 'error') {

## useAuthClientAction

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

```ts [pages/pricing.vue]
const checkout = useAuthClientAction((client) => client.checkout)
Expand All @@ -343,7 +325,7 @@ 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`.
Returns a keyed action handle for Better Auth `signIn.*`. Each method handle exposes template-friendly async state, similar to composables like `useFetch`.

```ts [pages/login.vue]
const { execute, data, status, error } = useSignIn('email')
Expand Down Expand Up @@ -450,7 +432,7 @@ If you relied on raw payloads, use `error.raw`.

## useSignUp

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

```ts [pages/signup.vue]
const { execute, data, status, error } = useSignUp('email')
Expand Down
44 changes: 44 additions & 0 deletions src/runtime/app/composables/useAuthClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { AppAuthClient } from '#nuxt-better-auth'
import createAppAuthClient from '#auth/client'
import { useRequestURL, useRuntimeConfig } from '#imports'
import { createVueSafeAuthProxy } from '../internal/vue-safe-auth-proxy'

interface RuntimeFlags { client: boolean, server: boolean }

let _client: AppAuthClient | null = null
let _clientFacade: AppAuthClient | null = null

export function getAuthRuntimeFlags(): RuntimeFlags {
const globalFlags = (globalThis as { __NUXT_BETTER_AUTH_TEST_FLAGS__?: RuntimeFlags }).__NUXT_BETTER_AUTH_TEST_FLAGS__
if (globalFlags)
return globalFlags
return { client: Boolean(import.meta.client), server: Boolean(import.meta.server) }
}

function getClient(baseURL: string): AppAuthClient {
if (!_client)
_client = createAppAuthClient(baseURL)
return _client
}

function getClientFacade(client: AppAuthClient): AppAuthClient {
if (!_clientFacade)
_clientFacade = createVueSafeAuthProxy(client)
return _clientFacade
}

export function useRawAuthClient(): AppAuthClient | null {
const runtimeFlags = getAuthRuntimeFlags()
if (!runtimeFlags.client)
return null

const runtimeConfig = useRuntimeConfig()
const requestURL = useRequestURL()
const siteUrl = typeof runtimeConfig.public.siteUrl === 'string' ? runtimeConfig.public.siteUrl : requestURL.origin
return getClient(siteUrl)
}

export function useAuthClient(): AppAuthClient | null {
const rawClient = useRawAuthClient()
return rawClient ? getClientFacade(rawClient) : null
}
Loading
Loading