Webhooks and Domain Events
Spotlight publishes domain events to an Amazon EventBridge custom event bus. These events represent meaningful state changes in the system and can be consumed by external services via EventBridge rules, Lambda targets, or webhook integrations.
Event Bus
All events are published to the spotlight-events EventBridge bus (configurable via EVENT_BUS_NAME).
Source: "spotlight"
EventBusName: "spotlight-events"Events are archived for 30 days by default for replay capability.
Event Envelope
Every event follows the EventBridge entry format with a consistent detail structure:
{
"Source": "spotlight",
"DetailType": "TourCompleted",
"EventBusName": "spotlight-events",
"Detail": {
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "TourCompleted",
"category": "user_interaction",
"tenant_id": "tenant_abc",
"timestamp": "2025-05-10T14:22:00+00:00",
"data": {
"tour_id": "tour_456",
"user_id": "user_123",
"step_id": null
},
"metadata": {
"user_id": "user_123"
}
}
}Envelope Fields
| Field | Type | Description |
|---|---|---|
event_id | string (UUID) | Unique event identifier |
event_type | string | Event name (e.g., TourCompleted) |
category | string | Event category for routing |
tenant_id | string | Tenant scope |
timestamp | string (ISO 8601) | When the event occurred |
data | object | Event-specific payload |
metadata | object | Additional context (user ID, session ID, etc.) |
Event Categories
Events are grouped into four categories used for EventBridge rule routing:
| Category | Description | Example Events |
|---|---|---|
tour_lifecycle | Tour state changes (created, published, archived) | TourCreated, TourPublished |
content_lifecycle | Content state changes | ContentCreated, ContentPublished |
user_interaction | End-user actions (views, completions, dismissals) | TourStarted, StepViewed |
admin | Admin actions (audit trail) | TourCreatedByAdmin, ThemeUpdatedByAdmin |
Tour Lifecycle Events
TourCreated
Fired when a new tour is created via the admin API.
{
"event_type": "TourCreated",
"category": "tour_lifecycle",
"data": {
"tour_id": "tour_456",
"title": "Getting Started",
"step_count": 5
}
}TourPublished
Fired when a tour is published, creating a new version.
{
"event_type": "TourPublished",
"category": "tour_lifecycle",
"data": {
"tour_id": "tour_456",
"version": 3,
"published_by": "user_admin_1"
}
}TourArchived
Fired when a tour is archived, removing it from user-facing content.
{
"event_type": "TourArchived",
"category": "tour_lifecycle",
"data": {
"tour_id": "tour_456",
"archived_by": "user_admin_1"
}
}TourVersionCreated
Fired when a new version snapshot is created.
{
"event_type": "TourVersionCreated",
"category": "tour_lifecycle",
"data": {
"tour_id": "tour_456",
"version": 4,
"previous_version": 3
}
}User Interaction Events
TourStarted
{
"event_type": "TourStarted",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123"
},
"metadata": { "user_id": "user_123" }
}StepViewed
{
"event_type": "StepViewed",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123",
"step_id": "step_2"
},
"metadata": { "user_id": "user_123" }
}StepCompleted
{
"event_type": "StepCompleted",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123",
"step_id": "step_2"
},
"metadata": { "user_id": "user_123" }
}TourCompleted
{
"event_type": "TourCompleted",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123"
},
"metadata": { "user_id": "user_123" }
}TourDismissed
{
"event_type": "TourDismissed",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123",
"step_id": "step_3"
},
"metadata": { "user_id": "user_123" }
}TourRestarted
{
"event_type": "TourRestarted",
"category": "user_interaction",
"data": {
"tour_id": "tour_456",
"user_id": "user_123"
},
"metadata": { "user_id": "user_123" }
}Content Events
ContentCreated / ContentPublished / ContentArchived
Same structure as tour lifecycle events, substituting content_id for tour_id.
ContentViewed
{
"event_type": "ContentViewed",
"category": "user_interaction",
"data": {
"content_id": "cnt_789",
"user_id": "user_123",
"content_type": "tooltip"
},
"metadata": { "user_id": "user_123" }
}ContentDismissed
{
"event_type": "ContentDismissed",
"category": "user_interaction",
"data": {
"content_id": "cnt_789",
"user_id": "user_123",
"content_type": "tooltip"
},
"metadata": { "user_id": "user_123" }
}ContentCtaClicked
{
"event_type": "ContentCtaClicked",
"category": "user_interaction",
"data": {
"content_id": "cnt_789",
"user_id": "user_123",
"content_type": "tooltip",
"cta_url": "/docs/reports"
},
"metadata": { "user_id": "user_123" }
}Admin Audit Events
Admin actions include before/after snapshots for full change tracking:
{
"event_type": "TourUpdatedByAdmin",
"category": "admin",
"source": "admin_api",
"data": {
"actor_id": "user_admin_1",
"actor_email": "admin@example.com",
"entity_type": "tour",
"entity_id": "tour_456",
"operation": "update",
"before": {
"title": "Old Title",
"description": "Old description"
},
"after": {
"title": "New Title",
"description": "Updated description"
}
},
"metadata": {
"actor_id": "user_admin_1",
"entity_type": "tour",
"entity_id": "tour_456",
"session_id": "sess_abc123"
}
}Admin Event Types
| Event Type | Entity | Operation |
|---|---|---|
TourCreatedByAdmin | tour | create |
TourUpdatedByAdmin | tour | update |
TourPublishedByAdmin | tour | publish |
TourArchivedByAdmin | tour | archive |
TourDuplicatedByAdmin | tour | duplicate |
TourVersionRolledBackByAdmin | tour | rollback |
ContentCreatedByAdmin | content | create |
ContentUpdatedByAdmin | content | update |
ContentPublishedByAdmin | content | publish |
ContentArchivedByAdmin | content | archive |
ThemeCreatedByAdmin | theme | create |
ThemeUpdatedByAdmin | theme | update |
ThemeDeletedByAdmin | theme | delete |
ThemeSetDefaultByAdmin | theme | set_default |
AdminUserAddedByAdmin | admin_user | add |
AdminUserUpdatedByAdmin | admin_user | update |
AdminUserRemovedByAdmin | admin_user | remove |
AudienceCreatedByAdmin | audience | create |
AudienceUpdatedByAdmin | audience | update |
AudienceDeletedByAdmin | audience | delete |
CircuitBreakerToggledByAdmin | config | toggle |
EventBridge Rules
DetailType values are the raw event class names: TourCompleted, ContentDismissed, AdminUserAddedByAdmin, and so on. Match them by exact name (or by list) when authoring EventBridge rules.
# Match a specific event class
event_pattern = jsonencode({
source = ["spotlight"]
detail-type = ["TourCompleted"]
})# Match several admin events
event_pattern = jsonencode({
source = ["spotlight"]
detail-type = [
"AdminUserAddedByAdmin",
"AdminUserRoleChangedByAdmin",
"AdminUserRemovedByAdmin"
]
})Dead Letter Queue
Failed event deliveries are routed to an SQS dead letter queue (spotlight-events-dlq) with a 14-day retention period. Monitor this queue for delivery failures.
Subscribing to Events
Lambda Target
Add a new EventBridge rule and Lambda target via Terraform:
resource "aws_cloudwatch_event_rule" "custom_rule" {
name = "spotlight-custom-webhook"
event_bus_name = module.eventbridge.bus_name
event_pattern = jsonencode({
source = ["spotlight"]
detail-type = ["TourCompleted", "TourDismissed"]
})
}
resource "aws_cloudwatch_event_target" "webhook_lambda" {
rule = aws_cloudwatch_event_rule.custom_rule.name
event_bus_name = module.eventbridge.bus_name
target_id = "custom-webhook"
arn = aws_lambda_function.webhook_handler.arn
dead_letter_config {
arn = module.eventbridge.dlq_arn
}
}API Destination (HTTP Webhook)
EventBridge supports API Destinations for sending events to external HTTP endpoints:
resource "aws_cloudwatch_event_api_destination" "slack_webhook" {
name = "spotlight-slack-notifications"
invocation_endpoint = "https://hooks.slack.com/services/..."
http_method = "POST"
invocation_rate_limit_per_second = 10
connection_arn = aws_cloudwatch_event_connection.slack.arn
}Batch Publishing
The EventBridgePublisher automatically chunks event batches to respect the EventBridge limit of 10 entries per PutEvents call:
publisher = EventBridgePublisher(events_client, bus_name="spotlight-events")
# Single event
await publisher.publish(event)
# Batch -- automatically chunked into groups of 10
await publisher.publish_batch(events)Outbox Pattern
Events are not published directly to EventBridge from the API. Instead, they follow the transactional outbox pattern:
- The API writes the event to
Spotlight.Events.Outboxin the same DynamoDB transaction as the state change. - An outbox processor (Lambda in production, background task in local dev) polls for
pendingevents. - Pending events are published to EventBridge.
- Successfully published events are marked as
processed(with TTL for cleanup).
This guarantees at-least-once delivery even if EventBridge is temporarily unavailable.