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