Skip to main content

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)

GenerationBackend computeStatus
v1EC2 per tenantRetired — leftovers still in repo.
v2ECS Fargate per tenantPhasing out (11 prod tenants ran here pre-cutover).
v3Shared api-router Lambda → shared swishing-game-backend LambdaLive.

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] holds secret_arn. getDbPool({ tenantId }) reads the tenant secret and caches a per-tenant pg.Pool (max=2, idleTimeoutMillis=5000).
  • Per-tenant Cognito. TenantDirectory[SK=COGNITO] holds the pool id + app client. AWS SDK calls to cognito-idp:Admin* target the tenant pool.
  • Per-tenant routing. TenantDirectory[SK=ROUTING] holds backend_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:

  1. HTTP requests: X-Tenant-Id header. Frontend attaches this; the api-router validates it against TenantDirectory; the backend reads req.headers['x-tenant-id'] (helper: tenantIdFromReq()).
  2. Scheduled / EventBridge invocations: event.tenantId field on the Lambda event payload. The Lambda dispatcher (lambda.js) routes based on event.trigger; for game-transition events the tenant comes from event.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-Id is validated against the token's iss claim — 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 $default route 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)

HostnameBacked byNotes
gateway.swishing.cards / gateway.dev.swishing.cardsAPI Gateway HTTP API → api-router LambdaProduction gateway. Replaces the legacy api-app.swishing.cards (kept live during soak).
<uuid>.api.swishing.cardsPer-tenant ECS Fargate (legacy v2)Scaled to 0 after the 2026-04-29 cutover. Reversible until cleanup runs.
docs.<service>.[dev.]swishing.cardsThis 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).