Skip to content

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:

json
{
  "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

FieldTypeDescription
event_idstring (UUID)Unique event identifier
event_typestringEvent name (e.g., TourCompleted)
categorystringEvent category for routing
tenant_idstringTenant scope
timestampstring (ISO 8601)When the event occurred
dataobjectEvent-specific payload
metadataobjectAdditional context (user ID, session ID, etc.)

Event Categories

Events are grouped into four categories used for EventBridge rule routing:

CategoryDescriptionExample Events
tour_lifecycleTour state changes (created, published, archived)TourCreated, TourPublished
content_lifecycleContent state changesContentCreated, ContentPublished
user_interactionEnd-user actions (views, completions, dismissals)TourStarted, StepViewed
adminAdmin actions (audit trail)TourCreatedByAdmin, ThemeUpdatedByAdmin

Tour Lifecycle Events

TourCreated

Fired when a new tour is created via the admin API.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "event_type": "TourVersionCreated",
  "category": "tour_lifecycle",
  "data": {
    "tour_id": "tour_456",
    "version": 4,
    "previous_version": 3
  }
}

User Interaction Events

TourStarted

json
{
  "event_type": "TourStarted",
  "category": "user_interaction",
  "data": {
    "tour_id": "tour_456",
    "user_id": "user_123"
  },
  "metadata": { "user_id": "user_123" }
}

StepViewed

json
{
  "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

json
{
  "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

json
{
  "event_type": "TourCompleted",
  "category": "user_interaction",
  "data": {
    "tour_id": "tour_456",
    "user_id": "user_123"
  },
  "metadata": { "user_id": "user_123" }
}

TourDismissed

json
{
  "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

json
{
  "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

json
{
  "event_type": "ContentViewed",
  "category": "user_interaction",
  "data": {
    "content_id": "cnt_789",
    "user_id": "user_123",
    "content_type": "tooltip"
  },
  "metadata": { "user_id": "user_123" }
}

ContentDismissed

json
{
  "event_type": "ContentDismissed",
  "category": "user_interaction",
  "data": {
    "content_id": "cnt_789",
    "user_id": "user_123",
    "content_type": "tooltip"
  },
  "metadata": { "user_id": "user_123" }
}

ContentCtaClicked

json
{
  "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:

json
{
  "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 TypeEntityOperation
TourCreatedByAdmintourcreate
TourUpdatedByAdmintourupdate
TourPublishedByAdmintourpublish
TourArchivedByAdmintourarchive
TourDuplicatedByAdmintourduplicate
TourVersionRolledBackByAdmintourrollback
ContentCreatedByAdmincontentcreate
ContentUpdatedByAdmincontentupdate
ContentPublishedByAdmincontentpublish
ContentArchivedByAdmincontentarchive
ThemeCreatedByAdminthemecreate
ThemeUpdatedByAdminthemeupdate
ThemeDeletedByAdminthemedelete
ThemeSetDefaultByAdminthemeset_default
AdminUserAddedByAdminadmin_useradd
AdminUserUpdatedByAdminadmin_userupdate
AdminUserRemovedByAdminadmin_userremove
AudienceCreatedByAdminaudiencecreate
AudienceUpdatedByAdminaudienceupdate
AudienceDeletedByAdminaudiencedelete
CircuitBreakerToggledByAdminconfigtoggle

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.

hcl
# Match a specific event class
event_pattern = jsonencode({
  source      = ["spotlight"]
  detail-type = ["TourCompleted"]
})
hcl
# 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:

hcl
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:

hcl
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:

python
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:

  1. The API writes the event to Spotlight.Events.Outbox in the same DynamoDB transaction as the state change.
  2. An outbox processor (Lambda in production, background task in local dev) polls for pending events.
  3. Pending events are published to EventBridge.
  4. Successfully published events are marked as processed (with TTL for cleanup).

This guarantees at-least-once delivery even if EventBridge is temporarily unavailable.

Spotlight