Migration guides

From Svix self-hosted

API surface mapping from a self-hosted Svix instance to Postel's outbound API.

View as Markdown

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

SvixPostel
ApplicationTenant (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 filterTypesEndpoint types (glob strings: "order.*", "*")
svix.message.create(appId, { eventType, payload })postel.outbound.send({ type, data }, { tx })
Message AttemptA row in attempts, read via postel.outbound.messages.attempts(id)
svix.messageAttempt.listByMsg(appId, msgId)postel.outbound.messages.attempts(id)
svix.message.get / listpostel.outbound.messages.get(id) / .list(...)
Event Type registryNo 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.rotateSecretpostel.outbound.endpoints.rotateSecret(id, { keepPreviousFor })
Endpoint secret rotation overlap windowSame 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 failurecircuitBreaker + autoDisable config (see Retries & backoff)
Operational webhooks (endpoint.updated, message.attempt.exhausted, …) subscribed on your own Svix instancepostel.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 PortalNot available — see what you give up
Self-hosted Postgres + Redis + dispatcher deploymentNo separate deployment — postel.start() runs the worker pool inside your app process against your existing database

Migration steps

  1. Add @postel/core and 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.
  2. 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.
  3. For every Svix Endpoint, call postel.outbound.endpoints.create({ url, types: filterTypes, tenantId }). Exact-match filterTypes values carry over unchanged; Postel's types additionally support globs ("order.*"), which Svix's exact-match filters don't have.
  4. Replace every svix.message.create(...) call with postel.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.
  5. 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.
  6. Swap any code that polled Svix's operational webhooks for postel.on(event, handler) listeners in-process.
  7. Point receivers at your new endpoint URLs. Because the signature headers are the spec's, not Svix-specific, a receiver already using svix.Webhook or standardwebhooks.Webhook to 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.)
  8. 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?.

  • Endpoints — the full lifecycle, filtering, and per-endpoint overrides.
  • Replay — the verbs behind svix.message.resend.
  • Admin API — building your own operational surface.

On this page