Webhooks
Outbound HMAC-signed HTTP delivery on record events + custom dispatch from hooks. Retry budget, dead-letter trail, SSRF guard.
Vaultbase ships outbound webhooks — when records change (or your hook code says so), it POSTs JSON to URLs you configure. Each delivery is HMAC-SHA-256 signed, retried on failure, and logged for audit.
Subscribe to events
Section titled “Subscribe to events”A webhook lists which events it cares about. Patterns:
| Pattern | Matches |
|---|---|
posts.create | exact |
posts.* | every event under posts |
* | everything |
Built-in events fired automatically:
<collection>.create— after a record is created<collection>.update— after a record is updated<collection>.delete— after a record is deleted
Records created by auth flows (sign-up, invite, guest, OAuth2) and relations cleared by a cascade delete don’t fire these events.
Custom events fired from hooks/routes/cron:
await ctx.helpers.webhooks.dispatch("billing.invoice_paid", { customer: ctx.record.customer_id, amount: ctx.record.amount,});Delivery payload
Section titled “Delivery payload”{ "id": "<uuid>", "event": "posts.create", "timestamp": 1735000000, "data": { "record": { "id": "...", "title": "...", ... } }}For <collection>.delete, data is { "id": "...", "record": { ... } } with the
deleted record. For custom events, data is what you passed to dispatch.
Headers on every request:
| Header | Notes |
|---|---|
X-Vaultbase-Event | event label, e.g. posts.create |
X-Vaultbase-Delivery | unique delivery id (also visible in admin) |
X-Vaultbase-Timestamp | unix-seconds when the request was prepared |
X-Vaultbase-Signature | sha256=<hex> of HMAC over <timestamp>.<body> |
Plus any custom headers you configured (X-API-Key, …). Authorization,
Cookie, Set-Cookie, Host, Content-Type, Content-Length,
Transfer-Encoding, User-Agent and the X-Vaultbase-* headers are reserved:
a custom header with one of these names is dropped.
Verifying deliveries (receiver side)
Section titled “Verifying deliveries (receiver side)”const expected = "sha256=" + hmacSha256(secret, `${ts}.${rawBody}`);if (!constantTimeEqual(expected, header)) reject();if (Date.now() / 1000 - parseInt(ts) > 300) reject(); // replay windowAlways compare in constant time, and reject deliveries with a stale timestamp (5-minute window is a sane default).
Retry & dead-letter
Section titled “Retry & dead-letter”Each webhook configures:
- Retry max — attempts before giving up (default 3)
- Retry backoff —
exponential(1s, 2s, 4s, …) orfixed - Retry delay (ms) — base delay
- Timeout (ms) — per-attempt request timeout (default 30 s)
Failure progression: pending → pending (retry scheduled) → … → dead.
A 2xx response marks the delivery succeeded and the row is finalised.
Anything else (network error, timeout, 3xx, 4xx, 5xx) increments the attempt
counter and reschedules — until retry_max attempts have been made, after
which the row goes to dead. Redirects are not followed: a receiver that
redirects must be configured with its final URL.
The admin UI shows recent deliveries per webhook: status badge, attempt count, response code, timestamps. Click a row for the full payload and response body (truncated to 2 KB).
SSRF guard
Section titled “SSRF guard”Outbound URLs pass through the same egress CIDR filter as
helpers.http. Default-deny RFC1918 / loopback / link-local. Override
via Hook egress settings.
If a webhook’s URL resolves into a denied range, the delivery goes
directly to dead with error: "egress blocked: …" — no retry, no
network call.
REST API
Section titled “REST API”GET /api/v1/admin/webhooks — listGET /api/v1/admin/webhooks/:id — read onePOST /api/v1/admin/webhooks — create (URL must be http(s)://)PATCH /api/v1/admin/webhooks/:id — updateDELETE /api/v1/admin/webhooks/:id — deletePOST /api/v1/admin/webhooks/:id/test — fire a synthetic eventGET /api/v1/admin/webhooks/:id/deliveries — recent deliveriesPOST /api/v1/admin/webhooks/:id/deliveries/:deliveryId/redeliver — send a past delivery againA test fire on a disabled webhook is a 409. A redelivery is a new delivery
with the same payload, so its id (the event id receivers dedupe on) is the
original’s; the X-Vaultbase-Delivery header is new.
All admin-auth gated. Mutations are picked up by the audit log under
webhooks.* actions.
Admin UI
Section titled “Admin UI”/_/webhooks — two-pane editor:
- List — name, status dot, URL preview
- Editor — name, URL, event chips, HMAC secret (regenerate), custom headers, retry/timeout settings
- Recent deliveries — table with click-through to full payload + response. Fire test button enqueues a synthetic event scoped to this webhook only (won’t disturb other subscribers). Redeliver in a delivery’s drawer sends its payload again.
Limits
Section titled “Limits”- In-process dispatcher — fires every 2 seconds, claim limit 50
deliveries per tick. Sufficient for most loads; for very high event
volumes, several servers on one Postgres database share the dispatcher
load (each delivery is claimed by one of them).
vaultbase clusteris deprecated. - No webhook receives more than
retry_maxtotal attempts. Each delivery is claimed before it’s sent, so overlapping ticks and cluster workers never send the same attempt twice. If the process dies mid-send, the claim expires after 3 minutes and the attempt is retried. - Per-attempt timeout is capped at 120 s, whatever
timeout_mssays. - Response body is captured to the delivery log up to 2048 bytes.
- Finished deliveries (
succeeded/dead) are kept 30 days by default, then pruned (Settings → Data retention,maintenance.retention_days, 0 = forever). - The dispatcher does not preserve cross-event order — slow consumers
- retries can shuffle the receive order.