Skip to content

Architecture ​

Spotlight is a multi-tenant platform for product tours and onboarding. This page covers the system design, data flow, infrastructure components, and tenant isolation model.

System Overview ​

The platform has three main layers: a client-side JavaScript SDK, a serverless API backend, and an asynchronous event processing pipeline.

Request Flow ​

When a user visits a page with the SDK installed, the following happens:

Infrastructure Components ​

Lambda Functions ​

The backend runs as a single container-image Lambda:

FunctionPurposeTrigger
API Handler (spotlight-{env}-api)FastAPI app serving every SDK, admin, platform, and MCP routeAPI Gateway HTTP requests

Python 3.13 on arm64, 2048 MB memory in production / 1024 MB in development, packaged as a container image. Mangum bridges API Gateway HTTP events into the ASGI app.

DynamoDB Tables ​

Data is organised across multiple single-table-design tables, all with per-tenant partition keys for isolation:

Tables with DynamoDB Streams enabled: Tours.Definitions, Content.Definitions, Content.Checklists, Progress.UserState, Events.Outbox, Surveys.Definitions.

All tables use pay-per-request billing and server-side encryption.

Event Pipeline ​

Domain events are written into Events.Outbox as part of the same DynamoDB transaction as the business write — the transactional outbox pattern. This gives at-least-once delivery without coupling the API request to external publishers.

Events.Aggregates powers the dashboard's Analytics views. Events.Interactions is the raw event log used by the Selector Health, Audit, and per-tour funnel pages.

Authentication Flow ​

Authentication uses a two-layer model: API keys identify the tenant, JWTs identify the user.

Key points:

  • API key is always required. It is the tenant identifier.
  • JWT is optional for SDK endpoints. Anonymous users can still see tours, but progress is not tracked.
  • JWT is required for admin endpoints. The require_admin dependency enforces both a valid JWT and admin status.
  • Origin lockdown is enforced per-tenant. The allowed_origins list in tenant config restricts which domains can use the API key.
  • Admin detection uses a configurable strategy. The default table_lookup strategy checks the Admin.Users table.

Multi-Tenant Isolation ​

Every piece of data in the system is scoped to a tenant. Isolation is enforced at multiple levels:

Data Layer ​

All DynamoDB tables use tenant_id (or a composite key starting with tenant_id) as the partition key. This means:

  • Queries are physically scoped to a single tenant's partition.
  • No query can accidentally return data from another tenant.
  • Each tenant's data can be deleted independently.

API Layer ​

The AuthContext middleware extracts tenant_id from the API key on every request and passes it through to all repository calls. There is no mechanism to query across tenants.

SDK Layer ​

Each SDK instance is bound to a single API key (and therefore a single tenant). The SDK cannot access another tenant's content.

Event Layer ​

Domain events carry tenant_id in their payload. EventBridge rules and downstream processors always filter by tenant.

SDK Architecture ​

The browser SDK is structured as a set of modules coordinated by a central SDK class:

Shadow DOM ​

All SDK UI is rendered inside an isolated Shadow DOM root (#spotlight-host). This provides:

  • CSS isolation -- host application styles do not affect SDK components, and SDK styles do not leak out.
  • DOM isolation -- SDK elements are not queryable via document.querySelector from the host page.
  • Z-index containment -- the host element is positioned fixed with pointer-events: none. Individual components enable pointer events as needed.

Content Types ​

The SDK supports six content types, each rendered by a dedicated component:

TypeComponentTargets ElementDescription
tourTooltip + TourOverlayYesMulti-step guided tour with navigation
tooltipTooltipYesSingle tooltip anchored to an element
spotlightTooltip + TourOverlayYesTooltip with backdrop overlay and cutout
modalModalNoCentered modal dialog
bannerBannerNoTop or bottom banner
hotspotHotspotYesPulsing dot that expands into a tooltip on click

Analytics ​

Every user interaction fires an analytics event. Events are sent to the backend via POST /v1/sdk/events. If the request fails, the event is queued and delivered via navigator.sendBeacon on page unload for reliable delivery.

Tracked event types: impression, click, cta_click, dismiss, tour_start, tour_step, tour_complete, tour_skip, hotspot_expand.

Deployment ​

Infrastructure is defined in Terraform, organised into reusable modules:

infrastructure/terraform/
  modules/
    api-gateway/     # HTTP API Gateway with throttling
    lambda/          # Lambda function with IAM roles
    dynamodb/        # All DynamoDB tables
    eventbridge/     # Event bus + rules
    cdn/             # CloudFront + S3 for SDK assets
    monitoring/      # CloudWatch alarms (planned)
  environments/
    local/           # Docker-based local development
    dev/             # AWS dev account
    staging/         # AWS staging account
    prod/            # AWS production account

Each environment composes the same modules with environment-specific variables (table prefixes, memory sizes, feature flags).

Local Development ​

The docker-compose.yml at the project root spins up:

  • DynamoDB Local for data storage
  • LocalStack for EventBridge emulation
  • Backend API on http://localhost:8000
  • Grafana + Prometheus + Loki + Tempo for observability
  • Seed data script for pre-populating a demo tenant
bash
docker compose up -d

The local environment uses a polling-based event processor instead of DynamoDB Streams, since DynamoDB Local does not support streams natively.

Spotlight