# Fastify

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

permdock/fastify registers the Fetch kernel as a Fastify plugin that decorates request.permdock and provides protect as a typed preHandler hook.

## Purpose [#purpose]

Fastify's plugin system, request decorators and typed route generics map cleanly onto the [server kernel](/docs/adapters/server-kernel). `permdock/fastify` registers a plugin that resolves the subject in an `onRequest` hook and decorates `request.permdock`; `protect` is a `preHandler` that loads data, decides and replies with Problem Details on denial. Fastify already fails closed after `reply.send()` in a hook, so denial never falls through to the handler.

## API [#api]

```ts
import Fastify from "fastify";
import { createPermDock } from "permdock/fastify";
import { policy } from "./policy";
import { permissions } from "./permissions";

export const { permdock, protect } = createPermDock(policy, {
  subject: (request) => request.user ?? null,
});

const app = Fastify();
await app.register(permdock); // request.permdock is the request-scoped PermDock

app.delete<{ Params: { id: string } }>(
  "/posts/:id",
  {
    preHandler: protect(permissions.post.delete, (request) =>
      loadPost(request.params.id),
    ),
    schema: { security: openapi.security(permissions.post.delete).security }, // @fastify/swagger
  },
  async (request, reply) => {
    await deletePost(request.permdockData);
    return reply.code(204).send();
  },
);
```

| Export | Role |
| --- | --- |
| `permdock` | Plugin (wrapped with `fastify-plugin` so decorators are visible to the parent scope). Adds `request.permdock` and registers an error handler mapping PermDock errors to Problem Details. Any other error goes to the error handler that was in place before, so an app's own `setErrorHandler` keeps working. |
| `protect(permission, loadData?, options?)` | `preHandler` hook. The `tenant` option (`(request) => request.params.org`) is resolved for each `protect`. Generic over the route, schema and type provider (`protect<Route, Schema, Provider>`): route generics infer from the `preHandler` slot, so `request.params` and `request.body` keep their types inside `loadData` (permix's Elysia and Fastify guards lost route typing). With a zod or TypeBox type provider, pass the provider explicitly or read the body in the handler; see [Why](#why). |
| `permdockHandler` | Plugin mounting the AuthZEN-shaped decision endpoint: `app.register(permdockHandler, { prefix: '/api/permdock' })`. |
| `withPermDock(handler)` | Wraps a route handler registered after `permdock` or `protect` and types `request.permdock` with the policy's roles, plans and permissions. Generic over the route, like `protect`. The adapter does not augment `FastifyRequest`, for the reason the [Express adapter](/docs/adapters/express) gives. |
| `openapi` | Kernel hook contract; feeds `@fastify/swagger` route `schema.security` and the document-level `securitySchemes`. |

### Encapsulation [#encapsulation]

Fastify plugins are encapsulated by default. `permdock` is wrapped with `fastify-plugin` so the decorator and error handler are visible to the registering scope and its children; registering it once on the root instance covers every route. Apps that want different policies per prefix register the plugin from a second `createPermDock` result inside a child scope, and `request.permdock` is typed per scope through Fastify's declaration merging on the plugin's generic rather than a global `FastifyRequest` augmentation.

```ts
await app.register(
  async (admin) => {
    await admin.register(adminPermdock); // a second factory with a stricter policy
    admin.get(
      "/audit",
      { preHandler: adminProtect(permissions.audit.read) },
      listAudit,
    );
  },
  { prefix: "/admin" },
);
```

## Request lifecycle [#request-lifecycle]

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

* The plugin's `onRequest` hook resolves the subject once; `protect` runs as a `preHandler` and stores the loaded row on `request.permdockData`, so the handler does not run the loader twice.
* The plugin registers the error handler itself and chains to the one that was in place before, so thrown PermDock errors become Problem Details and an app's own `setErrorHandler` keeps handling everything else.
* Fastify's own JSON Schema validation runs first; PermDock validates only the resource shape the policy uses.
* The decision endpoint builds its Web `Request` from the body Fastify already parsed. Multipart bodies stay with `@fastify/multipart`: `request.file()` works in a handler behind `protect`.
* Using `protect` on a route in a scope where the plugin is not registered throws at startup.
* Decision events carry `request.method` and `request.routeOptions.url`.

Denials are the kernel's Problem Details responses.

## Why [#why]

`protect` takes the route, schema and type provider as generics instead of fixing them to Fastify's defaults, so the handler it returns fits any typed `preHandler` slot. TypeScript infers route generics (`app.get<{ Params }>`) into `loadData` from that slot. A body derived from a type provider's schema does not flow into an inline `preHandler: protect(...)`: the route shorthand infers its own schema generic from the same options object and accepts one handler or an array, and TypeScript does not carry the provider back through both into a nested generic call. The handler still sees the typed body. Rather than a Fastify-specific wrapper, `protect<RouteGenericInterface, FastifySchema, ZodTypeProvider>(...)` names the provider explicitly, and a loader that only needs `request.params.id` needs nothing at all. Both patterns are type-tested.

## Example app [#example-app]

`apps/examples/fastify`: posts API with the plugin, `protect` on CRUD routes, `@fastify/swagger` emitting `securitySchemes` and per-route `security`, the decision endpoint, and `app.inject()` tests asserting exact denial bodies.

`tests/integration/src/http/fastify.test.ts` runs the [`testHttpAdapter`](/docs/adapters/testing) scenarios with `@fastify/multipart` and a prefixed admin plugin.

## Related standards [#related-standards]

* [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi).
