Event And Audit¶
Hubuum stores one canonical event stream in events. Audit reads query that
stream directly, while external delivery fans matching events out through
event_subscriptions and event_deliveries.
The event stream is append-only during normal application operation. Domain changes emit events in the same database transaction as the state change, so an event exists only if the change commits.
New events can also carry an internal validated OpenTelemetry trace link. It is not serialized in audit API responses. Fan-out and delivery workers use it as a span link so delayed work remains connected without persisting baggage or raw headers. See Distributed Tracing.
New event bodies are constructed as backend-neutral AuditDocument values.
The type validates object-shaped snapshots and metadata and owns schema-version
selection: version 1 has no numeric resource revision, while version 2 carries
a positive revision in at least one snapshot. Storage adapters add entity and
actor coordinates but do not define a separate document shape.
At the storage boundary, ordinary audited mutations require an explicit actor context and return a committed receipt identifying that durable event. A receipt is non-sensitive proof of persistence, not an audit projection; event snapshots and provenance remain permission-scoped reads. The normative adapter guarantees and their reusable certification are documented in the storage contract.
Audited mutation paths compare the requested state with the stored state before
writing. Requests that would leave domain state unchanged are no-ops: they do
not advance updated_at or append lifecycle events. This includes identical
entity updates, moves to the current parent, repeated permission grants or
revocations, repeated service-account disable requests, and membership changes
that do not change effective membership.
Audit Log¶
Audit readers query the canonical stream with GET /api/v1/events. The
endpoint is cursor-paginated and supports the normal pagination headers:
GET /api/v1/events?entity_type=collection&action=created&limit=50&sort=-occurred_at
Authorization: Bearer <token>
Supported audit filters are:
| Filter | Meaning |
|---|---|
entity_type |
Event entity type, such as collection, class, object, or task |
entity_id |
Integer id of the affected entity |
action |
Event action for the entity type, such as created, updated, or deleted |
actor_kind |
Immediate actor class: user, worker, or system |
actor_user_id |
Principal id for user actors, including service accounts |
initiator_user_id |
Root task initiator principal id; legacy task events derive this from their queued event |
collection_id |
Collection directly attached to the event |
occurred_after |
Lower occurred_at bound; accepts RFC 3339 or YYYY-MM-DD |
occurred_before |
Upper occurred_at bound; accepts RFC 3339 or YYYY-MM-DD |
before_revision |
Positive revision from the pre-mutation entity snapshot |
after_revision |
Positive revision from the post-mutation entity snapshot |
Supported sorts are id and occurred_at, with - for descending order.
For example, sort=-occurred_at returns the newest visible events first.
Every audit response retains the compatibility fields actor_user_id and
actor_kind and also includes shared mutation provenance:
{
"actor_user_id": null,
"actor_kind": "worker",
"provenance": {
"actor": {
"kind": "worker",
"principal": null
},
"initiator": {
"principal_id": 1,
"name": "admin"
},
"task_id": 12
}
}
The actor is the principal or server component that performed the immediate
mutation. The initiator is the principal that submitted the root task. Direct
request mutations normally have a user actor and no separate initiator.
Principal names are resolved when a page is read; durable IDs remain present
when a principal has been deleted, while an unavailable name is null.
Historical task events that predate stored initiator provenance derive it from
the task's queued event with one bounded query per response page.
Audit visibility is collection-scoped:
- A caller sees collection events only for collections where the caller has
ReadAudit. - Events that reference related collections in event metadata are visible to a
caller with
ReadAuditon one of those related collections. Related-collection-only visibility returns the event identity, actor, summary, metadata, and schema version, but redactsbeforeandaftersnapshots. A caller sees snapshots only when they also have directReadAuditon the event's own collection. - Collection-less events are visible only to unscoped admins.
- Scoped tokens are constrained by both their token scope and the caller's underlying permissions.
Task lifecycle history is also stored in events. User-facing task reads
should usually use GET /api/v1/tasks/{task_id}/events, which applies the task
authorization model and returns task-focused history.
Convenience audit routes are available for common resources. These routes are
thin wrappers around GET /api/v1/events, apply the same ReadAudit scoping,
and accept the same pagination, actor, initiator, action, collection, and time
filters:
GET /api/v1/collections/12/events
GET /api/v1/classes/34/events
GET /api/v1/classes/34/56/events
GET /api/v1/iam/users/78/events
GET /api/v1/iam/groups/90/events
GET /api/v1/export-templates/11/events
GET /api/v1/remote-targets/22/events
The object convenience route verifies that the object belongs to the class in
the path. A mismatched class and object pair returns 404 Not Found rather than
querying the object through an unrelated class URL.
Use the generic endpoint for relation, permission, token, sink, and subscription events where the useful identity is often in event metadata rather than a single resource path.
Sinks And Subscriptions¶
External delivery is configured in two layers:
- Event sinks are global transport definitions. Admins manage them through
/api/v1/event-sinks. - Event subscriptions are collection-scoped routing rules. Callers need
ManageEventSubscriptionon the collection and manage them through/api/v1/collections/{collection_id}/event-subscriptions. - Administrators manage collection-less events through
/api/v1/system-event-subscriptions. System subscriptions match only events with neither a direct collection nor related collections.
A sink describes how to deliver. A subscription describes which events should
be delivered to a sink. The primary subscription filters are entity_types and
actions; Hubuum validates these against the event catalog and rejects
impossible entity/action pairs at write time. Subscriptions may also include a
filter object that narrows matching by stable event-envelope fields before
delivery rows are created.
Supported filter fields are:
| Field | Meaning |
|---|---|
collection_ids |
Match events directly attached to one of these collections |
related_collection_ids |
Match events whose metadata references one of these related collections |
entity_ids |
Match affected entity ids |
entity_names |
Match affected entity names exactly |
actor_kinds |
Match actor kinds: user, system, or worker |
actor_user_ids |
Match actor principal ids |
initiator_user_ids |
Match root task initiator principal ids |
request_ids |
Match request UUIDs |
correlation_ids |
Match correlation ids exactly |
task_kinds |
Match recognized task kinds in task-event metadata; missing metadata does not match |
Each field is optional. Empty and omitted fields match all events for that dimension. Multiple populated fields are combined with AND; values inside one field are combined with OR. The filter can only narrow the subscription's collection-scoped visibility. It cannot deliver unrelated collection events to a subscription.
Example sink:
{
"name": "inventory-webhook",
"kind": "webhook",
"config": {
"headers": {
"X-Integration": "inventory"
}
},
"secret_ref": "inventory_webhook",
"enabled": true
}
Example collection subscription:
{
"sink_id": 1,
"name": "collection-lifecycle-to-inventory",
"description": "Send collection lifecycle events to inventory",
"entity_types": ["collection"],
"actions": ["created", "updated", "deleted"],
"filter": {
"actor_kinds": ["user"]
},
"routing": {
"url": "https://inventory.example/hubuum/events"
},
"enabled": true
}
For email sinks, create narrow subscriptions rather than sending every audit
event to human recipients. For example, this subscription sends only failed
task lifecycle events to the configured mailbox. Create it through
POST /api/v1/system-event-subscriptions:
{
"sink_id": 2,
"name": "task-failures-to-ops",
"description": "Email ops when tasks fail",
"entity_types": ["task"],
"actions": ["failed"],
"filter": {
"actor_kinds": ["worker"]
},
"routing": {
"recipients": ["Ops <ops@example.com>"]
},
"enabled": true
}
Both the sink and the subscription must be enabled for matching events to fan out to delivery rows. Disabling either one stops new matching deliveries without deleting historical events or existing delivery rows.
Webhook Sinks¶
Webhook delivery is the reference concrete sink. For a setup walkthrough, see
chat and notification webhooks. By default, a webhook
subscription posts the event envelope as JSON to the URL in its routing object:
The request method is always POST. Hubuum sends the event UUID in both
Idempotency-Key and X-Hubuum-Event-Id, and the JSON body includes the same
event_id field. Consumers should deduplicate by event_id.
Webhook, AMQP, Valkey, and email deliveries all receive the same provenance
object shown above. Actor and initiator names are resolved once for each
claimed delivery batch. A deleted principal keeps its durable ID and has a
null name when no current or tombstoned principal record can be resolved.
Webhook sink config may include static string headers and optional local
delivery limits:
{
"config": {
"headers": {
"X-Integration": "inventory-sync"
},
"timeout_ms": 5000,
"max_response_bytes": 65536,
"max_request_bytes": 1000000
},
"secret_ref": "inventory_webhook"
}
When secret_ref is set, Hubuum resolves the alias through the selected
secret source and sends it as a bearer token. With the
default environment source, the example maps to
HUBUUM_EVENT_SINK_SECRET_INVENTORY_WEBHOOK.
Webhook HTTP execution uses the shared hardened outbound HTTP layer: HTTPS-only URLs, embedded credential rejection, DNS resolution and address screening, IP pinning, redirect refusal, timeout caps, response-size caps, and sensitive response-header redaction.
AMQP Sinks¶
AMQP delivery is available when Hubuum is built with the amqp feature. An
AMQP sink publishes the event envelope as JSON to the exchange in sink
config. AMQP event delivery requires TLS, so configure amqps:// URLs:
{
"config": {
"uri": "amqps://publisher:{secret}@rabbitmq.example/%2f",
"exchange": "hubuum.events",
"exchange_type": "topic",
"declare_exchange": true,
"durable": true,
"mandatory": true,
"max_payload_bytes": 1000000
},
"secret_ref": "rabbitmq_password"
}
When secret_ref is set, the AMQP URI must contain {secret}. Hubuum resolves
the alias through the selected secret source,
percent-encodes the value for URI userinfo use, and substitutes it into the
URI. The default environment source maps this example to
HUBUUM_EVENT_SINK_SECRET_RABBITMQ_PASSWORD.
Literal credentials in sink URIs are rejected; use {secret} plus secret_ref
instead.
The routing key is always {entity_type}.{action}, such as
collection.created. Hubuum sets the AMQP message_id property to the event
UUID and enables publisher confirms for each delivery attempt. Consumers should
deduplicate by event_id or message_id.
Valkey Stream Sinks¶
Valkey stream delivery is available when Hubuum is built with the valkey
feature. Hubuum uses the mature Redis protocol client for this transport, so
the connection URL uses the standard Redis protocol URL form accepted by Redis
and Valkey deployments. Event delivery requires TLS, so configure rediss://
URLs.
The subscription routing object selects the stream key:
The sink config holds the Valkey connection URL and optional stream trim
settings:
{
"config": {
"uri": "rediss://default:{secret}@valkey.example/0",
"max_len": 100000,
"approximate_trim": true,
"max_payload_bytes": 1000000,
"io_timeout_ms": 25000
},
"secret_ref": "valkey_password"
}
When secret_ref is set, the URI must contain {secret}. Hubuum resolves the
alias through the selected secret source, percent-encodes
the value for URI userinfo use, and substitutes it into the URI. The default
environment source maps this example to
HUBUUM_EVENT_SINK_SECRET_VALKEY_PASSWORD.
Literal credentials in sink URIs are rejected; use {secret} plus secret_ref
instead. io_timeout_ms bounds the Redis protocol connection and socket I/O
for the blocking driver call and defaults to 25,000 ms.
Each XADD entry includes discrete fields for event_id, entity_type,
entity_name, action, and actor_kind, plus the full JSON envelope in the
payload field. Consumers should deduplicate by event_id.
Email / SMTP Sinks¶
Email delivery is available when Hubuum is built with the email feature. This
transport is intended for human-facing notifications, while the canonical event
stream remains the source of truth for audit and retry state.
The subscription routing object holds the message recipients:
{
"routing": {
"recipients": ["Ops <ops@example.com>"],
"cc": ["audit@example.com"],
"bcc": ["archive@example.com"]
}
}
to is accepted as an alias for recipients.
The sink config holds the SMTP connection URL, sender, optional reply-to
address, and MiniJinja export templates for the subject and text body:
{
"config": {
"uri": "smtps://hubuum:{secret}@smtp.example.com",
"from": "Hubuum <hubuum@example.com>",
"reply_to": "noreply@example.com",
"max_payload_bytes": 1000000,
"subject_template": "Hubuum {{ entity_type }} {{ action }}: {{ entity_name | default_if_empty(summary) }}",
"body_template": "{{ summary }}\n\nEvent: {{ event_id }}\nEntity: {{ entity_type }}\nAction: {{ action }}\n"
},
"secret_ref": "smtp_password"
}
SMTP URLs must use the TLS smtps:// scheme. Certificate verification uses the
system trust store, including operator-installed private CAs (SSL_CERT_FILE
and SSL_CERT_DIR on supported Unix platforms). When secret_ref is set, the URI
must contain {secret}. Hubuum resolves the alias through the selected
secret source, percent-encodes the value for URI userinfo
use, and substitutes it into the URI. The default environment source maps this
example to HUBUUM_EVENT_SINK_SECRET_SMTP_PASSWORD.
Literal credentials in sink URIs are rejected; use {secret} plus secret_ref
instead.
Template context exposes the event envelope fields at the top level, including
event_id, entity_type, entity_name, action, summary, and
occurred_at, and provenance. The full envelope is also available as
event. Subjects must render to a single non-empty line, and bodies must
render to non-empty text. Webhook max_request_bytes and the transport
max_payload_bytes settings default to 1,000,000 bytes.
Delivery Semantics¶
Delivery is at least once. A successful transport-specific acknowledgement
marks the delivery succeeded; transport errors or failed acknowledgements are
retried with backoff until the configured attempt limit, then marked dead.
By default, webhooks treat any 2xx response as successful and retry non-2xx
responses. Optional response policies can
require an acknowledgement, classify permanent errors, or defer HTTP 429.
Hubuum does not guarantee ordering across events. Consumers that need ordering
should reconcile with occurred_at and the internal monotonic id, while still
deduplicating by event_id.
Operators can inspect delivery rows through GET /api/v1/event-deliveries and
GET /api/v1/event-deliveries/{delivery_id}. Admins can release a failed or
dead delivery with POST /api/v1/event-deliveries/{delivery_id}/retry, or move
a row to the dead-letter state with
POST /api/v1/event-deliveries/{delivery_id}/dead.
Delivery workers are configurable and default-disabled:
HUBUUM_EVENT_FANOUT_WORKERS=1
HUBUUM_EVENT_FANOUT_BATCH_SIZE=100
HUBUUM_EVENT_FANOUT_POLL_INTERVAL_MS=5000
HUBUUM_EVENT_FANOUT_LOCK_TIMEOUT_MS=30000
HUBUUM_EVENT_DELIVERY_WORKERS=0
HUBUUM_EVENT_DELIVERY_BATCH_SIZE=100
HUBUUM_EVENT_DELIVERY_POLL_INTERVAL_MS=5000
HUBUUM_EVENT_DELIVERY_LOCK_TIMEOUT_MS=30000
HUBUUM_EVENT_DELIVERY_TRANSPORT_TIMEOUT_MS=25000
HUBUUM_EVENT_DELIVERY_RETRY_BACKOFF_BASE_MS=1000
HUBUUM_EVENT_DELIVERY_RETRY_BACKOFF_MAX_MS=300000
HUBUUM_EVENT_DELIVERY_MAX_ATTEMPTS=10
Keep delivery workers at 0 when the deployment uses the audit log only or
when sink credentials are not ready. Set HUBUUM_EVENT_DELIVERY_WORKERS above
zero once operators are ready for external transport delivery.
HUBUUM_EVENT_DELIVERY_TRANSPORT_TIMEOUT_MS must be less than
HUBUUM_EVENT_DELIVERY_LOCK_TIMEOUT_MS. Each worker claims at most
min(HUBUUM_EVENT_DELIVERY_BATCH_SIZE, 8) rows, starts them concurrently, and
waits for their acknowledgements before claiming more. Larger batch settings
remain valid upper bounds; claimed deliveries no longer wait through local
execution waves.
Immediately before transport, storage checks the claim token, in-flight status, and expiry. Expired or replaced claims do not start a send. The remaining lease is carried as a conservative monotonic deadline, including time spent waiting for the ownership check. Claim loading, dispatch delays, and the ownership check reduce the time available to transport. The difference between the lock and transport timeouts is reserved for recording success or failure (5 seconds with the defaults). Transport stops at its remaining deadline, and acknowledgment is bounded by lease expiry. Increase the lock timeout if database latency needs a larger acknowledgment allowance. No setting values or validation rules changed.
Claim-token fencing still rejects acknowledgments from replaced workers. A
process interruption or lost acknowledgment after a sink accepts an event can
still cause a retry; cancellation cannot undo an external side effect. Consumers
must continue to deduplicate by event_id under the at-least-once contract.
Workers use PostgreSQL LISTEN/NOTIFY for low-latency wakeups across
processes and fall back to the configured poll intervals for eventual progress.
Event writes notify the fan-out channel only after commit, and fan-out notifies
delivery workers when it creates delivery rows. An idle delivery worker also
wakes at the earliest scheduled retry or expired-claim deadline, so retry
backoffs shorter than the safety poll remain effective.
Operational Health¶
The admin endpoint GET /api/v1/event-deliveries/health returns a delivery
pipeline snapshot for operators and dashboards. It includes:
- Fan-out backlog: undispatched events, in-flight fan-out claims, stale fan-out claims, oldest pending age, worker settings, and notification-versus-poll wakeup counters.
- Delivery backlog: status counts, retryable failed rows, stale delivery claims, oldest due age, worker settings, and notification-versus-poll wakeup counters.
- Per-sink and per-subscription delivery counts, stale claims, retryable rows, enabled flags, and oldest due age.
Use the fields together to distinguish common failure modes:
fanout.pending_events > 0with risingoldest_pending_age_secondsmeans events are being written but fan-out is not keeping up.- Fan-out
stale_claims > 0means a worker claimed events but did not clear them before the lock expired. delivery.counts.pendingordelivery.counts.retryablegrowing while fan-out is clear means delivery workers are not keeping up or are disabled.delivery.stale_claims > 0means a delivery worker claimed rows and did not finish before the lock expired.- Per-sink
failed,dead, orretryablegrowth with other sinks healthy usually points to a sink configuration, credential, endpoint, or broker problem. - Wakeup
notifications_sentincreasing whilenotification_wakeupsstays flat and backlog only drains onpoll_wakeupspoints to worker wakeup trouble or missing workers.
Alert thresholds are deployment-specific, but a practical baseline is to page
when fan-out or delivery oldest due age exceeds the poll interval by several
minutes, when stale claims remain non-zero across multiple lock-timeout
windows, or when dead-letter counts grow for a production sink. First confirm
worker counts and lock/poll settings in the health response, then inspect the
affected delivery rows, fix the sink or worker condition, and use the retry
admin action for rows that should be released from failed or dead.
Retention And Archival¶
Event retention purge is available as an operational worker, but is disabled by default because it deletes audit rows. Enable it only after choosing retention windows that match the deployment's audit requirements:
HUBUUM_EVENT_RETENTION_PURGE_ENABLED=true
HUBUUM_EVENT_RETENTION_DAYS=365
HUBUUM_EVENT_DELIVERY_RETENTION_DAYS=30
HUBUUM_EVENT_RETENTION_PURGE_INTERVAL_SECONDS=3600
HUBUUM_EVENT_RETENTION_PURGE_BATCH_SIZE=1000
The purge path is the only application path allowed to bypass the append-only
events trigger. It sets the transaction-local events.allow_purge guard
before deleting eligible event rows. Normal DELETE statements against
events continue to fail.
Worker replicas coordinate claims with a PostgreSQL advisory lock. A claim transaction persists an immutable batch ID, the exact selected event IDs and documents, and the bounded terminal-delivery cutoff; it does not delete the events. Archive I/O then runs outside the database transaction. A completion transaction locks the same claim, deletes exactly its event IDs, performs the bounded terminal-delivery cleanup, and records the final counts. A partial index on terminal delivery age keeps that lookup bounded to its eligible queue.
If archival fails or the process exits, the pending claim and source events remain durable. The next worker receives the same batch ID and documents. Archives must therefore be idempotent by batch ID, and completion is also idempotent. A purge-count mismatch, malformed claimed document, maintenance transition, or busy coordinator is an error rather than a successful empty batch.
Events become purge-eligible after HUBUUM_EVENT_RETENTION_DAYS, but the purge
will not delete an event while it has active pending, failed, or
in_flight deliveries. Deleting an eligible event cascades to its remaining
delivery rows. Terminal succeeded and dead delivery rows are also purged
independently after HUBUUM_EVENT_DELIVERY_RETENTION_DAYS, using their
updated_at timestamp so retention starts when the delivery reaches its
terminal state.
Local file archival is opt-in. Most deployments should export or consume audit events through the database or event sinks instead of writing container-local files. Enable local JSON Lines archival only when the configured directory is durable and access-controlled:
HUBUUM_EVENT_RETENTION_FILE_ARCHIVE_ENABLED=true
HUBUUM_EVENT_RETENTION_ARCHIVE_PATH=/var/lib/hubuum/event-archive
Each claim is written atomically to <batch-uuid>.jsonl. Every line contains
retention_batch_id, archived_at, and the full event row, including durable
initiator_user_id and task_id values. Event schema_version is 2 for
revision-aware mutation events; older stored events remain readable as version
1 with null revision fields. If the final file already exists, the worker
accepts it only after validating its batch ID, line count, and exact event
documents against the claim; any mismatch is fatal.
On Unix, the archive directory is a real non-symlink directory restricted to
mode 0700. Temporary and final batch files are regular non-symlink files with
mode 0600. The worker flushes and synchronizes file contents before atomic
rename, then synchronizes the directory entry before acknowledging archival.
Schema lifecycle and object validation¶
class_schema events record staging, abandonment, activation, work requests,
cancellation, and deletion. Activation metadata binds the old/new revisions,
policy, immediate population effect, revalidation task, and dependent rebuild.
object_validation uses succeeded, failed, and updated for committed
valid, mismatched, and not-required findings. Impact failures are advisory and
explicitly carry compliance_changed: false.
These events are also audit entries in the canonical stream. Their documents contain schema/object identities and safe categories, without object values or raw validator messages. They commit with evidence and checkpoint updates; rolled-back writes and rejected leases publish neither. The activation event records immediate pending/not-required effects; per-object findings follow in bounded batches. See class schema evolution.
Operator monitoring¶
Use the shared operator package for Grafana dashboards, Prometheus recording and alerting rules, SLO definitions and response runbooks. The same assets work with the optional single-host stack, independently managed Prometheus/Grafana installations, and Prometheus Operator. Pin the package to your server release and scrape every process directly with deployment labels.
Configurable Webhook Notifications¶
The webhook sink can send the original event envelope or render a custom JSON
payload. Slack, Mattermost, and notification bridges use this same transport;
there are no provider-specific sink kinds or bot API clients. Events remain raw
facts: no threshold evaluator, aggregation, or alert-state machine is added.
System subscriptions and task_kinds filters can select existing failed-backup
events. Metrics alerting systems remain appropriate for queue delay and database
pressure unless a component emits a corresponding raw event.
Payload Templates And Secrets¶
Set config.body_template to a MiniJinja template that renders valid JSON.
Omitting it preserves the original event envelope. Templates expose the same
bounded event context as email: envelope fields at the top level and the full
original envelope as event. Use tojson for inserted values so quotes,
newlines, and Unicode remain valid JSON. Templates share the isolated execution
engine with fuel, recursion, context, and output limits. The rendered request
also respects max_request_bytes and the global outbound request limit.
The additional context fields test and test_marker identify test deliveries.
test_marker is [TEST] followed by a space for tests and an empty string for normal delivery.
Templates choose where to display it; Hubuum does not inject fields into the
receiver's payload. Test sends also carry X-Hubuum-Delivery-Purpose: test.
For a destination containing credentials, set config.url_secret_ref to an
alias in the configured secret source. Its value must be
the complete HTTPS URL. This is independent of the sink's existing
secret_ref, which continues to supply a bearer token. Neither credential is
available to templates or previews. A secret-backed destination requires
omitting routing.url; subscriptions cannot override that destination.
Without url_secret_ref, the subscription's routing.url works as before.
HTTPS validation, DNS screening, private-target policy, timeouts, response caps,
and redirect refusal apply equally to literal and secret-backed URLs. Requests
remain JSON POSTs; templates cannot change transport, headers, or credentials.
Slack And Mattermost Incoming Webhook Example¶
Follow Set Up Chat And Notification Webhooks for provider setup, secret storage, complete sink and subscription requests, and preview/test delivery. It includes separate Slack and Mattermost instructions, plus Discord and Apprise bridge recipes.
All examples use kind: "webhook". Keep provider credentials in the secret
source, use tojson for dynamic payload values, and select an acknowledgement
policy matching the destination. No provider-specific sink or bot client is
required.
Response And Delivery Policies¶
Without config.response, webhooks retain their existing behavior: every 2xx
is successful and every other HTTP status retries with backoff. Response rules
are optional and declarative:
| Field | Meaning |
|---|---|
success_statuses |
Explicit successful 2xx statuses; omitted or empty accepts all 2xx |
retry_statuses |
Non-success 3xx/4xx/5xx statuses to retry; omitted retries all failures, while an empty list makes all HTTP failures permanent |
rate_limit |
When true, HTTP 429 defers without consuming failure attempts, taking precedence over retry_statuses; defaults to false |
body |
Optional acknowledgement check, applied only after a successful status |
For body, use {"kind":"text_equals","value":"ok"} to compare trimmed
response text, or {"kind":"json_equals","pointer":"/ok","value":true}
to compare a JSON value using an RFC 6901 pointer. A missing value, invalid JSON,
or failed comparison is a permanent failure. Response bodies that reach the
configured capture limit cannot be safely checked for truncation and retry
instead. Status errors and failed acknowledgements never include response
contents or destination URLs. These rules do not interpret provider error names.
When rate_limit is enabled, valid Retry-After delta seconds or HTTP dates
control the cooldown; invalid or absent values use 60 seconds. Transport failures
remain retryable. Permanent failures dead-letter immediately. All sinks remain
unlimited unless delivery_policy.min_interval_ms is configured. Values range
from 1 to 86,400,000 milliseconds; null disables configured spacing.
The persisted schedule is shared across workers and subscriptions using a sink.
It spaces sends without dropping or aggregating events. Deferred deliveries stay
pending with deferred_reason (configured_rate or provider_rate) and
next_attempt_at, without consuming failure attempts. Separate sinks do not
share their schedules, even if they target the same external channel.
Delivery remains at least once: a received request followed by a lost
acknowledgement can produce a duplicate. Custom payloads may include event_id
for receiver deduplication; X-Hubuum-Event-Id always identifies the source event.
Normal deliveries keep the event UUID as Idempotency-Key. Queued tests use
{event_uuid}-test-{delivery_id}, stable across retries and distinct for each
test, so tests cannot suppress normal sends at receivers using that header.
Preview And Test¶
Administrators can preview or enqueue a webhook test using an existing event UUID and saved subscription. Preview resolves no secrets and performs no HTTP request; it returns only the JSON payload and sink kind:
POST /api/v1/event-sinks/3/preview
Content-Type: application/json
{"subscription_id": 7, "event_id": "00000000-0000-4000-8000-000000000001"}
Use /api/v1/event-sinks/3/test with the same body to enqueue a delivery.
Replace the example identifiers with real saved records. Both verify sink and
event scope. Tests bypass filters and enabled flags for setup but respect
throttling. Preview renders the same test context as delivery. Include
test_marker in templates when a visible message label is wanted.
The test route returns 202 with an inspectable delivery and Location header.
The request is audited as event_sink.invoked. Repeated tests create separate
purpose: "test" records without suppressing normal event delivery. Workers
render using the saved configuration at execution time.
Upgrade And Rollback¶
Stop older API and worker processes, apply
2026-10-01-000001_webhook_notifications, reconcile database role grants, and
start matching binaries. Mixed old/new workers are unsupported because normal
and test deliveries now use purpose-aware uniqueness.
Binary-only rollback to 0.0.16 is unsupported: its event worker's insert conflict
constraint no longer exists after this migration. Before upgrading, stop all
writers and take a PostgreSQL snapshot, retaining the old binaries and
credentials. For recovery, stop the candidate, restore that snapshot, and start
matching old APIs, workers, and restore executors. Writes after the snapshot
are lost. The single-host updater selects downtime using the candidate's
hubuum-admin --migration-mode preflight and leaves old binaries stopped if
migration fails. The updater does not create the required snapshot for you.
Manual schema rollback is a separate, destructive operation. Its guard rejects
enhanced webhook configuration, system subscriptions, rate
policies, and test/deferred deliveries. Archive and remove retained
event_sink.invoked audit records and their dependent deliveries with processes
stopped before rollback. Format 7 backups preserve these settings and terminal
delivery history, while transient scheduling resets on restore. Format 6
backups restore legacy defaults. Remove enhanced webhook settings before using
older binaries, which cannot interpret their request or response rules.