Skip to content

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.

pub

Derives: Debug, Clone, Serialize, Deserialize, ToSchema

A Brokkr event that can trigger webhook deliveries.

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.

pub

fn new(event_type: &str, data: serde_json::Value) -> Self

Creates 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,
}
}

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_id therefore receives nothing for event types whose payload has no stack_id (agent.* and workorder.*; the whole deployment.* 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 null counts as absent, not as a value. Several payloads emit "stack_id": null / "agent_id": null when the source column is NULL (e.g. deployment.deleted for an already-purged object, workorder.completed for 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.

Name Type Description
agent_id Option<Uuid> Filter by specific agent ID.
stack_id Option<Uuid> Filter by specific stack ID.

pub

fn is_empty(&self) -> bool

Returns 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()
}

pub

fn matches(&self, event: &BrokkrEvent) -> bool

Evaluates 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
}

pub

Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema

A webhook subscription record from the database.

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.

pub

Derives: Debug, Clone, Insertable, Serialize, Deserialize

A new webhook subscription to be inserted.

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.

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,
})
}

pub

Derives: Debug, Clone, Default, AsChangeset

Changeset for updating a webhook subscription.

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.

pub

Derives: Debug, Clone, Queryable, Selectable, Identifiable, Serialize, Deserialize, ToSchema

A webhook delivery record from the database.

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

pub

Derives: Debug, Clone, Insertable, Serialize, Deserialize

A new webhook delivery to be inserted.

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

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(),
})
}

pub

Derives: Debug, Clone, Default, AsChangeset

Changeset for updating a webhook delivery.

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