brokkr-models::models::webhooks
Rust
Webhook models for event notifications.
This module provides models for webhook subscriptions and deliveries, enabling external systems to receive notifications when events occur in Brokkr.
Structs
Section titled “Structs”struct BrokkrEvent
Section titled “struct BrokkrEvent”pub
Derives: Debug, Clone, Serialize, Deserialize, ToSchema
A Brokkr event that can trigger webhook deliveries.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
id |
Uuid |
Unique identifier for this event (idempotency key). |
event_type |
String |
Event type (e.g., “deployment.applied”). |
timestamp |
DateTime<Utc> |
When the event occurred. |
data |
serde_json::Value |
Event-specific data. |
Methods
Section titled “Methods”pub
fn new(event_type: &str, data: serde_json::Value) -> SelfCreates a new event.
Source
pub fn new(event_type: &str, data: serde_json::Value) -> Self { Self { id: Uuid::new_v4(), event_type: event_type.to_string(), timestamp: Utc::now(), data, } }struct WebhookFilters
Section titled “struct WebhookFilters”pub
Derives: Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema
Filters for webhook subscriptions.
A filter narrows an event-type match further, using field values carried in
the event payload. Evaluated by WebhookFilters::matches at emission time
(see brokkr_broker::utils::event_bus::emit_event).
Semantics (BROKKR-T-0288):
- Every filter field that is set must match: the fields are ANDed.
- An event that does not carry a filtered field does not match. A
subscription filtering on
stack_idtherefore receives nothing for event types whose payload has nostack_id(agent.*andworkorder.*; the wholedeployment.*family does carry it). This is deliberate: a filter is a narrowing statement, and widening it to “…or the event didn’t say” would deliver precisely the events the operator asked to exclude. - A JSON
nullcounts as absent, not as a value. Several payloads emit"stack_id": null/"agent_id": nullwhen the source column is NULL (e.g.deployment.deletedfor an already-purged object,workorder.completedfor an unclaimed order).
Read/write asymmetry
This type is the read path: deserialization deliberately ignores unknown
keys, so rows written before a field was removed still load — notably the
dropped labels filter, which was never evaluated and is superseded by
target_labels delivery routing. A legacy row must keep delivering exactly
as it does today; a broker that refused to load it would silently stop the
subscription instead. Do not add deny_unknown_fields here.
The write path is strict, and intentionally not symmetric: POST and
PUT /webhooks reject a body carrying filters.labels with a 422 that names
target_labels as the replacement (see
brokkr_broker::api::v1::webhooks::reject_removed_write_fields). Accepting
the key and dropping it would leave the caller believing their deliveries
were scoped when they were not — the failure mode that motivated
BROKKR-T-0288 in the first place. Rejection is enforced on the raw request
body, above this type, precisely so the tolerant deserialization below is
preserved.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
agent_id |
Option<Uuid> |
Filter by specific agent ID. |
stack_id |
Option<Uuid> |
Filter by specific stack ID. |
Methods
Section titled “Methods”is_empty
Section titled “is_empty”pub
fn is_empty(&self) -> boolReturns true when no filter field is set, i.e. the filter matches every event of a subscribed type.
A legacy row carrying only the removed labels key deserializes to this.
Source
pub fn is_empty(&self) -> bool { self.agent_id.is_none() && self.stack_id.is_none() }matches
Section titled “matches”pub
fn matches(&self, event: &BrokkrEvent) -> boolEvaluates this filter against an event.
Returns true when every set field is present in event.data and equal
to the filter’s value. See the type-level docs for the absent-field rule.
Source
pub fn matches(&self, event: &BrokkrEvent) -> bool { if let Some(agent_id) = self.agent_id && !payload_uuid_eq(&event.data, "agent_id", agent_id) { return false; } if let Some(stack_id) = self.stack_id && !payload_uuid_eq(&event.data, "stack_id", stack_id) { return false; } true }struct WebhookSubscription
Section titled “struct WebhookSubscription”pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A webhook subscription record from the database.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
id |
Uuid |
Unique identifier for the subscription. |
name |
String |
Human-readable name for the subscription. |
url_encrypted |
Vec<u8> |
Encrypted webhook URL. |
auth_header_encrypted |
Option<Vec<u8>> |
Encrypted Authorization header value. |
event_types |
Vec<Option<String>> |
Event types to subscribe to (supports wildcards like “deployment.*”). |
filters |
Option<String> |
JSON-encoded filters. |
target_labels |
Option<Vec<Option<String>>> |
Labels for delivery targeting (NULL = broker delivers). |
enabled |
bool |
Whether the subscription is active. |
max_retries |
i32 |
Maximum delivery retry attempts. |
timeout_seconds |
i32 |
HTTP request timeout in seconds. |
created_at |
DateTime<Utc> |
When the subscription was created. |
updated_at |
DateTime<Utc> |
When the subscription was last updated. |
created_by |
Option<String> |
Who created the subscription. |
struct NewWebhookSubscription
Section titled “struct NewWebhookSubscription”pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new webhook subscription to be inserted.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
name |
String |
Human-readable name. |
url_encrypted |
Vec<u8> |
Encrypted webhook URL. |
auth_header_encrypted |
Option<Vec<u8>> |
Encrypted Authorization header value. |
event_types |
Vec<Option<String>> |
Event types to subscribe to. |
filters |
Option<String> |
JSON-encoded filters. |
target_labels |
Option<Vec<Option<String>>> |
Labels for delivery targeting (NULL = broker delivers). |
enabled |
bool |
Whether the subscription is active (defaults to true). |
max_retries |
i32 |
Maximum retry attempts (defaults to 5). |
timeout_seconds |
i32 |
HTTP timeout in seconds (defaults to 30). |
created_by |
Option<String> |
Who created the subscription. |
Methods
Section titled “Methods”pub
fn new( name: String, url_encrypted: Vec<u8>, auth_header_encrypted: Option<Vec<u8>>, event_types: Vec<String>, filters: Option<WebhookFilters>, target_labels: Option<Vec<String>>, created_by: Option<String>,) -> Result<Self, String>Creates a new webhook subscription.
Parameters:
| Name | Type | Description |
|---|---|---|
name |
String |
Human-readable name for the subscription. |
url_encrypted |
Vec<u8> |
Pre-encrypted webhook URL. |
auth_header_encrypted |
Option<Vec<u8>> |
Pre-encrypted Authorization header (optional). |
event_types |
Vec<String> |
List of event types to subscribe to. |
filters |
Option<WebhookFilters> |
Optional filters as WebhookFilters struct. |
target_labels |
Option<Vec<String>> |
Optional labels for delivery targeting. |
created_by |
Option<String> |
Who is creating the subscription. |
Returns:
A Result containing the new subscription or an error.
Source
pub fn new( name: String, url_encrypted: Vec<u8>, auth_header_encrypted: Option<Vec<u8>>, event_types: Vec<String>, filters: Option<WebhookFilters>, target_labels: Option<Vec<String>>, created_by: Option<String>, ) -> Result<Self, String> { // Validate name if name.trim().is_empty() { return Err("Name cannot be empty".to_string()); } if name.len() > 255 { return Err("Name cannot exceed 255 characters".to_string()); }
// Validate event types if event_types.is_empty() { return Err("At least one event type is required".to_string()); }
// Serialize filters to JSON if provided let filters_json = filters .map(|f| serde_json::to_string(&f)) .transpose() .map_err(|e| format!("Failed to serialize filters: {}", e))?;
Ok(Self { name, url_encrypted, auth_header_encrypted, event_types: event_types.into_iter().map(Some).collect(), filters: filters_json, target_labels: target_labels.map(|labels| labels.into_iter().map(Some).collect()), enabled: true, max_retries: 5, timeout_seconds: 30, created_by, }) }struct UpdateWebhookSubscription
Section titled “struct UpdateWebhookSubscription”pub
Derives: Debug, Clone, Default, AsChangeset
Changeset for updating a webhook subscription.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
name |
Option<String> |
New name. |
url_encrypted |
Option<Vec<u8>> |
New encrypted URL. |
auth_header_encrypted |
Option<Option<Vec<u8>>> |
New encrypted auth header. |
event_types |
Option<Vec<Option<String>>> |
New event types. |
filters |
Option<Option<String>> |
New filters. |
target_labels |
Option<Option<Vec<Option<String>>>> |
New target labels for delivery. |
enabled |
Option<bool> |
Enable/disable. |
max_retries |
Option<i32> |
New max retries. |
timeout_seconds |
Option<i32> |
New timeout. |
struct WebhookDelivery
Section titled “struct WebhookDelivery”pub
Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema
A webhook delivery record from the database.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
id |
Uuid |
Unique identifier for the delivery. |
subscription_id |
Uuid |
The subscription this delivery belongs to. |
event_type |
String |
The event type being delivered. |
event_id |
Uuid |
The event ID (idempotency key). |
payload |
String |
JSON-encoded event payload. |
target_labels |
Option<Vec<Option<String>>> |
Labels for delivery targeting (copied from subscription). |
status |
String |
Delivery status: pending, acquired, success, failed, dead. |
acquired_by |
Option<Uuid> |
Agent ID that acquired this delivery (NULL = broker). |
acquired_until |
Option<DateTime<Utc>> |
TTL for the acquisition - release if exceeded. |
attempts |
i32 |
Number of delivery attempts. |
last_attempt_at |
Option<DateTime<Utc>> |
When the last delivery attempt was made. |
next_retry_at |
Option<DateTime<Utc>> |
When to retry after failure. |
last_error |
Option<String> |
Error message from last failed attempt. |
created_at |
DateTime<Utc> |
When the delivery was created. |
completed_at |
Option<DateTime<Utc>> |
When the delivery completed (success or dead). |
struct NewWebhookDelivery
Section titled “struct NewWebhookDelivery”pub
Derives: Debug, Clone, Insertable, Serialize, Deserialize
A new webhook delivery to be inserted.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
subscription_id |
Uuid |
The subscription to deliver to. |
event_type |
String |
The event type. |
event_id |
Uuid |
The event ID. |
payload |
String |
JSON-encoded payload. |
target_labels |
Option<Vec<Option<String>>> |
Labels for delivery targeting (copied from subscription). |
status |
String |
Initial status (pending). |
Methods
Section titled “Methods”pub
fn new( subscription_id: Uuid, event: &BrokkrEvent, target_labels: Option<Vec<Option<String>>>,) -> Result<Self, String>Creates a new webhook delivery.
Parameters:
| Name | Type | Description |
|---|---|---|
subscription_id |
Uuid |
The subscription to deliver to. |
event |
&BrokkrEvent |
The event to deliver. |
target_labels |
Option<Vec<Option<String>>> |
Labels for delivery targeting (from subscription). |
Returns:
A Result containing the new delivery or an error.
Source
pub fn new( subscription_id: Uuid, event: &BrokkrEvent, target_labels: Option<Vec<Option<String>>>, ) -> Result<Self, String> { if subscription_id.is_nil() { return Err("Subscription ID cannot be nil".to_string()); }
let payload = serde_json::to_string(event) .map_err(|e| format!("Failed to serialize event: {}", e))?;
Ok(Self { subscription_id, event_type: event.event_type.clone(), event_id: event.id, payload, target_labels, status: DELIVERY_STATUS_PENDING.to_string(), }) }struct UpdateWebhookDelivery
Section titled “struct UpdateWebhookDelivery”pub
Derives: Debug, Clone, Default, AsChangeset
Changeset for updating a webhook delivery.
Fields
Section titled “Fields”| Name | Type | Description |
|---|---|---|
status |
Option<String> |
New status. |
acquired_by |
Option<Option<Uuid>> |
Agent that acquired this delivery. |
acquired_until |
Option<Option<DateTime<Utc>>> |
TTL for the acquisition. |
attempts |
Option<i32> |
Increment attempts. |
last_attempt_at |
Option<DateTime<Utc>> |
When last attempted. |
next_retry_at |
Option<Option<DateTime<Utc>>> |
When to retry. |
last_error |
Option<Option<String>> |
Error message. |
completed_at |
Option<DateTime<Utc>> |
When completed. |
Constants
Section titled “Constants”| Name | Type | Value | Description |
|---|---|---|---|
DELIVERY_STATUS_PENDING |
&str |
"pending" |
Valid delivery statuses |
DELIVERY_STATUS_ACQUIRED |
&str |
"acquired" |
|
DELIVERY_STATUS_SUCCESS |
&str |
"success" |
|
DELIVERY_STATUS_FAILED |
&str |
"failed" |
|
DELIVERY_STATUS_DEAD |
&str |
"dead" |
|
VALID_DELIVERY_STATUSES |
&[&str] |
&[DELIVERY_STATUS_PENDING, DELIVERY_STATUS_ACQUIRED, DELI... |
|
EVENT_AGENT_REGISTERED |
&str |
"agent.registered" |
|
EVENT_AGENT_DEREGISTERED |
&str |
"agent.deregistered" |
|
EVENT_STACK_CREATED |
&str |
"stack.created" |
|
EVENT_STACK_DELETED |
&str |
"stack.deleted" |
|
EVENT_DEPLOYMENT_CREATED |
&str |
"deployment.created" |
|
EVENT_DEPLOYMENT_APPLIED |
&str |
"deployment.applied" |
|
EVENT_DEPLOYMENT_FAILED |
&str |
"deployment.failed" |
|
EVENT_DEPLOYMENT_DELETED |
&str |
"deployment.deleted" |
|
EVENT_WORKORDER_CREATED |
&str |
"workorder.created" |
|
EVENT_WORKORDER_CLAIMED |
&str |
"workorder.claimed" |
|
EVENT_WORKORDER_COMPLETED |
&str |
"workorder.completed" |
|
EVENT_WORKORDER_FAILED |
&str |
"workorder.failed" |
|
VALID_EVENT_TYPES |
&[&str] |
&[EVENT_AGENT_REGISTERED, EVENT_AGENT_DEREGISTERED, EVENT... |