# Hono

Source: https://permdock.com/docs/adapters/hono

permdock/hono wraps the Fetch kernel as a Hono middleware and a protect route guard, with OpenAPI 3.2 security emitted through hono-openapi or @hono/zod-openapi.

## Purpose [#purpose]

Hono is the reference HTTP adapter because it already speaks Fetch, runs on Node, Bun, Deno and Workers, and is what the example backends for the React, Vue, Solid and Expo examples use. `permdock/hono` is a thin wrapper over the [server kernel](/docs/adapters/server-kernel): a middleware that puts the request-scoped `PermDock` on the context, a `protect` guard for routes, and hooks that emit OpenAPI 3.2 `security` for the two common OpenAPI integrations.

## API [#api]

```ts
import { Hono } from "hono";
import {
  createPermDock,
  discoverViaSignatureAgent,
  verifyWebBotAuth,
} from "permdock/hono";
import { policy } from "./policy";
import { permissions } from "./permissions";

export const { permdock, protect } = createPermDock(policy, {
  subject: (c) => c.get("user"),
  webBotAuth: (request) =>
    verifyWebBotAuth(request, {
      keys: discoverViaSignatureAgent({ allow: ["agents.example.com"] }),
    }),
});

const app = new Hono<Env>();
app.use(permdock()); // c.get('permdock') is the request-scoped PermDock
app.delete(
  "/posts/:id",
  protect(permissions.post.delete, (c) => loadPost(c.req.param("id"))),
  handler,
);
// deny → 403 application/problem+json { type, title, permission, denials, alternatives }; approval-required → 403 with type .../approval-required
// OpenAPI 3.2 `security`, `securitySchemes` (incl. oauth2MetadataUrl, deviceAuthorization, deprecated) emitted via the framework hook; registered x-oai-* fallbacks (plus x-permdock-oauth2MetadataUrl) for 3.1
// Optional Web Bot Auth verification (RFC 9421) fills `actor` for agent callers
```

| Export | Role |
| --- | --- |
| `permdock()` | Middleware. Resolves the subject with the `subject` option (receives the Hono context `c`), builds the instance through the kernel and sets it as `c.set('permdock', instance)`. It returns `MiddlewareHandler<PermDockEnv>`, and Hono merges that `Variables` type into the route's Env, so `c.get('permdock')` is typed without global module augmentation and without a factory generic. |
| `protect(permission, loadData?, options?)` | Route middleware. The `tenant` option (`(c) => c.req.param('org')`) is resolved for each `protect`, where route params exist. For instance actions `loadData(c)` runs first; the result is available to the handler as `c.get('permdockData')` and validated at the boundary if the loader is marked untrusted. Denials short-circuit with a Problem Details response. |
| `permdockHandler()` | Mounts the AuthZEN-shaped decision endpoint (`POST /`) for `permdock/react` clients on any sub-app: `app.route('/api/permdock', permdockHandler())`. |
| `openapi` | Kernel hook contract, used by the two integrations below. |

### OpenAPI hooks [#openapi-hooks]

With `hono-openapi`:

```ts
import { describeRoute } from "hono-openapi";
app.delete(
  "/posts/:id",
  describeRoute({
    ...openapi.security(permissions.post.delete),
    description: "Delete a post",
  }),
  protect(permissions.post.delete, (c) => loadPost(c.req.param("id"))),
  handler,
);
```

With `@hono/zod-openapi`:

```ts
const route = createRoute({
  method: "delete",
  path: "/posts/{id}",
  security: openapi.security(permissions.post.delete).security,
  responses: { 403: openapi.problemResponse() },
});
```

`openapi.securitySchemes()` returns the `oauth2` scheme with every `scope` from `listPermissions(permissions)`, plus `oauth2MetadataUrl` and `deviceAuthorization` when configured. Emitted documents target OpenAPI 3.2; for a 3.1 document `deviceAuthorization` and `deprecated` are written under the registered `x-oai-deviceAuthorization` / `x-oai-deviceAuthorizationUrl` / `x-oai-deprecated` extensions and `oauth2MetadataUrl` under `x-permdock-oauth2MetadataUrl` (see [OpenAPI registries](/docs/standards/openapi-registry)). `permdock openapi` in the CLI reads the same hook output to check that every protected route is documented.

### Streams and sockets [#streams-and-sockets]

`connection(c, options?)` opens a kernel [`Connection`](/docs/adapters/server-kernel#connections) for the request behind `c`; pass `revocations` to `createPermDock` so revoked sessions and membership changes reach it. `sse(conn, stream, source, options?)` writes an async iterable to a `streamSSE` stream: items the subscriber cannot read under `options.items` are dropped (`unwrap` picks the row inside an envelope, `format` builds the frame), and when the connection aborts it writes `event: permdock` with the Problem Details body and resolves. Await it last in the callback, since `streamSSE` closes the stream when the callback returns. `socket(conn, events)` wraps `upgradeWebSocket` events so an abort closes the socket with `1008` and the Problem Details `type` as reason, and a closed socket closes the connection.

```ts
const { protect, connection, sse, socket } = createPermDock(policy, {
  subject,
  revocations,
});

app.get(
  "/projects/:id/events",
  protect(permissions.project.read, loadProject),
  async (c) => {
    const conn = await connection(c, {
      permission: permissions.project.read,
      data: c.get("permdockData"),
    });
    return streamSSE(c, async (stream) => {
      await sse(conn, stream, projectEvents(c.req.param("id"), conn.signal), {
        items: permissions.project.read,
      });
    });
  },
);

app.get(
  "/ws",
  upgradeWebSocket(async (c) => {
    const conn = await connection(c);
    return socket(conn, {
      onMessage(event, ws) {
        const decision = conn.check(
          permissions.post.update,
          JSON.parse(String(event.data)),
        );
        if (decision.outcome !== "granted")
          ws.send(JSON.stringify({ denied: decision.denials }));
      },
    });
  }),
);
```

A denied message leaves the socket open; only a revocation, an expiry or a re-denied opening permission closes it.

### With better-supabase [#with-better-supabase]

better-supabase's `createHono` verifies the session and puts it on `c.var.auth`; `bs.middleware()` adds `c.var.db`. Map the session with [`subjectFromBetterSupabase`](/docs/adapters/better-supabase) and pair both middlewares in one helper used on every route, instead of `app.use(bs.middleware())`, so a route that skips the helper has no `c.var.db` and `bs.handler` answers 500 instead of running unguarded:

```ts title="src/server.ts"
import { createHono } from "better-supabase/hono";
import type { Permission } from "permdock";
import { createPermDock } from "permdock/hono";
import { subjectFromBetterSupabase } from "permdock/better-supabase";

const bs = createHono(betterSupabase);
const { protect } = createPermDock(policy, {
  subject: (c) =>
    subjectFromBetterSupabase(c.get("auth"), { anonymousSignIns: "deny" }),
});

const guarded = (permission: Permission) =>
  [bs.middleware(), protect(permission)] as const;

export const app = bs.app().get(
  "/customers",
  ...guarded(permissions.customers.read),
  bs.handler((_c, { db }) => db.customers.findMany({ select: ["id"] })),
);
```

`protect` sets `c.get('permdock')` itself, so the route needs no `permdock()` middleware. A support session's token is the user's, with the admin as the actor; it reaches only what a policy delegation names ([Supabase](/docs/adapters/supabase#support-and-impersonation-actors)).

## Request lifecycle [#request-lifecycle]

`permdock/hono` follows the [shared adapter contract](/docs/adapters) through the [server kernel](/docs/adapters/server-kernel). What differs on Hono:

* `subject(c)` typically reads a user set by an auth middleware earlier in the chain.
* `protect` exposes the loaded row as `c.get('permdockData')`, so the handler does not run the loader again. A loader that returns `null` or `undefined` answers `404`.
* After `await next()`, `permdock()` and `protect()` read the error Hono recorded for the downstream handler (for example from `c.get('permdock').assert(...)`) and replace the response with the same Problem Details body. No `app.onError` is needed, and an app's own `onError` keeps handling every other error.
* `protect` builds the instance itself, so it works with or without `permdock()` earlier in the chain; there is no ordering to get wrong.
* The [decision event](/docs/concepts/wire-formats) carries the permission, resource, subject, tenant and outcome, not the HTTP method or route.
* `context(c)`, `onDenied` and `wrap` take the Hono context and behave as on the [server kernel](/docs/adapters/server-kernel#options): `context: (c) => ({ region: c.get('geo').region })` adds server-derived request values, and `onDenied` replaces the refusal body.

Denials are the kernel's Problem Details responses, or what `onDenied` returned.

## Why [#why]

Hono types context variables through the `Env` generic of each middleware and merges them along the chain, so the adapter only has to say what it sets. `permdock()` returns `MiddlewareHandler<PermDockEnv>` and `protect(permission, loadData)` returns `MiddlewareHandler<PermDockEnv<Row>>`, where `Row` is the loader's non-null return type. That works with `new Hono()`, with an app `Env` that declares its own variables, and inside `createMiddleware<PermDockEnv>` for an app middleware that reads `c.get('permdock')`, with no `createFactory` generic and no `declare module 'hono'` augmentation (which would leak one policy's types into every app in a monorepo). All three cases are type-tested.

## Example app [#example-app]

`apps/examples/hono`: `src/permissions.ts`, `src/policy.ts`, `createPermDock` from `permdock/hono`, `protect` on `PATCH /posts/:id` (granted) and `POST /posts/:id/publish` (denied for `member`), plus `src/serve.ts`, which serves `app.fetch` with `@hono/node-server`, for `pnpm start`. `tests/integration/src/http/hono.test.ts` runs the [`testHttpAdapter`](/docs/adapters/testing) scenarios on `@hono/node-server` with a global `permdock()`, a tenant from the `:org` param and a sub-app. The factory enables `otel` with a structural logger so decision lines are recorded locally.

## Related standards [#related-standards]

* [Problem Details](/docs/standards/problem-details).
* [OpenAPI 3.2](/docs/standards/openapi).
* [Web Bot Auth](/docs/standards/web-bot-auth).
* [AuthZEN](/docs/standards/authzen) for the decision endpoint.
