# Express (/docs/integrations/express)

> Serve the Better IAM API from an Express 4 or 5 app with createIamExpress, read the session from req.iam, and guard routes with requireSession and authorize.



`createIamExpress` connects a `betterIam()` instance to an Express (or Connect) application. One middleware serves
the IAM HTTP API under `/api/iam` and gives every other request a `req.iam` object with the caller's
session, permission checks, and an in-process client. Route guards
(`requireSession`, `authorize`) refuse signed-out callers, callers without a permission, and cross-site form posts,
and an error handler turns refusals into the IAM JSON error envelope or a redirect to your login page.

Use it when your server is an Express app. For Hono and Fastify, use the [Hono](/docs/integrations/hono) and
[Fastify](/docs/integrations/fastify) adapters; they expose the same helpers. For a plain `node:http` server or a
framework with no adapter, build on the [framework-neutral core](/docs/integrations/node).

## Installation [#installation]

      ### Install the package [#install-the-package]

    <CodeBlockTabs defaultValue="npm" groupId="package-manager">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm i better-iam express
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add better-iam express
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add better-iam express
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add better-iam express
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    The umbrella package `better-iam` exposes the adapter as `better-iam/express` and the shared core as
    `better-iam/middleware`. If you install scoped packages instead, install `@better-iam/middleware` and import from
    `@better-iam/middleware/express` and `@better-iam/middleware`. The adapter's types cover Express 4 and 5; the repository's tests run it on Express 5.
  
      ### Create the instance [#create-the-instance]

    Create the instance once and import it everywhere. The configuration file is the one the
    [quickstart](/docs/quickstart) builds, which the `better-iam` CLI also reads for migrations.

    ```ts title="src/iam.ts"
    import { betterIam } from 'better-iam';
    import config from '../better-iam.config.mjs';

    export const iam = betterIam(config);
    ```

    Run the migrations before the first start:

    ```bash
    npx better-iam migrate --config better-iam.config.mjs
    ```
  
      ### Create the adapter [#create-the-adapter]

    Create the adapter in its own module so route files can import its guards.

    ```ts title="src/iam-express.ts"
    import { createIamExpress } from 'better-iam/express';
    import { iam } from './iam.js';

    export const iamExpress = createIamExpress(iam, {
      loginPath: '/login', // signed-out page navigations go here with ?next=
      stepUpPath: '/verify', // page navigations that must step up go here with ?next= and ?reason=
    });
    ```
  
      ### Mount the handler [#mount-the-handler]

    Register `iamExpress.middleware` before any body parser. It answers every request under the instance's base path
    (`/api/iam`) and attaches `req.iam` to all other requests. Register `errorHandler` after your routes.

    ```ts title="src/server.ts"
    import express from 'express';
    import { iamExpress } from './iam-express.js';
    import { projects } from './routes/projects.js';

    const app = express();
    app.use(iamExpress.middleware); // before express.json(): the IAM server reads the raw body
    app.use(express.json());
    app.use(express.urlencoded({ extended: false })); // for plain HTML form posts

    app.use(projects);

    app.use(iamExpress.errorHandler); // last: IAM refusals thrown by routes become JSON or a redirect
    app.listen(3000);
    ```

    The route examples below each export an Express `Router`; mount each one the same way, with `app.use(router)`,
    between the body parsers and the error handler.

    * **Order matters.** The IAM server reads the raw request stream, which also lets node-only protocol mounts (the
      OAuth provider) work. If a parser runs first, the adapter re-serializes the parsed body for the JSON API (up to
      2 MiB; larger bodies answer `413`), but protocol mounts need the untouched stream.
    * **Mounting under a path** (`app.use('/api/iam', iamExpress.middleware)`) also works: the adapter restores the
      path Express strips from `req.url`.
    * **Protocol mounts outside the base path**, such as `/oauth` or `/.well-known`, need their prefixes in `serve`
      (see [Protocol mounts](/docs/operations/deployment/protocol-mounts)).
  
      ### Type `req.iam` [#type-reqiam]

    Declare the helpers on Express's `Request` so `req.iam` is typed with your instance's session and API.

    ```ts title="src/types.d.ts"
    import type { IamRequest } from 'better-iam/middleware';
    import type { iam } from './iam.js';

    declare global {
      namespace Express {
        interface Request {
          iam: IamRequest<typeof iam>;
        }
      }
    }
    ```
  
      ### Create the browser client [#create-the-browser-client]

    Browser code calls the API you just mounted with the [typed client](/docs/concepts/client). Import the instance
    with `import type` so no server code reaches the bundle.

    ```ts title="web/iam-client.ts"
    import { createIamClient } from 'better-iam/client';
    import type { iam } from '../src/iam.js';

    export const client = createIamClient<typeof iam>({
      baseURL: window.location.origin, // the Express app's origin; calls go to /api/iam/{group}/{method}
    });
    ```

    A front end served from a different origin than the Express app also needs that origin in the instance's
    `trustedOrigins` option (for the IAM API) and in the adapter's `trustedOrigins` option (for your guarded routes).
  
## Protect routes [#protect-routes]

Guards are Express middleware. Each one reads the session, checks step-up when asked,
then checks authorization, and answers a refusal itself: a `401` or `403` JSON envelope for API calls, or a `303`
redirect to `loginPath` or `stepUpPath` for page navigations (a `GET` with `Accept: text/html`).

### Require a session [#require-a-session]

```ts title="src/routes/me.ts"
import { Router } from 'express';
import { iamExpress } from '../iam-express.js';

export const me = Router();

me.get('/me', iamExpress.requireSession(), async (req, res) => {
  const session = await req.iam.getSession();
  res.json({ email: session!.identity.email });
});
```

A signed-out API call gets `401` with `{ "error": { "code": "UNAUTHENTICATED", "message": "A session is required" } }`
and `Cache-Control: no-store`. A signed-out page navigation to `/me?tab=1` is redirected to
`/login?next=%2Fme%3Ftab%3D1`.

### Require a permission [#require-a-permission]

`authorize(action, { resource?, tenantId?, stepUp? })` requires a session allowed to perform `action`. The resource
defaults to the tenant itself (`iam/{tenantId}`) and the tenant to the session's tenant. `resource` and `tenantId`
are functions of `(req, session)`. Their `req` carries the route parameters (`req.params`). For typed parameters, pass
the request type, as in `authorize<Request<{ id: string }>>('projects:read', …)`.

```ts title="src/routes/projects.ts"
import { Router } from 'express';
import { iam } from '../iam.js';
import { iamExpress } from '../iam-express.js';
import { loadProject } from '../data.js'; // your data layer

export const projects = Router();

projects.get(
  '/projects/:id',
  iamExpress.authorize('projects:read', {
    resource: (req) => ({ type: 'project', id: String(req.params.id) }),
  }),
  async (req, res) => {
    res.json(await loadProject(String(req.params.id)));
  },
);

projects.get('/admin/people', iamExpress.authorize('iam:identities:read'), async (req, res) => {
  const session = await req.iam.requireSession();
  res.json(
    await iam.api.identities.list(req.iam.credential(), { tenantId: session.session.tenantId }),
  );
});
```

The check runs through `iam.require`, so it evaluates the same roles,
policies, conditions, and boundaries
as every other check. A denial answers `403 ACCESS_DENIED`.

### Require step-up [#require-step-up]

`stepUp` asks for more assurance than a session: `mfa: true` needs a session that completed MFA, `mfa: 'fresh'`
also refuses remembered devices, impersonation, assumed roles, and API keys, and `maxAgeMs` needs a recent
sign-in.

```ts title="src/routes/billing.ts"
import { Router } from 'express';
import { iamExpress } from '../iam-express.js';
import { cancelSubscription } from '../data.js'; // your data layer

export const billing = Router();

billing.post(
  '/billing/cancel',
  iamExpress.authorize('billing:manage', { stepUp: { mfa: true, maxAgeMs: 10 * 60 * 1000 } }),
  async (_req, res) => {
    await cancelSubscription();
    res.status(204).end();
  },
);
```

A session that fails the requirement gets `403` with `MFA_REQUIRED`, `RECENT_AUTH_REQUIRED`, or
`IMPERSONATION_RESTRICTED`. Page navigations go to `stepUpPath?next=...&reason=mfa|recent|impersonation`. See
[Step-up](/docs/authentication/mfa#step-up) for completing the challenge.

### Enforce inside a route [#enforce-inside-a-route]

When the resource is only known after loading data, enforce inside the handler with `req.iam.require`. It throws the
server's refusal, which `errorHandler` answers.

```ts title="src/routes/documents.ts"
import { Router } from 'express';
import { iamExpress } from '../iam-express.js';
import { deleteDocument, findDocument } from '../data.js'; // your data layer

export const documents = Router();

documents.delete('/documents/:id', iamExpress.requireSession(), async (req, res) => {
  const document = await findDocument(String(req.params.id));
  await req.iam.require('documents:delete', { type: 'document', id: document.id });
  await deleteDocument(document.id);
  res.status(204).end();
});
```

Express 5 passes a rejected promise from an async handler to the error middleware. On Express 4, wrap the body in
`try { ... } catch (error) { next(error); }` so the refusal reaches `errorHandler`.

### Cross-site requests [#cross-site-requests]

Guards also refuse a cookie-authenticated `POST`, `PUT`, `PATCH`, or `DELETE` whose `Origin` is not the app itself,
the IAM origin, or one of `trustedOrigins` (`403 UNTRUSTED_ORIGIN`, or `403 CSRF_REJECTED` with no `Origin`). A
`Sec-Fetch-Site: same-origin` request passes, and bearer-token requests skip the check. A state-changing route
without a guard should run the same rule with `checkRequestOrigin`:

```ts title="src/routes/newsletter.ts"
import { Router } from 'express';
import { checkRequestOrigin, nodeHeaders } from 'better-iam/middleware';
import { iam } from '../iam.js';
import { subscribe } from '../data.js'; // your data layer

export const newsletter = Router();

newsletter.post('/newsletter/subscribe', async (req, res, next) => {
  const refusal = checkRequestOrigin(
    {
      method: req.method,
      url: new URL(req.originalUrl, `${req.protocol}://${req.get('host')}`),
      headers: nodeHeaders(req.headers),
    },
    [iam.endpoint.origin],
  );
  if (refusal) return next(refusal); // errorHandler answers it
  await subscribe(String(req.body.email));
  res.status(204).end();
});
```

[The CSRF check](/docs/integrations/node#the-csrf-check) explains why it matters with `SameSite=Lax` cookies.

## Read the session [#read-the-session]

`req.iam` is created once per request. It memoizes the session lookup, and cookies the in-process client receives
count for later calls in the same request.

### Get the session [#get-the-session]

```ts title="src/routes/home.ts"
import { Router } from 'express';

export const home = Router();

home.get('/', async (req, res) => {
  const session = await req.iam.getSession(); // null when signed out
  if (!session) return res.redirect(303, '/login');
  res.json({
    name: session.identity.name,
    email: session.identity.email,
    tenantId: session.session.tenantId,
    mfa: session.session.mfa,
  });
});
```

`getSession()` resolves to `null` instead of throwing when the request has no usable session: no credential, or
the server answers `UNAUTHENTICATED`, `MFA_REQUIRED`, `EMAIL_UNVERIFIED`, `TENANT_INACTIVE`, or
`TENANT_UNAVAILABLE`. `requireSession({ stepUp? })` returns the session or throws an `IamRequestError` (`401`, or
`403` with a `reason`). The helper calls `iam.api.auth.getSession` in process; browsers reach the same method over
HTTP.

**Route:** `POST /api/iam/auth/getSession`

### Check permissions for rendering [#check-permissions-for-rendering]

`can` returns an advisory boolean. Checks made in the same tick share one
`authorizeMany` call, so a page with many buttons costs one round trip.

```ts title="src/routes/toolbar.ts"
import { Router } from 'express';
import { iamExpress } from '../iam-express.js';

export const toolbar = Router();

toolbar.get('/projects/:id/toolbar', iamExpress.requireSession(), async (req, res) => {
  const project = { type: 'project', id: String(req.params.id) };
  const [canEdit, canDelete, canInvite] = await Promise.all([
    req.iam.can('projects:update', project),
    req.iam.can('projects:delete', project),
    req.iam.can('iam:identities:create'),
  ]);
  res.json({ canEdit, canDelete, canInvite });
});
```

`req.iam.authorize(checks, { tenantId? })` returns `{ action, resource, allowed, reason }[]` for a list of checks.
Advisory checks decide what to show; enforce the operation itself with a guard or `req.iam.require`.

### Filter lists [#filter-lists]

`plan({ action, type, tenantId? })` returns the resources of a type the caller may act on as a filter for your own
query (`never` when signed out). `listAccessible({ action, type, tenantId?, limit?, offset? })` lists registered
resources of a managed type.

```ts title="src/routes/document-list.ts"
import { Router } from 'express';
import { filterToSql } from 'better-iam/core';
import { iamExpress } from '../iam-express.js';
import { columns, db } from '../data.js'; // your data layer: a field-to-column map and a SQL client

export const documentList = Router();

documentList.get('/documents', iamExpress.requireSession(), async (req, res) => {
  const plan = await req.iam.plan({ action: 'documents:read', type: 'document' });
  if (plan.kind === 'never') return res.json([]);
  const where = filterToSql(plan.filter, {
    dialect: 'postgres',
    column: (field) => columns[field],
    offset: 1,
  });
  const sql = `SELECT * FROM documents d WHERE d.tenant_id = $1 AND ${where.sql}`;
  res.json(await db.query(sql, [plan.tenantId, ...where.params]));
});
```

See [Data filtering](/docs/authorization/data-filtering) for the plan format and the other compilers.

### Sign in and out [#sign-in-and-out]

The in-process client (`req.iam.client`) is the full typed client with `iam.handler` as its transport. The cookies
the server issues land on this response, so a plain HTML form can sign someone in without client JavaScript.

  **Server:**

    ```ts title="src/routes/auth.ts"
    import { Router } from 'express';
    import { safeRedirectPath } from 'better-iam/middleware';
    import { iam } from '../iam.js';

    export const auth = Router();

    auth.post('/login', async (req, res) => {
      const { tenantId } = await iam.api.tenants.lookup({ slug: String(req.body.org) });
      const result = await req.iam.client.auth.signIn({
        tenantId,
        email: String(req.body.email),
        password: String(req.body.password),
      });
      if ('mfaRequired' in result) return res.redirect(303, `/verify?challenge=${result.challenge}`);
      res.redirect(303, safeRedirectPath(String(req.query.next ?? '')));
    });

    auth.post('/logout', async (req, res) => {
      await req.iam.signOut(); // ends the session and clears its cookie; never throws for a dead session
      res.redirect(303, '/');
    });
    ```
  
  **Client:**

    ```ts title="web/login.ts"
    import { client } from './iam-client.js';

    export async function signIn(org: string, email: string, password: string) {
      const { tenantId } = await client.tenants.lookup({ slug: org });
      const result = await client.auth.signIn({ tenantId, email, password });
      if ('mfaRequired' in result) return { challenge: result.challenge }; // complete the second factor next
      return { signedIn: true };
    }
    ```
  
**Route:** `POST /api/iam/auth/signIn`, `POST /api/iam/auth/signOut`, `POST /api/iam/tenants/lookup`

`safeRedirectPath` accepts only same-site paths and falls back to `/`, so `?next=` cannot become an open redirect. A
failed sign-in rejects with the server's error (for example `INVALID_CREDENTIALS` or `RATE_LIMITED`), which
`errorHandler` answers on Express 5; catch it to render the form again with the code instead. See
[MFA](/docs/authentication/mfa#completing-a-challenge) for completing a challenge.

### Call the server API directly [#call-the-server-api-directly]

`req.iam.credential()` returns `{ headers }` for direct `iam.api.*` calls, including cookies set earlier in the same
request.

```ts title="src/routes/sessions.ts"
import { Router } from 'express';
import { iam } from '../iam.js';
import { iamExpress } from '../iam-express.js';

export const sessions = Router();

sessions.get('/me/sessions', iamExpress.requireSession(), async (req, res) => {
  res.json(await iam.api.auth.listSessions(req.iam.credential()));
});
```

**Route:** `POST /api/iam/auth/listSessions`

### API keys and session tokens [#api-keys-and-session-tokens]

`requireSession` and the `authorize` guard build on `auth.getSession`, which serves signed-in people only, so API
keys and temporary credentials are refused there with `401`. Machine routes check them through the helpers, which
pass the request's `Authorization` header to the server: `req.iam.require(action, resource, { tenantId })` with an
explicit tenant, or `iam.api.*` with `req.iam.credential()`.

```ts title="src/routes/imports.ts"
import { Router } from 'express';
import { iam } from '../iam.js';
import { runImport } from '../data.js'; // your data layer

export const imports = Router();

imports.post('/hooks/import', async (req, res, next) => {
  try {
    const caller = await iam.api.sts.getCallerIdentity(req.iam.credential());
    await req.iam.require('imports:run', undefined, { tenantId: caller.tenantId });
    res.json(await runImport(req.body));
  } catch (error) {
    next(error);
  }
});
```

**Route:** `POST /api/iam/sts/getCallerIdentity`

Bearer requests skip the CSRF check. Services that only need to verify session JWTs offline use
`createSessionTokenVerifier` from `better-iam/session-tokens`; see
[Temporary access](/docs/authorization/temporary-access).

## Organizations [#organizations]

Every session acts in one tenant (an organization). Guards and helpers default to it: the
tenant is `session.session.tenantId`, and a check with no resource targets the tenant itself.

### Check in another tenant [#check-in-another-tenant]

Pass `tenantId` to check against a different tenant, for example a child organization the URL names. The server
decides whether the caller's credential may act there.

```ts title="src/routes/org-members.ts"
import { Router } from 'express';
import { iam } from '../iam.js';
import { iamExpress } from '../iam-express.js';

export const orgMembers = Router();

orgMembers.get(
  '/orgs/:tenantId/members',
  iamExpress.authorize('iam:identities:read', {
    tenantId: (req) => String(req.params.tenantId),
  }),
  async (req, res) => {
    const tenantId = String(req.params.tenantId);
    res.json(await iam.api.identities.list(req.iam.credential(), { tenantId }));
  },
);
```

Inside a route, the same option is `req.iam.require(action, resource, { tenantId })` and
`req.iam.can(action, resource, { tenantId })`.

### The organization in the URL [#the-organization-in-the-url]

When each organization lives under its own path (`/acme/projects`), resolve the alias with `tenants.lookup` and
send visitors whose session belongs to another organization to sign in there.

```ts title="src/organization.ts"
import type { NextFunction, Request, Response } from 'express';
import { safeRedirectPath, tenantOf, withQuery } from 'better-iam/middleware';
import { iam } from './iam.js';

export async function requireOrganization(req: Request, res: Response, next: NextFunction) {
  try {
    const slug = String(req.params.org);
    const organization = await iam.api.tenants.lookup({ slug }); // NOT_FOUND for unknown or inactive aliases
    const session = await req.iam.getSession();
    if (tenantOf(session) !== organization.tenantId) {
      const back = safeRedirectPath(req.originalUrl);
      return res.redirect(303, withQuery('/login', { org: slug, next: back }));
    }
    res.locals.organization = organization; // { tenantId, name, type, slug, ... }
    next();
  } catch (error) {
    next(error);
  }
}
```

```ts title="src/routes/org-projects.ts"
import { Router } from 'express';
import { requireOrganization } from '../organization.js';
import { listProjects } from '../data.js'; // your data layer

export const orgProjects = Router();

orgProjects.get('/:org/projects', requireOrganization, async (req, res) => {
  await req.iam.require('projects:read'); // the session's tenant is the organization's tenant here
  res.json(await listProjects(res.locals.organization.tenantId));
});
```

**Route:** `POST /api/iam/tenants/lookup`

A person with identities in several organizations can list them with `links.list` and switch with `links.switch`;
see [Tenants and identities](/docs/concepts/tenants-and-identities).

### Organization addresses [#organization-addresses]

With [organization sign-in addresses](/docs/operations/deployment/hosts-and-regions) (`acme.signin.example.com`),
calls through `req.iam.client` keep the host the visitor is on, so the server pins them to that organization exactly
as it would a browser call: sign-in on Acme's address needs no `tenantId`, and credentials of other organizations
are refused with `HOST_MISMATCH`. `iam.hosts.resolve(host)` tells your own middleware which organization an address
belongs to.

## Options [#options]

`createIamExpress(iam, options)` takes the instance, or a function returning it (or a promise of it), and these
options:

| Option           | Type           | Default                                             | Description                                                                                                                                          |
| ---------------- | -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `loginPath`      | `string`       | none                                                | Where signed-out page navigations (a `GET` with `Accept: text/html`) are redirected, with `?next=`. Without it they get the JSON `401`               |
| `stepUpPath`     | `string`       | none                                                | Where page navigations go when the session must step up, with `?next=` and `?reason=`. Without it they get the JSON `403`                            |
| `serve`          | `string[]`     | `[basePath]`                                        | Path prefixes the IAM server answers. Add protocol mounts outside the base path, such as `/oauth` or `/.well-known`                                  |
| `basePath`       | `string`       | the instance's `endpoint.basePath`, else `/api/iam` | Where the IAM API is served; the default `serve` prefix and the in-process client's target                                                           |
| `trustedOrigins` | `string[]`     | `[]`                                                | Origins besides the request's own and the IAM origin that may send cookie-authenticated unsafe requests to guarded routes                            |
| `csrf`           | `boolean`      | `true`                                              | Check the `Origin` of cookie-authenticated `POST`, `PUT`, `PATCH`, and `DELETE` requests in guards. Turn off only when something else checks origins |
| `now`            | `() => number` | `Date.now`                                          | The clock for step-up recency checks, for tests                                                                                                      |

The returned object:

| Member                                                 | What it does                                                                                                           |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `middleware`                                           | Serves the IAM API under the `serve` prefixes and attaches `req.iam` to every other request. Mount before body parsers |
| `requireSession({ stepUp? })`                          | Middleware requiring a session, and a step-up when given                                                               |
| `authorize(action, { resource?, tenantId?, stepUp? })` | Middleware requiring a session allowed to perform `action`                                                             |
| `errorHandler`                                         | Error middleware: IAM refusals become the JSON envelope or a redirect; other errors go to `next(error)`                |
| `helpers(req, res)`                                    | The per-request helpers, created on first use (the same object as `req.iam`)                                           |
| `resolve()`                                            | Resolves the instance                                                                                                  |

The members of `req.iam` are listed under [Per-request helpers](/docs/integrations/node#per-request-helpers).

## Errors [#errors]

| Code                                                                          | Status | When                                                                                           |
| ----------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------- |
| [`UNAUTHENTICATED`](/docs/reference/errors#unauthenticated)                   | 401    | A guard or `requireSession` ran with no usable session                                         |
| [`MFA_REQUIRED`](/docs/reference/errors#mfa_required)                         | 403    | `stepUp.mfa` was not met; `reason` is `mfa`                                                    |
| [`RECENT_AUTH_REQUIRED`](/docs/reference/errors#recent_auth_required)         | 403    | `stepUp.maxAgeMs` was not met; `reason` is `recent`                                            |
| [`IMPERSONATION_RESTRICTED`](/docs/reference/errors#impersonation_restricted) | 403    | An impersonated session met a `maxAgeMs` or `'fresh'` requirement; `reason` is `impersonation` |
| [`ACCESS_DENIED`](/docs/reference/errors#access_denied)                       | 403    | `authorize` or `req.iam.require` was denied                                                    |
| [`UNTRUSTED_ORIGIN`](/docs/reference/errors#untrusted_origin)                 | 403    | A cookie-authenticated unsafe request came from an untrusted origin                            |
| [`CSRF_REJECTED`](/docs/reference/errors#csrf_rejected)                       | 403    | A cookie-authenticated unsafe request had no `Origin`                                          |
| [`INVALID_INPUT`](/docs/reference/errors#invalid_input)                       | 400    | An `authorize` guard had no tenant to check in                                                 |

`errorHandler` answers any error that carries an IAM `code` and HTTP `status` (`IamRequestError`, the server's
`IamError`, `QUOTA_EXCEEDED`, `NOT_FOUND` from `tenants.lookup`) with that status and
`{ "error": { "code", "message" } }`. A body the adapter has to re-serialize above 2 MiB answers `413` with plain
text.

## Related [#related]

  - [Hono](/docs/integrations/hono): The same helpers and guards for Hono on Node, Bun, Deno, and Workers.

  - [Fastify](/docs/integrations/fastify): A plugin with `preHandler` guards and `request.iam`.

  - [Node and custom servers](/docs/integrations/node): The per-request helpers, the CSRF rule, and the core for other frameworks.

  - [Typed client](/docs/concepts/client): Call the API you mounted from the browser.

  - [Policies](/docs/authorization/policies): What `authorize` guards evaluate.

  - [Hosts and regions](/docs/operations/deployment/hosts-and-regions): Organization sign-in addresses and pinning.
