> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Client-side auth with AuthProvider, useAuth(), and form() integration

`AuthProvider` gives your app session management: sign-in, sign-up, sign-out, token refresh, MFA, and SSR hydration. Wrap your app, use `useAuth()` to read state, and use `form(auth.signIn)` for login — same patterns as the rest of the framework.

## Quick start

### 1. Wrap your app

```tsx theme={null}
import { AuthProvider } from '@vertz/ui-auth';

function App() {
  return (
    <AuthProvider>
      <AppContent />
    </AuthProvider>
  );
}
```

### 2. Build a login form

```tsx theme={null}
import { useAuth } from '@vertz/ui-auth';
import { form } from '@vertz/ui';

function LoginPage() {
  const auth = useAuth();

  const loginForm = form(auth.signIn, {
    onSuccess: () => navigate({ to: '/dashboard' }),
  });

  return (
    <form action={loginForm.action} method={loginForm.method} onSubmit={loginForm.onSubmit}>
      <input name={loginForm.fields.email} type="email" />
      <span>{loginForm.email.error}</span>

      <input name={loginForm.fields.password} type="password" />
      <span>{loginForm.password.error}</span>

      <button type="submit" disabled={loginForm.submitting}>
        Sign In
      </button>
    </form>
  );
}
```

### 3. Read auth state anywhere

```tsx theme={null}
import { useAuth } from '@vertz/ui-auth';

function Header() {
  const auth = useAuth();

  return (
    <header>
      {auth.isAuthenticated ? (
        <span>Welcome, {auth.user.email}</span>
      ) : (
        <a href="/login">Sign in</a>
      )}
    </header>
  );
}
```

`auth.signIn` is an `SdkMethod` — `form()` works with it directly for validation, submission, and error handling. No manual fetch calls, no state wiring.

***

## How it works

### Auth state machine

```
idle → loading → authenticated | unauthenticated | mfa_required | error
```

| Status            | Meaning                                             |
| ----------------- | --------------------------------------------------- |
| `idle`            | Initial state before any auth check (SSR/Node only) |
| `loading`         | Auth operation in progress                          |
| `authenticated`   | Valid session, `auth.user` is populated             |
| `unauthenticated` | No valid session                                    |
| `mfa_required`    | Sign-in succeeded but MFA verification needed       |
| `error`           | Auth operation failed, `auth.error` has details     |

### JWT session lifecycle

Vertz uses httpOnly cookies for JWT tokens — the client never reads the token directly. The server returns `expiresAt` in the response body, and the client schedules proactive refresh:

```
signIn/signUp → { user, expiresAt } → schedule refresh at expiresAt - 10s → POST /refresh → repeat
```

Token refresh is automatic:

* Scheduled 10 seconds before expiry
* Deduplicated (concurrent calls share one in-flight request)
* Deferred when the tab is hidden (refreshes on focus if stale)
* Deferred when offline (refreshes on reconnect)

***

## API reference

### AuthProvider

Wraps your app with auth context. All `useAuth()` calls must be inside an `AuthProvider`.

```tsx theme={null}
<AuthProvider basePath="/api/auth" accessControl>
  <App />
</AuthProvider>
```

| Prop            | Type      | Default       | Description                            |
| --------------- | --------- | ------------- | -------------------------------------- |
| `basePath`      | `string`  | `'/api/auth'` | Base URL for auth endpoints            |
| `accessControl` | `boolean` | `false`       | Enable automatic access set management |
| `children`      | `unknown` | —             | App content                            |

### useAuth()

Returns reactive auth state. All signal properties are auto-unwrapped by the compiler — no `.value` needed.

```ts theme={null}
const auth = useAuth();
```

| Property          | Type                      | Description                             |
| ----------------- | ------------------------- | --------------------------------------- |
| `user`            | `User \| null`            | Current user or null                    |
| `status`          | `AuthStatus`              | Current auth state                      |
| `isAuthenticated` | `boolean`                 | `true` when status is `'authenticated'` |
| `isLoading`       | `boolean`                 | `true` when status is `'loading'`       |
| `error`           | `AuthClientError \| null` | Last auth error                         |
| `signIn`          | `SdkMethod`               | Sign in with email/password             |
| `signUp`          | `SdkMethod`               | Create account with email/password      |
| `signOut`         | `() => Promise<void>`     | Clear session and cookies               |
| `refresh`         | `() => Promise<void>`     | Manually refresh the token              |
| `mfaChallenge`    | `SdkMethod`               | Submit MFA TOTP code                    |
| `forgotPassword`  | `SdkMethod`               | Request password reset email            |
| `resetPassword`   | `SdkMethod`               | Reset password with token               |

### AuthGate

Gates rendering on auth state resolution. Shows fallback while auth is loading, children once resolved.

```tsx theme={null}
import { AuthGate } from '@vertz/ui-auth';

<AuthGate fallback={() => <LoadingScreen />}>
  <App />
</AuthGate>;
```

### ProtectedRoute

Route guard that handles loading, authentication, entitlements, and redirect — all in one component. Wraps `useAuth()`, the router, and `can()` so you don't have to.

```tsx theme={null}
import { ProtectedRoute } from '@vertz/ui-auth';

// In route definitions:
'/dashboard': {
  component: () => (
    <ProtectedRoute loginPath="/login" fallback={() => <LoadingSpinner />}>
      <Dashboard />
    </ProtectedRoute>
  ),
}
```

| Prop        | Type            | Default    | Description                                                                              |
| ----------- | --------------- | ---------- | ---------------------------------------------------------------------------------------- |
| `loginPath` | `string`        | `'/login'` | Where to redirect unauthenticated users                                                  |
| `fallback`  | `() => unknown` | `null`     | Rendered while auth is resolving                                                         |
| `children`  | `unknown`       | —          | Rendered when authenticated                                                              |
| `requires`  | `Entitlement[]` | —          | Entitlements the user must have (checked via `can()`). Type-safe when codegen is active. |
| `forbidden` | `() => unknown` | `null`     | Rendered when authenticated but lacking entitlements                                     |
| `returnTo`  | `boolean`       | `true`     | Append `?returnTo=<currentPath>` to the login redirect                                   |

**Behavior:**

| Auth state                                   | Result                            |
| -------------------------------------------- | --------------------------------- |
| `idle` / `loading`                           | Renders `fallback`                |
| `authenticated` (entitlements met)           | Renders `children`                |
| `authenticated` (entitlements denied)        | Renders `forbidden` (no redirect) |
| `unauthenticated` / `error` / `mfa_required` | Navigates to `loginPath`          |

<Note>
  `ProtectedRoute` does NOT redirect when entitlements fail — it renders `forbidden` instead. This
  avoids redirect loops where a logged-in user is sent to login but still lacks permissions after
  signing in.
</Note>

**With entitlement checks:**

```tsx theme={null}
'/admin': {
  component: () => (
    <ProtectedRoute
      requires={['admin:manage']}
      forbidden={() => <p>You don't have access to this page.</p>}
    >
      <AdminPanel />
    </ProtectedRoute>
  ),
}
```

**Without a provider (fail-open):** If there's no `AuthProvider` in the tree, `ProtectedRoute` renders children and logs a dev-mode warning. This matches `AuthGate`'s fail-open behavior.

**SSR:** During server rendering, `ProtectedRoute` renders the fallback. The redirect fires client-side after hydration.

***

## Auth methods as SdkMethods

Every auth method (`signIn`, `signUp`, `mfaChallenge`, `forgotPassword`, `resetPassword`) is an `SdkMethod`. This means:

1. **`form()` works directly** — validation, submission, field errors, all automatic
2. **`.url` and `.method`** are available for `<form action={...} method={...}>`
3. **`.meta.bodySchema`** provides the validation schema

```tsx theme={null}
// All of these work the same way
const loginForm = form(auth.signIn);
const signupForm = form(auth.signUp);
const mfaForm = form(auth.mfaChallenge);
const forgotForm = form(auth.forgotPassword);
const resetForm = form(auth.resetPassword);
```

### Input types

| Method           | Required fields                       |
| ---------------- | ------------------------------------- |
| `signIn`         | `{ email: string, password: string }` |
| `signUp`         | `{ email: string, password: string }` |
| `mfaChallenge`   | `{ code: string }`                    |
| `forgotPassword` | `{ email: string }`                   |
| `resetPassword`  | `{ token: string, password: string }` |

***

## MFA flow

When the server requires MFA, `signIn` transitions to `mfa_required` instead of `authenticated`:

```tsx theme={null}
function LoginPage() {
  const auth = useAuth();

  const loginForm = form(auth.signIn);
  const mfaForm = form(auth.mfaChallenge, {
    onSuccess: () => navigate({ to: '/dashboard' }),
  });

  // Show MFA form when required
  if (auth.status === 'mfa_required') {
    return (
      <form onSubmit={mfaForm.onSubmit}>
        <input name={mfaForm.fields.code} placeholder="Enter 6-digit code" />
        <span>{mfaForm.code.error}</span>
        <button type="submit" disabled={mfaForm.submitting}>
          Verify
        </button>
      </form>
    );
  }

  return (
    <form onSubmit={loginForm.onSubmit}>
      <input name={loginForm.fields.email} type="email" />
      <input name={loginForm.fields.password} type="password" />
      <button type="submit" disabled={loginForm.submitting}>
        Sign In
      </button>
    </form>
  );
}
```

***

## Password reset flow

### Request reset email

```tsx theme={null}
function ForgotPasswordPage() {
  const auth = useAuth();
  let submitted = false;

  const forgotForm = form(auth.forgotPassword, {
    onSuccess: () => {
      submitted = true;
    },
  });

  if (submitted) {
    return <p>Check your email for a reset link.</p>;
  }

  return (
    <form onSubmit={forgotForm.onSubmit}>
      <input name={forgotForm.fields.email} type="email" placeholder="Your email" />
      <span>{forgotForm.email.error}</span>
      <button type="submit" disabled={forgotForm.submitting}>
        Send Reset Link
      </button>
    </form>
  );
}
```

### Reset with token

```tsx theme={null}
function ResetPasswordPage() {
  const auth = useAuth();
  const token = new URLSearchParams(location.search).get('token');

  const resetForm = form(auth.resetPassword, {
    onSuccess: () => navigate({ to: '/login' }),
  });

  return (
    <form onSubmit={resetForm.onSubmit}>
      <input type="hidden" name={resetForm.fields.token} value={token} />
      <input name={resetForm.fields.password} type="password" placeholder="New password" />
      <span>{resetForm.password.error}</span>
      <button type="submit" disabled={resetForm.submitting}>
        Reset Password
      </button>
    </form>
  );
}
```

***

## SSR hydration

When using SSR, the server injects the session into the page so the client doesn't need an initial `/api/auth/session` fetch.

### Server side

```ts theme={null}
import { createSessionScript } from '@vertz/ui-server';

// In your SSR handler, after validating the JWT:
const sessionScript = createSessionScript({
  user: { id: '1', email: 'user@example.com', role: 'admin' },
  expiresAt: session.expiresAt.getTime(),
});

// Include in the HTML response <head>
```

### What happens on the client

1. Server injects `window.__VERTZ_SESSION__` with `{ user, expiresAt }`
2. `AuthProvider` reads it on initialization — no fetch needed
3. Auth state is `'authenticated'` immediately — no loading flicker
4. Token refresh is scheduled from the hydrated `expiresAt`

When there's no session (guest user), `AuthProvider` transitions to `'unauthenticated'` immediately.

***

## Access control integration

When `accessControl` is enabled, `AuthProvider` automatically manages the access set:

```tsx theme={null}
<AuthProvider accessControl>
  <App />
</AuthProvider>
```

This:

* Wraps children in `AccessContext.Provider`
* Fetches the access set from `${basePath}/access-set` after successful auth
* Clears the access set on sign out
* Hydrates from `window.__VERTZ_ACCESS_SET__` during SSR

Use `can()` anywhere inside the provider:

```tsx theme={null}
import { can } from '@vertz/ui-auth';

function AdminPanel() {
  const check = can('admin:manage');

  if (!check.allowed) {
    return <p>Access denied</p>;
  }

  return <AdminDashboard />;
}
```

See the [Access Control guide](/guides/ui/access-control) for full details on `can()`, `AccessGate`, and entity-scoped checks.

***

## Error handling

Auth errors are available via `auth.error`:

```tsx theme={null}
const auth = useAuth();

if (auth.error) {
  // auth.error has: code, message, statusCode, retryAfter?
  return <div className="error">{auth.error.message}</div>;
}
```

### Error codes

| Code                  | When                                                           |
| --------------------- | -------------------------------------------------------------- |
| `INVALID_CREDENTIALS` | Wrong email/password                                           |
| `USER_EXISTS`         | Email already registered                                       |
| `MFA_REQUIRED`        | MFA verification needed (status transitions to `mfa_required`) |
| `INVALID_MFA_CODE`    | Wrong MFA code                                                 |
| `RATE_LIMITED`        | Too many attempts (`retryAfter` is set)                        |
| `NETWORK_ERROR`       | Fetch failed (offline, DNS, etc.)                              |
| `SERVER_ERROR`        | Unexpected server error                                        |

***

## Common patterns

### Logout button

```tsx theme={null}
function LogoutButton() {
  const auth = useAuth();

  return <button onClick={() => auth.signOut()}>Sign Out</button>;
}
```

### Conditional rendering based on auth

```tsx theme={null}
function AppContent() {
  const auth = useAuth();

  return (
    <AuthGate fallback={() => <LoadingSpinner />}>
      {auth.isAuthenticated ? <Dashboard /> : <LandingPage />}
    </AuthGate>
  );
}
```

### Sign-up with extra fields

```tsx theme={null}
const signupForm = form(auth.signUp);

<form onSubmit={signupForm.onSubmit}>
  <input name={signupForm.fields.email} type="email" />
  <input name={signupForm.fields.password} type="password" />
  <input name={signupForm.fields.name} />
  <button type="submit">Create Account</button>
</form>;
```

The `signUp` input accepts `{ email, password, ...extra }`, but reserved auth fields such as
`role`, `plan`, `emailVerified`, `id`, and timestamps are ignored by the auth handler.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Multi-Tenancy" icon="building" href="/guides/ui/multi-tenancy">
    TenantProvider, useTenant(), and TenantSwitcher for multi-tenant apps.
  </Card>

  <Card title="Server-Side Auth" icon="lock" href="/guides/server/auth">
    Configure JWT sessions, RBAC, plans, and usage limits.
  </Card>

  <Card title="Access Control" icon="shield" href="/guides/ui/access-control">
    Use `can()` and `AccessGate` in your UI components.
  </Card>

  <Card title="Forms" icon="rectangle-list" href="/guides/ui/forms">
    Deep dive into `form()` — validation, fields, progressive enhancement.
  </Card>

  <Card title="SSR" icon="server" href="/guides/ui/ssr">
    Server-side rendering setup and hydration.
  </Card>
</CardGroup>
