# Overview





Every web adapter is a thin **gate** over [`@postel/http`](/docs/reference/http): it reads the raw request bytes, runs the verifier(s) you configured on a source, maps any `PostelError` to the right HTTP status, optionally dedup-acks, and only then hands control to your handler. You keep writing a normal handler — you don't re-implement verify → error-mapping → ack in every route.

The error→status policy, byte preservation, and the dedup-ack signal live **once** in `@postel/http`; each adapter binds them to its framework's idiom (middleware, preHandler, guard), so they can't drift. A non-`PostelError` (e.g. a programming bug) bubbles as 5xx — never miscast as a 4xx.

## The shape every adapter shares [#the-shape-every-adapter-shares]

`XxxWebAdapter(postel, app)` binds to your app and exposes bindings grouped by intent — present only for the config slots you set:

* **`hwa.inbound.<source>.post(route, handler, opts?)`** — register a gated route. Raw bytes are verified before your handler runs, with the typed result on the request context (`c.var.postel` / `req.postel`). The source key is type-checked against your config. For a non-`POST` delivery (some providers use `PUT`/`PATCH&#x60;), use &#x2A;*`.on(method, route, handler, opts?)`** — `.post` is sugar for `.on("POST", …)`. Bodyless verbs (`GET`/`HEAD`) aren't offered: the gate verifies a signature over the body.
* **`hwa.outbound.bindJwks(route?, provider?)`** — publish your public keys (defaults to `/.well-known/webhooks-keys` + `postel.outbound.keys.publicJwks()`).
* **`hwa.admin.bindAdminRoutes(prefix, opts)`** — mount the [`@postel/admin`](/docs/operations/admin) control plane.

Prefer to own the routing yourself? The facade is sugar over the **low-level primitives** every package still exports: `verifyWebhook(source, opts?)` (the framework-native gate) and `withWebhook(source, handler, opts?)` (gate + handler folded into one). NestJS is the exception — it stays DI-idiomatic with a `WebhookGuard` rather than a routing facade.

**Next.js is the other exception.** App Router routes files, not an app object, so `NextjsWebAdapter(postel)` takes no `app` and its bindings return Route Handlers keyed by HTTP method instead of registering a path: `nwa.inbound.<source>.post(handler, opts?)` returns `{ POST }`, `nwa.outbound.bindJwks(provider?)` returns `{ GET }`, `nwa.admin.bindAdminRoutes(opts)` returns `{ GET, POST, PUT, PATCH, DELETE }` — each spread directly as the route file's exports. It keeps the low-level `withWebhook(source, handler, opts?)` primitive; there's no `verifyWebhook`/`getVerified` pair since a Fetch `Request` isn't mutated with a stashed result the way a framework's request/context object is. See [Next.js](/docs/web-adapters/nextjs).

## Adding your own middleware [#adding-your-own-middleware]

The facade owns the route, but you keep your framework's native middleware story — no special API needed:

* **Express / Hono** — layer middleware on the path with `app.use("/webhooks/vendor", …)`, registered **before** the facade `.post` (middleware runs in registration order). The gate parses the body, so path middleware sees the request pre-verification — ideal for auth, rate-limit, or logging.
* **Fastify** — pass route options to `.post`: `onRequest` runs before the gate, `preHandler` after it (with the result on `req.postel`). See [Fastify](/docs/web-adapters/fastify).

Want the gate as a standalone middleware to wire entirely by hand? `verifyWebhook(source)` / `withWebhook(source, handler)` are exported from every package — read the verified result on that primitive path with `getVerified(reqOrCtx)` (it throws if the gate didn't run). Facade routes are already typed (`c.var.postel` / `req.postel`, narrowed to the source's [schema](/docs/inbound/verify#validating-the-payload) output), so they don't need it.

## OpenAPI / route docs [#openapi--route-docs]

The facade binds **verification**, not documentation — its routes don't auto-populate an OpenAPI/Swagger spec, and the source `schema` (a [Standard Schema](/docs/inbound/verify#validating-the-payload) for verify + types) is a **different slot** from your framework's route schema. How it interacts:

* **Fastify** — `@fastify/swagger` discovers the route (it's in the route table) but reads Fastify's route `schema`, not Postel's. Pass `schema` through the `.post` / `.on` route options and it flows to the spec (reuse the same zod object with `fastify-type-provider-zod`).
* **NestJS** — unaffected: `@nestjs/swagger` reads your controllers + DTOs; the `WebhookGuard` doesn't touch it.
* **Hono** — `@hono/zod-openapi` only documents routes declared through its own `createRoute(...)` API, so facade-bound routes are invisible to it. To put a webhook route in the spec, define it with `@hono/zod-openapi` and apply the gate as middleware (`verifyWebhook(postel.inbound.<source>)`).
* **Express** — OpenAPI is manual (`swagger-jsdoc` / `tsoa`) regardless of the facade.

We deliberately don't bridge the two slots: auto-converting Standard Schema → JSON Schema would couple `@postel/core` to a specific library and break its zero-dependency guarantee.

## Pick your target [#pick-your-target]

<Cards>
  <Card icon="<PlugIcon />" title="Hono" href="/docs/web-adapters/hono" description="HonoWebAdapter(postel, app).inbound.<source>.post(...)." />

  <Card icon="<PlugIcon />" title="Express" href="/docs/web-adapters/express" description="ExpressWebAdapter — express.raw() + gate; sets req.postel." />

  <Card icon="<PlugIcon />" title="Fastify" href="/docs/web-adapters/fastify" description="fastifyPostel raw-body plugin + FastifyWebAdapter." />

  <Card icon="<PlugIcon />" title="NestJS" href="/docs/web-adapters/nestjs" description="PostelModule + WebhookGuard + @Event()." />

  <Card icon="<PlugIcon />" title="Next.js" href="/docs/web-adapters/nextjs" description="NextjsWebAdapter — typed Route Handler bindings over @postel/http." />

  <Card icon="<CodeIcon />" title="Bun" href="/docs/web-adapters/bun" description="Bun.serve is Fetch-native — use fetchWebhook directly." />
</Cards>

## Status [#status]

| Framework / runtime                 | Package                                | Ships? | Form                                              |
| ----------------------------------- | -------------------------------------- | ------ | ------------------------------------------------- |
| Hono                                | `@postel/hono`                         | ✅      | `HonoWebAdapter` routing facade + primitives      |
| Express                             | `@postel/express`                      | ✅      | `ExpressWebAdapter` facade + `express.raw()` gate |
| Fastify                             | `@postel/fastify`                      | ✅      | `FastifyWebAdapter` facade + raw-body plugin      |
| NestJS                              | `@postel/nestjs`                       | ✅      | Module + `WebhookGuard` + decorators              |
| Next.js Route Handlers              | `@postel/nextjs`                       | ✅      | `NextjsWebAdapter` route-handler bindings         |
| Any Fetch runtime (Deno, custom, …) | [`@postel/http`](/docs/reference/http) | ✅      | `fetchWebhook(source)`                            |
| Bun.serve                           | `@postel/bun`                          | ⏳ stub | Use `@postel/http` meanwhile                      |

## Both directions [#both-directions]

Adapters also mount the **outbound** HTTP surface: `hwa.outbound.bindJwks()` publishes your current public keys (see [key rotation](/docs/inbound/key-rotation)), and `hwa.admin.bindAdminRoutes()` mounts the [`@postel/admin`](/docs/operations/admin) control plane (natively on Hono; via `fetchToExpress` / `fetchToFastify` on Node).
