Versioned archive. You are viewing v0.38.0. For the latest released guidance, use Latest release. Report archive issues on GitHub.
You operate an active registry, or are about to activate one as deploy a registry
describes, whose project declares events that name a webhook destination, and a receiver is
waiting for them. At the end of this page every destination identifier the package names has a
binding in the runtime file, the receiver holds the same signing key and has been built against a
sample request, and you can list and recover deliveries.
An event is a declaration in the project, a trigger on an entity with a projection of its fields, that the runtime delivers after the write commits. A destination is the receiver the declaration names by identifier; the package carries only that identifier, and the runtime file binds it to an origin, so a module stays deployment-neutral. Send registry events to a webhook walks a first delivery on a local registry, and the events section of author a registry project covers the declaration.
Bind each destination
Section titled “Bind each destination”The package refuses to activate until the runtime file binds every destination identifier it names, and it refuses extra bindings. Destination URLs and secret references belong in the runtime file, never in a module:
eventDestinations: record-receiver: origin: https://receiver.example.org path: /events networkProfile: productionHttps dnsFamily: dualStackStrict allowedPrivateCidrs: [] hmacSha256KeyRef: secret:file/record-webhook-key classificationCeiling: restricted deliveryCeilings: attemptTimeoutMilliseconds: 1000 maximumAttempts: 3eventDelivery: payloadRetentionDays: 7The destination’s classificationCeiling must cover the event’s derived classification; a binding
cannot add fields to a payload. For a receiver on a private network, list the intended canonical
ranges in allowedPrivateCidrs. Use dnsFamily: ipv4Only only on an IPv4-only network. Optional
tls.caBundleRef and tls.clientIdentityRef name PEM secrets for a private certificate authority
and mutual TLS. networkProfile: loopbackDevelopmentHttp exists for local development only.
The productionHttps profile refuses localhost and every *.localhost name during
configuration validation, including absolute names with a trailing dot. Use the loopback profile
for a receiver on the same development host.
Lifecycle events whose transition and state filters permit application have an internal
classification floor: their system request envelope may contain the applier’s explanation.
Filters excluding application retain the event’s ordinary derived classification.
The destination must permit that classification even when the record projection contains only
public fields. Reader grants that hide reason text do not change webhook delivery authority.
A binding may tighten the compiled ceilings and never widen them; the example tightens both.
bregctl explain events <project> reports the compiled values per delivery: five attempts with a
five-second timeout each, retried after 1, 2, 4, and 8 seconds, with a dead letter required when
the attempts run out. Automatic retries keep the event id and idempotency key. Any 2xx
acknowledges delivery; redirects, timeouts, and other responses follow the bounded retry policy.
Activation refuses a rebinding that would leave retained pending or dead-lettered deliveries
without the binding they were captured under. bregctl doctor opens every binding before the
process starts and reports one it cannot open as startup.event_destinations.refused, so run it
after every change to this block.
Provision the shared key
Section titled “Provision the shared key”Generate the HMAC-SHA-256 key as an owner-only file inside the secret root, as the user that runs
breg, and provision the identical bytes to the receiver through its own secret store:
(umask 077; openssl rand -hex 32 | tr -d '\n' > /etc/breg/secrets/record-webhook-key)The runtime signs with the file’s bytes exactly as written, neither trimmed nor decoded, so the
receiver must load the same 64 characters as its key rather than decode them. The key needs at
least 32 bytes, and the file must be a regular file with mode 0400 or 0600 owned by the running
user; doctor and startup refuse a binding whose key breaks either rule.
Build the receiver against a sample
Section titled “Build the receiver against a sample”Your receiver verifies X-Registry-Signature against the exact request bytes and checks
X-Registry-Delivery-Time for freshness before acting. webhook sample prints one request for
an event with placeholder identifiers and a placeholder signature, without a runtime or a
database, so the receiver can be built before the registry serves:
bregctl webhook sample ./my-registry --event record-created-v1Built the sample delivery. The canonical request follows. event record-created-v1
POST <configured-webhook-request-target> HTTP/1.1Accept: application/jsonContent-Type: application/jsonIdempotency-Key: sha256:0000000000000000000000000000000000000000000000000000000000000000X-Registry-Delivery-Attempt: 1X-Registry-Delivery-Time: 2026-01-01T00:00:00ZX-Registry-Event-Generation: 1X-Registry-Signature: v1=<computed-at-delivery>ce-dataschema: urn:breg:event-schema:generic-registry:record:record-created-v1:sha256:411f35fcbaa498497ac172fdb725d2f583d761a73d903c1647b34dc4af703855ce-id: 00000000-0000-4000-8000-000000000001ce-source: urn:registrystack:registry:generic-registry:instance:<configured-instance>ce-specversion: 1.0ce-time: 2026-01-01T00:00:00Zce-type: record-created-v1
{"causation":{"hop":0,"root":"00000000-0000-4000-8000-000000000001"},"data":{"entity":"record","packageRevision":"sha256:0000000000000000000000000000000000000000000000000000000000000000","recordId":"00000000-0000-4000-8000-000000000002","revision":1,"trigger":"created","values":{"code":"x","status":"draft"}},"dataschema":"urn:breg:event-schema:generic-registry:record:record-created-v1:sha256:411f35fcbaa498497ac172fdb725d2f583d761a73d903c1647b34dc4af703855","id":"00000000-0000-4000-8000-000000000001","source":"urn:registrystack:registry:generic-registry:instance:<configured-instance>","subject":{"recordReference":"hmac-sha256:0000000000000000000000000000000000000000000000000000000000000002","recordRevision":1},"time":"2026-01-01T00:00:00Z","type":"record-created-v1"}That sample carries generic-registry, the registry id bregctl init writes into registry.yaml;
your project carries its own id, schema hash, and projected fields. --format json returns the
same request as a document with the method, request target, headers, body, and canonical body. The
API reference defines the request shape and the signing input byte
order. Deduplicate retries by Idempotency-Key, use the record revision to order events, and
acknowledge only after durably recording acceptance.
Inspect and recover deliveries
Section titled “Inspect and recover deliveries”Inspect pending, leased, dead-lettered, and expired deliveries, then replay a dead letter after fixing the receiver:
bregctl --format json webhook list \ --runtime-config /etc/breg/runtime.yaml --limit 50
bregctl webhook replay \ --runtime-config /etc/breg/runtime.yaml \ --event-id "<eventId>" --delivery-id "<deliveryId>" \ --expected-generation "<generation>"The list carries delivery metadata and no record values, and --limit accepts 1 to 100. A
delivery that exhausts its attempts becomes a dead letter with a closed deadLetterReason while
its payload is retained. Older deliveries, and deployments awaiting a schema update, may omit
that reason. bindingActive reports whether the exact captured binding is deployed;
replayEligible is true only while that binding and the retained payload are available.
Successful delivery erases the payload once no sibling delivery needs it, and retained payloads
expire after payloadRetentionDays, which defaults to 7 and is capped at 30. An empty list is not
proof of success: completed
deliveries are excluded, so confirm effects from the receiver’s own record. Replay keeps the event
id and issues a new generation and idempotency key; if the receiver’s earlier effect is uncertain,
check its state before replaying. A replay whose --expected-generation no longer matches, whose
payload has expired, or whose destination is no longer bound is refused with
webhook.operation.refused.
If doctor reports startup.webhook.retained_bindings after you correct a destination or local
handler binding, keep the corrected configuration and use webhook list. Rows with
bindingActive: false name the retained work that still pins the earlier binding. Restore that
exact binding until those rows drain, or stop the runtime and discard each eligible row with the
generation returned by the list:
bregctl webhook discard \ --runtime-config /etc/breg/runtime.yaml \ --event-id "<eventId>" --delivery-id "<deliveryId>" \ --expected-generation "<generation>"Discard permanently closes this delivery without sending it under the corrected binding. It does not undo a request the receiver may already have accepted. Check the receiver before discarding when an earlier effect is uncertain.
discardEligible: true identifies pending work, dead letters with retained payload, and leases
whose deadline has passed when no proposal receipt is retained. A live lease or any retained
proposal receipt is refused. A stale generation is also refused, so refresh the list before
retrying. After discard, the row remains visible as expired with a new generation and without
payload availability. Discarding one delivery leaves the shared payload available while a sibling
delivery still needs it.
Operator commands connect to PostgreSQL directly. When the database uses a private certificate
authority, set SSL_CERT_FILE to its PEM bundle for each command.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Next move |
|---|---|
| Activation refuses the package over destinations | Every destination identifier the package names needs one binding, and no binding may name an identifier the package does not. Compare the eventDestinations keys with bregctl explain events. |
doctor reports startup.event_destinations.refused | Check the key file’s owner, mode, and length, the origin against the network profile, and allowedPrivateCidrs for a private receiver. |
doctor reports startup.webhook.retained_bindings | Run webhook list under the corrected configuration. Restore the exact captured binding until each row drains, or stop the runtime and discard each row where discardEligible is true. Wait for a live lease to expire before retrying discard. |
| The receiver rejects every signature | The runtime signs the raw file bytes. Load the same characters on the receiver without decoding them, and verify against the exact request bytes in the documented signing-input order. |
doctor reports startup.instance_id.pending_deliveries | Pending deliveries were captured under the event source of a previous identity.instanceId, and the worker would dead-letter them under the new one. Restore the instance id that ends the stored source the diagnostic names until webhook list shows them drained, then change it. |
| A webhook never fires | Confirm the destination is bound, the receiver returns 2xx, and the event’s condition matched; use webhook list for dead letters. |
| Replay is refused | Refresh the list and check the generation, replayEligible, payload expiry, and the destination binding. |
- Send registry events to a webhook to see one delivery end to end on a local registry.
- Author a registry project for the event declaration, its projection, and its classification.
- Base Registry Engine API reference for the delivery request shape, the signing input, and the CloudEvents headers.
- Base Registry Engine configuration reference for every
eventDestinationsandeventDeliverykey.