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:
| Function | Purpose | Trigger |
|---|---|---|
API Handler (spotlight-{env}-api) | FastAPI app serving every SDK, admin, platform, and MCP route | API 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_admindependency enforces both a valid JWT and admin status. - Origin lockdown is enforced per-tenant. The
allowed_originslist in tenant config restricts which domains can use the API key. - Admin detection uses a configurable strategy. The default
table_lookupstrategy checks theAdmin.Userstable.
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.querySelectorfrom the host page. - Z-index containment -- the host element is positioned
fixedwithpointer-events: none. Individual components enable pointer events as needed.
Content Types
The SDK supports six content types, each rendered by a dedicated component:
| Type | Component | Targets Element | Description |
|---|---|---|---|
tour | Tooltip + TourOverlay | Yes | Multi-step guided tour with navigation |
tooltip | Tooltip | Yes | Single tooltip anchored to an element |
spotlight | Tooltip + TourOverlay | Yes | Tooltip with backdrop overlay and cutout |
modal | Modal | No | Centered modal dialog |
banner | Banner | No | Top or bottom banner |
hotspot | Hotspot | Yes | Pulsing 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 accountEach 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
docker compose up -dThe local environment uses a polling-based event processor instead of DynamoDB Streams, since DynamoDB Local does not support streams natively.