# Overview





The outbound half of Postel delivers Standard Webhooks to your customers' HTTP endpoints. It's the half you reach for when *your* product emits webhooks — order.created, deployment.finished, message.posted — and someone else's app is the receiver. (Verifying webhooks other services send *you* is the [inbound half](/docs/inbound).)

## The mental model [#the-mental-model]

`send()` is an outbox INSERT under the hood: it commits or rolls back atomically with your business write, and a worker pool picks the row up and does the HTTP delivery — you never block on the network inside a transaction. The full picture, with diagrams, is on [How Postel works](/docs/concepts/how-postel-works).

```ts
import { Postel, InMemoryStorage, HmacV1, ExponentialBackoff, InProcess } from "@postel/core";

const postel = Postel({
  outbound: {
    storage: InMemoryStorage(),    // or a DB-backed Storage adapter in production
    signing: HmacV1(),             // or Ed25519V1a() for asymmetric signing
    retryPolicy: ExponentialBackoff({ maxAttempts: 8 }),
    workers: InProcess({ concurrency: 4 }),
  },
});

await postel.start(); // starts the in-process worker pool that delivers queued webhooks

// Inside a transaction:
await db.tx(async (tx) => {
  await db.orders.insert({ id: "ord_123", /* ... */ }, { tx });
  await postel.outbound.send(
    { type: "order.created", data: { id: "ord_123" } },
    { tx },                                  // joins the same transaction
  );
});
```

The worker only runs between `postel.start()` and `postel.stop()`; skip `start()` (and never call `drain()` either) and messages queue forever with no delivery and no error. Hosts that can't keep a pool alive — Lambda, Vercel Functions, Cloudflare Workers — call [`drain()`](/docs/operations/serverless) from a cron-triggered handler instead.

## Start here [#start-here]

<Cards>
  <Card icon="<BoxIcon />" title="Sending & the outbox" href="/docs/outbound/send" description="The transactional contract — an outbox INSERT that joins your business write." />

  <Card icon="<RepeatIcon />" title="Retries & backoff" href="/docs/outbound/retries" description="The retry schedule, circuit breaker, auto-disable." />

  <Card icon="<HistoryIcon />" title="Replay" href="/docs/outbound/replay" description="First-class replay verbs — by message, endpoint, or filter." />

  <Card icon="<LinkIcon />" title="Endpoints" href="/docs/outbound/endpoints" description="Manage the receiver side of your fanout." />

  <Card icon="<RocketIcon />" title="Serverless" href="/docs/operations/serverless" description="Bounded drain() calls for Lambda, Vercel, and Cloudflare — no long-lived process." />
</Cards>

## What's available [#whats-available]

✅ = available now · ⏳ = config slot present, adapter/runtime planned. The outbound runtime is exercised end-to-end by the [`@postel/compliance`](https://github.com/postel-sh/postel/tree/main/compliance) sender suite.

| Feature                                                 | Status                  | Notes                                                                                                                                                                                                        |
| ------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Transactional outbox**                                | ✅                       | `send()` writes an outbox row inside *your* business transaction — no orphaned events, no "we charged the card but never told the customer."                                                                 |
| **Retries, circuit breaker, dead-letter, auto-disable** | ✅                       | [Scheduled backoff](/docs/outbound/retries), configurable per endpoint, with breakers and auto-disable on persistent failure.                                                                                |
| **Replay**                                              | ✅                       | `replay({ messageId })`, `replay({ endpointId, since })`, or `replay({ filter })`. No queue surgery, no manual SQL.                                                                                          |
| **Fanout**                                              | ✅                       | One `send()` → N endpoints, filtered by event type globs, channels, structural filters, or a predicate.                                                                                                      |
| **Endpoint lifecycle**                                  | ✅                       | `endpoints.create/update/delete/list/get/disable/enable`, plus `rotateSecret({ keepPreviousFor })` for overlap-window rotation.                                                                              |
| **Signing & key management**                            | ✅                       | `HmacV1()` or `Ed25519V1a()`; key generation, rotation, and `keys.publicJwks()` served through each adapter's [`bindJwks()`](/docs/inbound/key-rotation#publishing-a-jwks-producer-side) mount.              |
| **Admin API**                                           | ✅                       | [`@postel/admin`](/docs/operations/admin) — a default-deny HTTP control plane: endpoint CRUD, replay, reconcile, tenants, key generation, `GET /health`.                                                     |
| **Introspection**                                       | ✅                       | [`messages.get / attempts / list`](/docs/outbound/messages) and [`tenants.get / list`](/docs/outbound/tenants), also over HTTP via the admin API.                                                            |
| **Storage**                                             | ✅                       | `InMemoryStorage()` for tests and single-process demos, plus the full [database adapter matrix](/docs/storage).                                                                                              |
| **Serverless drain**                                    | ✅                       | [`drain({ maxMessages, deadline })`](/docs/operations/serverless) — bounded single-pass delivery, safe alongside a live pool.                                                                                |
| **Observability**                                       | ✅                       | Logger pass-through with trace correlation, typed `postel.on(...)` events, `health()`, Prometheus-named `metrics()`, and OTel spans — the whole story is on [Observability](/docs/operations/observability). |
| **Multi-tenancy**                                       | ✅ scoping · ⏳ limits    | Tenant-scoped fanout, persistence, and reads ship; per-tenant rate-limit *enforcement* lands later (the config persists today).                                                                              |
| **Worker strategies**                                   | ✅ in-process · ⏳ queues | The in-process pool is the default and only runtime; `BullMQ` / `PgBoss` / `External` slots throw `NotImplementedError` until their adapters ship.                                                           |
| **KMS integration**                                     | ⏳ later                 | `PlaintextKms` only; `AwsKms` / `GcpKms` / `Vault` slots throw `NotImplementedError` until envelope encryption ships.                                                                                        |
| **Unwired slots fail fast**                             | ⏳ later                 | `retention`, `ephemeralKeys`, `http.tls` / `http.dns`, and per-endpoint `maxInflight` are typed but unwired — configuring any of them throws `NotImplementedError` rather than silently doing nothing.       |

**Security note:** until KMS lands, signing secrets are stored as-is in `endpoint_secrets.material` (`encryption = 'plaintext'`) — treat your database as the sole boundary protecting signing keys, the same as any secret store with no KMS in front of it.

## Where to read next [#where-to-read-next]

For the design rationale behind the library-not-service shape, read [Why Postel](/docs/project/why); for the guarantees this half makes and doesn't make, [Delivery guarantees](/docs/concepts/delivery-guarantees).
