From Svix self-hosted
API surface mapping from a self-hosted Svix instance to Postel's outbound API.
Svix and Postel agree on the wire format — Svix co-authored the Standard Webhooks spec that Postel implements, so svix-id / svix-timestamp / svix-signature and webhook-id / webhook-timestamp / webhook-signature are the same three headers under different names. Nothing on the receiving end of your customers' integrations needs to change. What changes is what runs your side: a separate Svix service (self-hosted Postgres + Redis + dispatcher process) becomes a library embedded in the app that already owns the data.
API surface mapping
| Svix | Postel |
|---|---|
| Application | Tenant (tenantId on send() / endpoints.create()) — or nothing, if you only ever ran one application |
svix.endpoint.create(appId, { url, filterTypes }) | postel.outbound.endpoints.create({ url, types, tenantId }) |
Endpoint filterTypes | Endpoint types (glob strings: "order.*", "*") |
svix.message.create(appId, { eventType, payload }) | postel.outbound.send({ type, data }, { tx }) |
| Message Attempt | A row in attempts, read via postel.outbound.messages.attempts(id) |
svix.messageAttempt.listByMsg(appId, msgId) | postel.outbound.messages.attempts(id) |
svix.message.get / list | postel.outbound.messages.get(id) / .list(...) |
| Event Type registry | No separate registry — type is just a string; validate it with a Standard Schema if you want type-safety |
svix.message.expungeContent / retention | ⏳ not yet wired — see the outbound status table |
svix.endpoint.rotateSecret | postel.outbound.endpoints.rotateSecret(id, { keepPreviousFor }) |
| Endpoint secret rotation overlap window | Same concept — endpoint_secrets keeps the previous key status: "expiring" until keepPreviousFor elapses |
svix.message.resend / "replay missing messages" | postel.outbound.replay({ messageId }), replay({ endpointId, since }), replay({ filter }) |
| Circuit breaker / endpoint auto-disable on repeated failure | circuitBreaker + autoDisable config (see Retries & backoff) |
Operational webhooks (endpoint.updated, message.attempt.exhausted, …) subscribed on your own Svix instance | postel.on("attempt" | "circuit-open" | "circuit-close" | "dead-letter", handler) — in-process, no separate subscription |
| The Svix Dashboard | @postel/admin — a REST control plane you mount yourself; no bundled UI |
| The Svix customer Portal | Not available — see what you give up |
| Self-hosted Postgres + Redis + dispatcher deployment | No separate deployment — postel.start() runs the worker pool inside your app process against your existing database |
Migration steps
- Add
@postel/coreand a storage adapter for your database — standalone@postel/pg/@postel/sqlite/@postel/mysql, or the adapter matching whatever ORM your app already uses. This replaces Svix's own Postgres instance; Postel's tables (endpoints,messages,attempts, …) live in your database now. - Recreate each Svix Application as either a Postel
tenantId(if you need per-customer isolation and rate limits) or drop the concept entirely if you only ran one. - For every Svix Endpoint, call
postel.outbound.endpoints.create({ url, types: filterTypes, tenantId }). Exact-matchfilterTypesvalues carry over unchanged; Postel'stypesadditionally support globs ("order.*"), which Svix's exact-match filters don't have. - Replace every
svix.message.create(...)call withpostel.outbound.send({ type, data }, { tx }), and — this is the point of moving off a separate service — pass{ tx }to join the DB transaction of the write that caused the event. Svix, being an external service, could never offer this. - Mount
@postel/admin(adminRouter(postel, { authorize })) wherever you need the operational surface the Svix Dashboard gave you — endpoint CRUD, replay, message/attempt reads, key rotation. - Swap any code that polled Svix's operational webhooks for
postel.on(event, handler)listeners in-process. - Point receivers at your new endpoint URLs. Because the signature headers are the spec's, not Svix-specific, a receiver already using
svix.Webhookorstandardwebhooks.Webhookto verify keeps working without a code change — only the secret and URL differ. (If a receiver is your own code, see From a signing-only library for the flip side.) - Decommission the Svix deployment once traffic has fully cut over and replay history you need has been exported.
What you give up
- The customer-facing portal. Svix's packaged, embeddable portal that lets your customers manage their own endpoints, view delivery logs, and download signing keys has no Postel equivalent — Postel gives you the admin API to build that surface yourself, but ships no UI. If your customers currently self-serve through the Svix portal, budget for that UI work, or keep Svix for the segment of customers who need it.
- Multi-region delivery and Svix's operational SLA. Svix operates dispatch infrastructure across regions with a published uptime commitment. Postel runs inside your own app process on your own infrastructure — its reliability is now a function of your deployment, not a vendor's.
- A managed, horizontally-scaled dispatcher. Svix's dispatcher is a separate scaling unit from your application. Postel's worker pool runs in-process; scaling delivery throughput means scaling your app, and very high fan-out may need the same benchmarking called out in the BullMQ migration guide.
If any of the above is a hard requirement rather than a nice-to-have, staying on Svix (or moving to Hookdeck Outpost) is the right call — see Is Postel for me?.