Architecture
This page is the deeper version of the diagram on the Overview. It explains the multi-tenant model, where tenant identity comes from, and how auth + CORS flow through the v3 stack.
Generations (history)
| Generation | Backend compute | Status |
|---|---|---|
| v1 | EC2 per tenant | Retired — leftovers still in repo. |
| v2 | ECS Fargate per tenant | Phasing out (11 prod tenants ran here pre-cutover). |
| v3 | Shared api-router Lambda → shared swishing-game-backend Lambda | Live. |
Frontend (CloudFront), Cognito (one pool per tenant), and RDS (shared, DB per tenant) are unchanged across generations.
Multi-tenant model
There is one swishing-game-backend Lambda for all tenants. Tenant
identity is not baked into the deploy; it arrives on every request and the
Lambda fans out per-tenant resources by id.
- Per-tenant DB.
TenantDirectory[PK=TENANT#<id>][SK=DB]holdssecret_arn.getDbPool({ tenantId })reads the tenant secret and caches a per-tenantpg.Pool(max=2,idleTimeoutMillis=5000). - Per-tenant Cognito.
TenantDirectory[SK=COGNITO]holds the pool id + app client. AWS SDK calls tocognito-idp:Admin*target the tenant pool. - Per-tenant routing.
TenantDirectory[SK=ROUTING]holdsbackend_base_url. In v3 every active tenant points at the same shared Lambda.
If a request doesn't carry a recognizable tenant id, the api-router rejects it before the backend ever sees it.
Where tenant identity comes from
Two sources, in priority order:
- HTTP requests:
X-Tenant-Idheader. Frontend attaches this; the api-router validates it againstTenantDirectory; the backend readsreq.headers['x-tenant-id'](helper:tenantIdFromReq()). - Scheduled / EventBridge invocations:
event.tenantIdfield on the Lambda event payload. The Lambda dispatcher (lambda.js) routes based onevent.trigger; forgame-transitionevents the tenant comes fromevent.tenantId.
There is no TENANT_ID environment variable. That was a v2 artifact and
was stripped during the v3 refactor.
Request flow (authenticated game request)
Auth flow
- Identity provider: Cognito User Pool per tenant. Each pool issues its own JWTs.
- Token validation: the backend verifies the bearer token against the
tenant's pool by id (looked up via
TenantDirectory[SK=COGNITO]). The token issuer is checked against the expected pool URL. - Authorization on the wire:
Authorization: Bearer <jwt>header. - Tenant scoping:
X-Tenant-Idis validated against the token'sissclaim — a token from one tenant's pool cannot be used to address a different tenant's resources.
CORS
CORS lives at API Gateway, not Express:
- Source of truth: the API Gateway HTTP API CORS config on
gateway.*. - Backend: Express
cors()was removed during the v3 refactor; the Lambda has only a 3-line OPTIONS-204 handler so the$defaultroute doesn't 404 on preflight. - Why: keeping CORS in the gateway means any future split (per-service Lambda, multi-region) doesn't drag duplicated CORS configs along.
Hostnames (v3)
| Hostname | Backed by | Notes |
|---|---|---|
gateway.swishing.cards / gateway.dev.swishing.cards | API Gateway HTTP API → api-router Lambda | Production gateway. Replaces the legacy api-app.swishing.cards (kept live during soak). |
<uuid>.api.swishing.cards | Per-tenant ECS Fargate (legacy v2) | Scaled to 0 after the 2026-04-29 cutover. Reversible until cleanup runs. |
docs.<service>.[dev.]swishing.cards | This portal (Phase 3.2) | One CloudFront distribution per env, 12 hostname aliases, CloudFront Function maps Host → root path. |
Where to go next
- Tenant API (game-backend) — full OpenAPI rendering.
docs/runbooks/in the repo — operational walkthroughs (cutover, hotfixes, recoveries).