`createClient(url, { auth })` attaches `api.auth` — session orchestration over the same `/auth/*`
Flows. Prefer that one call. `createAuthClient` + `bind` stays as a compose escape hatch.

For storefronts and SPAs attaching identity to typed calls.

<Callout title="The one rule">
  Gate on Flows is real authorization. `authorize` / `hasScope` / `Can` are **UI-only**. Cookie mode
  needs the `csrf` plugin (server soft-requires it when `gate.auth.cookies.enabled`); never persist
  tokens to Storage when cookies own the session.
</Callout>

## Smallest Example

<Steps>

<Step>
### One `createClient` with session auth

```typescript title="web/src/api.ts"
import { createClient } from "okengine/client";
import { vault } from "okengine/vault";
import { app } from "../../src/app";

export const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "", {
  auth: { mode: "cookie", csrfConfigured: true },
});
```

`api.auth` is an `AuthClient`. Unit Flows remain reachable (`api.auth.me`, …) under the same
namespace.

</Step>

<Step>
### Sign in and authorize chrome

```typescript
const result = await api.auth.signIn.email({ email: "alex@acme.co", password });
if (!result.ok) {
  if (result.twoFactor) {
    await api.auth.completeChallenge({
      challengeId: result.twoFactor.challengeId,
      code: totp,
    });
  }
  return;
}

const gate = api.auth.authorize({ all: ["orders:write"] });
if (gate.status === "allowed") showFulfillment();
await api.auth.signOut();
```

</Step>

</Steps>

## Progressive Patterns

<Tabs items={["Authorize", "Cookie mode", "SSR", "Methods", "Escape hatch"]}>

<Tab value="Authorize">

```typescript
const gate = api.auth.authorize({ all: ["orders:write"] });
// or: api.auth.authorize({ any: ["orders:write", "admin"] })

if (gate.status === "unauthenticated") redirectToSignIn();
if (gate.status === "loading") return;
if (gate.status === "denied") showMissing(gate.missing);
// gate.status === "allowed"
```

React: `<Can auth={api.auth} all={["orders:write"]} fallback={<Denied />}>`. Prefer
`authorize` / `Can` over bare `hasScope`. On server `Forbidden`, use `isForbidden` /
`forbiddenScopes` and refresh `getSession()` if scopes look stale.

</Tab>

<Tab value="Cookie mode">

When `gate.auth.cookies.enabled`, prefer HttpOnly cookies:

```typescript
export const api = createClient(app, base, {
  auth: { mode: "cookie", csrfConfigured: true },
});
```

Install `csrf({ allowNoHeader: false })`. Cross-origin also needs
`cors({ origin: [...], credentials: true })`. Prod boots **refuse** without `csrf` when cookies
are on; dev/test warn.

</Tab>

<Tab value="SSR">

```typescript
import { createClient } from "okengine/client";
import { tokenFromRequestCookies, createServerClient } from "okengine/client/auth";

// Explicit
const api = createClient(base, {
  credentials: "include",
  auth: {
    mode: "cookie",
    getToken: () => tokenFromRequestCookies(req),
  },
});

// Or the thin wrapper (same createClient)
const api2 = createServerClient(req, base, { $routes });
```

</Tab>

<Tab value="Methods">

```typescript
await api.auth.signIn.social({ provider: "google" });
await api.auth.signIn.passkey({ email });
await api.auth.signIn.magicLink.request({ email });
await api.auth.signIn.otp.verify({ email, code });
await api.auth.signIn.anonymous();
await api.auth.signUp.email({ email, password, name });
```

Helpers call existing plugin Flows only — PKCE / WebAuthn UV stay server-side.

</Tab>

<Tab value="Escape hatch">

```typescript
import { createClient } from "okengine/client";
import { createAuthClient } from "okengine/client/auth";

const shell = createClient(app, base);
const auth = createAuthClient(shell, { mode: "bearer", persist: "memory" });
const api = createClient(app, base, { ...auth.clientOptions });
auth.bind(api);
```

</Tab>

</Tabs>

## Helpers

| Helper                                           | Package                 | Role                             |
| ------------------------------------------------ | ----------------------- | -------------------------------- |
| `createClient({ auth })`                         | `okengine/client`       | Happy path — attaches `api.auth` |
| `createAuthClient`                               | `okengine/client/auth`  | Compose / bind escape hatch      |
| `tokenFromRequestCookies` / `createServerClient` | same                    | SSR cookie → token               |
| `authorize` / `hasScope` / `can`                 | on `AuthClient`         | UI-only chrome                   |
| `isUnauthorized` / `isCsrf` / `forbiddenScopes`  | same                    | Envelope narrowers               |
| `useSession` / `useAuthorize` / `Can`            | `okengine/client-react` | React session + chrome           |

## Denial codes

| Code           | HTTP | Typical fix                                    |
| -------------- | ---- | ---------------------------------------------- |
| `Unauthorized` | 401  | Sign in or refresh; re-login if still denied   |
| `Forbidden`    | 403  | Wrong scopes / CSRF — read `error.data.reason` |
| `RateLimited`  | 429  | Wait `retryAfterMs`                            |

Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf). Advanced create-oke starter
demos cookie + passkey + `<Can>`.

## Troubleshooting

<Accordions>

<Accordion title="Cookie mode warns about CSRF / prod refuses boot">
  Plug `csrf({ allowNoHeader: false })` when `gate.auth.cookies.enabled`. Set
  `csrfConfigured: true` on the client only after the plugin is installed.
</Accordion>

<Accordion title="authorize allowed but Flow returns Forbidden">
  Expected — client authorize is UI-only. Add `gate.scope` / `gate.policy` on the Flow.
</Accordion>

<Accordion title="localStorage warning">
  Prefer cookie mode or memory / `sessionStorage`. XSS can read Storage until family revoke.
</Accordion>

</Accordions>

## Learn more

- [Calling](/docs/client/calling) — envelopes, binary `{ response: "blob" }`, REST
- [React](/docs/client/react) — `useSession` / `Can` / `useAuthorize`
- [Gate · Auth](/docs/elements/gate/auth) — server cookies and scopes

## Next

<Cards>
  <Card title="Live" href="/docs/client/live" />
  <Card title="React" href="/docs/client/react" />
  <Card title="Gate Auth" href="/docs/elements/gate/auth" />
</Cards>
