Pre-alpha · inbound + outbound

Webhooks as a feature of your product.

Sending and receiving webhooks is easy. Doing it reliably and securely is hard — retries, replay, signing, key rotation, idempotency, raw-bytes preservation. Postel is a polyglot library that handles those for you, inside your app, against your own database.

pnpm add @postel/core
npm install @postel/core
yarn add @postel/core
bun add @postel/core
app.ts
import { Hono } from "hono";
import { HonoWebAdapter, POSTEL_CONTEXT_KEY } from "@postel/hono";
import { postel } from "@/lib/postel";

const app = new Hono();
const hwa = HonoWebAdapter(postel, app);

hwa.inbound.vendor.post("/webhooks/vendor", (c) => {
  const { event } = c.get(POSTEL_CONTEXT_KEY); // verified · raw bytes intact
  return c.json({ ok: true, type: event.type });
});

The outbox, by hand vs. by Postel

The queue handles retries. You reimplement everything else.

Hand-rolling outbound delivery means a broker, a worker process, and a transactional race you have to get right — before you even start on signing, backoff, dead-letter, replay, and key rotation. Postel collapses that into one outbox insert that commits with your write.

Without Postel

hand-rolled.ts
// Wiring an outbox by hand — plus a Redis to run it
import { Queue, Worker } from "bullmq";

const deliveries = new Queue("webhooks", { connection: redis });

await db.tx(async (tx) => {
  await db.orders.insert(order, { tx });
  // Enqueue inside the tx and a crash drops the event; enqueue after
  // commit and you can deliver an order that rolled back. Pick a race.
  await deliveries.add("order.created", { id: order.id });
});

// A separate worker process you build, deploy, and operate:
new Worker("webhooks", async (job) => {
  const payload = JSON.stringify(envelope(job.data));
  const signature = signHmac(payload, endpoint.secret); // you write this
  const res = await fetch(endpoint.url, { method: "POST", body: payload });
  if (!res.ok) throw new Error("retry");
  // backoff, circuit-breaking, dead-letter, replay, key rotation,
  // JWKS — all still yours to build, test, and keep correct.
}, { connection: redis });

With Postel

orders.ts
// The outbox is one INSERT in your own transaction
import { postel } from "@/lib/postel";

await db.tx(async (tx) => {
  await db.orders.insert(order, { tx });
  await postel.outbound.send(
    { type: "order.created", data: { id: order.id } },
    { tx }, // signing, retries, backoff, dead-letter, replay — handled
  );
});

Two halves, one library

Receive webhooks. Send webhooks. Use either alone.

The Postel factory composes both — but in these docs they stay separate, so you never wade through outbound material to integrate the receiver, or vice versa.

Inbound · receive

Configure once, per source

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

export const postel = Postel({
  inbound: {
    stripe: {
      verify: Secret(config.stripeSecret),
    },
    // rotate keys with zero downtime — accept either during the window
    github: {
      verify: [Secret(config.githubSecretNew), Secret(config.githubSecretOld)],
    },
    // or verify asymmetric signatures straight from a JWKS endpoint
    partner: {
      verify: Keyset({ jwksUri: "https://partner.example/jwks" }),
    },
  },
});
  • HMAC v1 + Ed25519 v1a signatures
  • JWKS consumer — caching, auto-refresh
  • Multi-secret rotation windows
  • Idempotent dedup (Postgres / SQLite / memory)
  • Raw-bytes preservation
  • Typed errors that name the failed step
Explore inbound

Outbound · send

Configure once, then send

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

export const postel = Postel({
  outbound: {
    storage: InMemoryStorage(), // or a DB-backed Storage adapter
    signing: HmacV1(), // or Ed25519V1a() for asymmetric + JWKS
    retryPolicy: ExponentialBackoff({ maxAttempts: 8 }),
  },
});
  • Transactional outbox — joins your write
  • Retries, backoff, circuit breaker
  • Replay by message, endpoint, or filter
  • Fanout to N endpoints
  • Endpoint lifecycle + secret rotation
  • Dead-letter + auto-disable
Explore outbound

Library, not service

Uses your existing database

Outbox inserts join your existing transaction. No Redis, no broker, no separate dispatcher process. The library you embed; not the service you stand up.

Standard Webhooks

Compliant by default

Headers, signature schemes (HMAC v1 + Ed25519 v1a), payload envelope, prefixes — all follow the Standard Webhooks spec. JWKS publication is a one-liner.

Polyglot

Same contract, four languages

TypeScript first. Go, Python, and Rust follow. One executable compliance suite gates every port at the same release version. The contract is the suite — not prose.

Evaluating?

Run the six-line filter before you read more.

Postel has a narrow scope on purpose. Six yes/no questions tell you whether the library fits your case, your stack, and your timeline — or whether you'd be happier with Svix, Hookdeck Outpost, or a hand-rolled queue worker.

Is Postel for me?