# Fastify



```ts title="lib/postel.ts"
import { Postel, Secret } from "@postel/core";
import { config } from "./config.js";

export const postel = Postel({
  inbound: {
    vendor: {
      verify: Secret(config.webhookSecret),
    },
  },
});
```

## Routing facade [#routing-facade]

`FastifyWebAdapter(postel, app)` registers gated routes by source key. Verification needs the exact received bytes, so the adapter registers everything it binds inside its **own encapsulated scope** with a raw-Buffer body parser — your app's JSON parsing stays intact, and there's no plugin to wire up.

```ts title="app.ts"
import Fastify from "fastify";
import { FastifyWebAdapter } from "@postel/fastify";
import { postel } from "./lib/postel";

const app = Fastify();
const fwa = FastifyWebAdapter(postel, app);

fwa.inbound.vendor.post("/webhooks/vendor", async (req) => ({
  ok: true,
  type: req.postel.event.type,
}));
```

Source keys are type-checked. On failure the gate replies with the mapped status; a non-`PostelError` bubbles to Fastify's error handler (5xx). The adapter's routes register when its scope loads (at `app.ready()` / `listen()`), so bind them before you call `listen`.

### Other methods [#other-methods]

`.post` is sugar for `.on("POST", …)`. Bind another body-bearing verb (`PUT`/`PATCH`) explicitly; `.on` accepts the same route-options overload (below):

```ts
fwa.inbound.vendor.on("PUT", "/webhooks/vendor", async () => ({ ok: true }));
```

## Route options & middleware [#route-options--middleware]

`.post` mirrors Fastify's own `fastify.post`: pass a route-options object and the gate is injected as the first `preHandler` for you. Anything Fastify routes accept works — `onRequest`, `preHandler`, `schema`, `bodyLimit`, `config`, … — so you attach whatever you'd attach to a normal route.

```ts
fwa.inbound.vendor.post(
  "/webhooks/vendor",
  {
    onRequest: rateLimit,              // runs before the gate — pre-verify (body not parsed yet)
    preHandler: attachTenant,          // runs after the gate — req.postel is set
    bodyLimit: 1_048_576,              // any route option is forwarded to Fastify untouched
    webhook: { dedup: { ttl: "1h" } }, // Postel gate options (see note)
  },
  async (req) => ({ ok: true, type: req.postel.event.type }),
);
```

Because the gate runs at `preHandler&#x60;, Fastify's lifecycle gives you the split for free: **`onRequest` = before verification*&#x2A;, **`preHandler` = after verification** (the verified result is already on `req.postel`).

> **Why gate options live under `webhook`.** Postel's gate options (`dedup`, `successStatus`) ride under a single `webhook` key *inside* the route-options object rather than as a separate argument — so what you pass stays a plain Fastify `RouteShorthandOptions` that reads like any other `fastify.post(...)` call. The adapter reads `webhook`, strips it, and forwards everything else to Fastify untouched. Not passing route options? Give them as the third argument instead: `fwa.inbound.vendor.post(route, handler, { dedup })`.

## JWKS [#jwks]

```ts
fwa.outbound.bindJwks();                         // GET /.well-known/webhooks-keys
fwa.outbound.bindJwks("/keys", customProvider);  // custom route + provider
```

`bindJwks()` defaults the provider to `postel.outbound.keys.publicJwks()`; available when an `outbound` slot is configured.

## Admin [#admin]

```ts
fwa.admin.bindAdminRoutes("/admin", { authorize: (req) => checkAdminToken(req) });
```

## Low-level primitives [#low-level-primitives]

The facade wraps the `withWebhook` / `verifyWebhook` preHandlers and the `fetchToFastify` bridge. On this manual path you install the raw-body parser yourself with `fastifyPostel`, on the instance (or encapsulated scope) serving your webhook routes — the facade does this in its own scope; here you're wiring it by hand:

```ts
import { fastifyPostel, verifyWebhook, fetchToFastify } from "@postel/fastify";
import { adminRouter } from "@postel/admin";

await app.register(fastifyPostel); // raw-Buffer parser for the verifyWebhook routes in this scope

app.post("/webhooks/vendor", { preHandler: verifyWebhook(postel.inbound.vendor) }, async () => "ok");
```

Wiring `verifyWebhook` with the `dedup` option by hand? Also attach the exported `releaseDedupOnError` as the route's `onError` hook so a throwing handler [releases the dedup record](/docs/inbound/deduplication#delivery-semantics-of-gate-level-dedup) — facade routes install it automatically:

```ts
app.post(
  "/webhooks/vendor",
  {
    preHandler: verifyWebhook(postel.inbound.vendor, { dedup: { ttl: "1h" } }),
    onError: releaseDedupOnError,
  },
  async () => "ok",
);
app.all("/admin/*", fetchToFastify(adminRouter(postel, { authorize: (req) => checkAdminToken(req) })));
```
