storypane.

Outbound webhooks

Admins can configure up to 20 destinations in Settings → Webhooks. Create an endpoint, copy its signing secret into the receiving service, and use Send test. Test delivery runs through the same durable queue as content events. Delivery logs show attempts and safe failure summaries; Retry delivery explicitly resends a failed or cancelled event with the current destination configuration. Secrets appear only when created or rotated. Configuration reads never return them.

Production and development spaces

For the recommended team workflow, configure live-site rebuild webhooks in production. Use separate endpoints for dev/test builds and validate the expected spaceId in each receiver. Cloning a space does not copy webhook destinations or signing secrets; configure them separately when needed.

A schema promoted into production emits schema.deployed there. Deploying or syncing a dev schema affects that dev space’s events. Keep generated frontend types aligned with the destination space before rebuilding the live site.

Operator setup

Run pnpm admin migrate before starting a new Node release; migration 016_webhooks adds the endpoint, event, delivery, and attempt tables. The Node application starts a minute scheduler that runs immediately at startup and every 60 seconds. It runs scheduled publications, invalidates changed editor rooms, and drains up to 25 webhook deliveries. Overlapping local runs are skipped; failures are logged and persisted work is retried on the next tick.

Set WEBHOOK_HOSTS to a comma-separated list of exact, operator-approved public HTTPS hostnames in the build/deployment environment. Wildcards are unsupported. Destinations must use port 443, without credentials, IP literals, or URL fragments. Every attempt checks DNS addresses and rejects private/reserved results and redirects. Approve hosts controlled by trusted operators: preflight DNS checks alone do not prevent DNS rebinding through a malicious hostname owner.

Generate a 32-byte encryption key with openssl rand -base64 32. Store the value as the server-side WEBHOOK_ENCRYPTION_KEY in ignored .env.production or your deployment secret manager. Never prefix it with VITE_. Keep a protected backup: losing/changing this encryption key makes existing endpoint secrets unreadable. Rotate individual endpoint signing secrets through Settings; this is different from replacing the master encryption key. Missing or malformed encryption configuration disables creation/sending. Cleanup still runs.

Keep runtime secrets in the Node process environment and never commit them. The scheduler starts with the application; no separate cron service or public HTTP trigger is required. See the self-hosting guide.

Events and delivery

Default rebuild subscriptions are story.published, story.unpublished, story.deleted, asset.updated, and schema.deployed. Draft story.saved and story.moved notifications are optional. webhook.test targets the selected endpoint without changing its subscription. Schema deployment produces one migration event, rather than synthetic publication events for each rewritten publication.

Events and eligible deliveries are inserted inside the same transaction as the content change. A rollback cannot leave an outbound event. Each event records immutable JSON bytes, captured at commit time, with identifiers and paths only:

{
  "version": 1,
  "id": "11111111-1111-4111-8111-111111111111",
  "type": "story.published",
  "occurredAt": "2026-11-04T12:00:00.000Z",
  "spaceId": "22222222-2222-4222-8222-222222222222",
  "actorId": "user_123",
  "data": {
    "storyId": "33333333-3333-4333-8333-333333333333",
    "localeId": "44444444-4444-4444-8444-444444444444",
    "locale": "en",
    "checkpointId": "55555555-5555-4555-8555-555555555555",
    "publicationId": "66666666-6666-4666-8666-666666666666",
    "revision": 8,
    "path": "news/launch"
  }
}

Other data fields are oldPath for movement, migrationVersion for deployment, and assetId/metadataRevision for asset updates. Actor identity is optional for service-originated events. Payloads exclude content, asset bytes, sessions, tokens, and secrets. Draft path movement leaves the existing publication unchanged.

Delivery is at least once, with no global ordering guarantee. Persist event IDs for deduplication and fetch current published content when rebuilding. A receiver can accept an event whose response is lost; that retry keeps the same event ID. Manual retry also keeps that ID. Each attempt has a new timestamp/signature.

Any 2xx succeeds. Network errors, timeouts, HTTP 408/429/5xx retry at 1, 5, 15, and 60 minutes, for five total attempts. A valid Retry-After can extend a delay to at most 24 hours. Other failures are terminal. Each request (including DNS and response reads) has a 10-second deadline and a 16 KiB response limit. Response bodies are not stored. A worker claims work for 60 seconds; expired claims are recoverable, and obsolete claims cannot overwrite a newer result.

Changing endpoint settings or rotating its secret cancels work queued for the old revision. Disabling/deleting stops new attempts; an already-started HTTP request cannot be recalled. Logs remain for 30 days after completion. Pending work and its event payload remain until processed. Manual retries start a new five-attempt cycle and preserve the complete attempt history during retention.

Verify signatures and build a static site

The POST sends Webhook-Id, Webhook-Timestamp (Unix seconds), and Webhook-Signature (v1= followed by a lowercase hex HMAC-SHA256). Sign the exact UTF-8 body bytes over timestamp + '.' + body; do not parse/re-serialize first. Reject timestamps more than five minutes from your current time. The exported verifyWebhookSignature(secret, timestamp, body, signature) helper uses Web Crypto verification and performs this timestamp check.

The executable receiver example verifies those headers and expected space, durably deduplicates/queues events in SQLite before returning HTTP 202, and runs a fixed pnpm build command in a configured site. It resumes pending builds after restart and retries failed builds independently of webhook delivery. Run one process per queue database behind a public HTTPS reverse proxy to its loopback port. Keep the SQLite database on persistent storage.

Configure the receiver’s process environment with:

Start it with pnpm exec tsx scripts/webhook-rebuild.ts. Configure the CMS endpoint as https://<approved-receiver-host>/webhook. Build commands and directories come only from operator configuration; event payloads cannot select commands. Publishing then queues a build that reads current content with the receiver’s own token. webhook.test verifies connectivity without running a build. Successful jobs are retained for deduplication for 30 days. Deploying build output remains the site’s existing build/deployment responsibility.

Verification

pnpm exec vitest run packages/db/src/webhook-transport.test.ts packages/db/src/webhooks.test.ts apps/web/src/lib/background.test.ts tests/webhook-rebuild.test.ts
pnpm exec playwright test --config playwright.webhooks.config.ts

Database tests use isolated schemas in disposable local test databases; all HTTP and DNS destinations are controlled test doubles. The sample receiver is exercised by a signed publication event and builds from token-authorized delivery content. The browser harness covers admin controls without requiring a Clerk sign-in; Postgres tests independently verify session, role, and space enforcement. The actual local scheduled endpoint was invoked twice against an isolated schema: a due checkpoint published once, produced one immutable event and delivery attempt, and the second invocation produced no duplicates. The empty runtime allowlist blocked that test destination before any external DNS/HTTP. Real signed-in end-to-end and deployed webhook delivery remain release checks.

View this guide on GitHub ↗