Skip to content

Webhooks

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.

A webhook lists which events it cares about. Patterns:

PatternMatches
posts.createexact
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

Custom events fired from hooks/routes/cron:

await ctx.helpers.webhooks.dispatch("billing.invoice_paid", {
customer: ctx.record.customer_id,
amount: ctx.record.amount,
});
{
"id": "<uuid>",
"event": "posts.create",
"timestamp": 1735000000,
"data": { "record": { "id": "...", "title": "...", ... } }
}

Headers on every request:

HeaderNotes
X-Vaultbase-Eventevent label, e.g. posts.create
X-Vaultbase-Deliveryunique delivery id (also visible in admin)
X-Vaultbase-Timestampunix-seconds when the request was prepared
X-Vaultbase-Signaturesha256=<hex> of HMAC over <timestamp>.<body>

Plus any custom headers you configured (Authorization, X-API-Key, …).

const expected = "sha256=" + hmacSha256(secret, `${ts}.${rawBody}`);
if (!constantTimeEqual(expected, header)) reject();
if (Date.now() / 1000 - parseInt(ts) > 300) reject(); // replay window

Always compare in constant time, and reject deliveries with a stale timestamp (5-minute window is a sane default).

Each webhook configures:

  • Retry max — attempts before giving up (default 3)
  • Retry backoffexponential (1s, 2s, 4s, …) or fixed
  • 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, 4xx, 5xx) increments the attempt counter and reschedules — until retry_max, after which the row goes to dead.

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).

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.

GET /api/v1/admin/webhooks — list
GET /api/v1/admin/webhooks/:id — read one
POST /api/v1/admin/webhooks — create (URL must be http(s)://)
PATCH /api/v1/admin/webhooks/:id — update
DELETE /api/v1/admin/webhooks/:id — delete
POST /api/v1/admin/webhooks/:id/test — fire a synthetic event
GET /api/v1/admin/webhooks/:id/deliveries — recent deliveries

All admin-auth gated. Mutations are picked up by the audit log under webhooks.* actions.

/_/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).
  • In-process dispatcher — fires every 2 seconds, claim limit 50 deliveries per tick. Sufficient for most loads; for very high event volumes, run vaultbase in cluster mode (SO_REUSEPORT) so multiple workers share the dispatcher load.
  • No webhook receives more than retry_max + 1 total attempts.
  • Response body is captured to the delivery log up to 2048 bytes.
  • The dispatcher does not preserve cross-event order — slow consumers
    • retries can shuffle the receive order.