AWS Microservice Platform
Test project created to verify the Zstem MCP connection.
Architecture
- Client App — Web/mobile client calling the platform API over HTTPS.
- CloudFront — CDN edge; TLS termination and static/media caching in front of the API.
- WAF — Web application firewall; rate limiting and common-exploit rules at the edge.
- API Gateway — Single REST entry point for the whole platform. Validation, throttling, routing. Delegates every access decision to the Authorizer before invoking any service.
- Authorizer — AUTHORIZATION LAYER. Lambda authorizer that runs before any service. Verifies the JWT, reads role + team claims, evaluates the requested route/resource against the Policy Store, returns allow/deny + scoped context.
- Policy Store — Policy / permissions store: role→permission mappings and per-team scopes. Read on every request by the Authorizer; synced by the Auth Service.
- Cognito — User pool with admin/member groups. Issues JWTs carrying group + team claims; verified by the Authorizer.
- Auth Service — PLATFORM SERVICE — Identity & Access. Login/signup, admin vs member roles, team creation/membership, token issue/refresh. Owns admins, members, teams.
- Auth DB — Auth Service's own database (MySQL 8, via RDS Proxy): users, teams, team_members, refresh_tokens. Source of truth for identity.
- Notification Service — PLATFORM SERVICE — Notifications. Email / SMS / push fan-out with templating and delivery logging, so no project reinvents messaging.
- Notify DB — Notification Service's own store: templates, delivery status, preferences.
- SES — Transactional and bulk email delivery.
- SNS — Push notifications and SMS fan-out.
- File Service — PLATFORM SERVICE — Files/Media. Upload, storage, signed URL issuance, and post-upload processing.
- Files DB — File Service's own metadata store: object keys, owners, content types, processing state.
- Media Bucket — Object storage for uploaded files/media; accessed via signed URLs.
- Search Service — PLATFORM SERVICE — Search. Indexing and query across whatever projects store; fed by domain events.
- Search Index — Search index (OpenSearch) owned by the Search Service.
- Jobs Service — PLATFORM SERVICE — Jobs/Scheduling. Async processing, cron, and background work, exposed so any project can offload work.
- Job Queue — Buffers work for the async worker; decouples request path from slow processing.
- Job Worker — Queue-triggered worker Lambda that executes background jobs.
- Jobs DB — Jobs Service's own store: job definitions, status, results.
- Scheduler — Cron / scheduled rules that invoke the Jobs Service on a timetable.
- Config Service — PLATFORM SERVICE — Config / Feature Flags. Runtime configuration and flags without redeploys.
- AppConfig — Managed feature flags and dynamic configuration backing the Config Service.
- Domain Service — PLACEHOLDER — project-specific domain service. Pure API logic; trusts the Authorizer's scoped context. Duplicate this + its own DB per new domain to scale the platform out horizontally.
- Domain DB — The Domain Service's own database (MySQL 8 by default; each domain owns its data — database-per-service).
- Event Bus — PLATFORM SERVICE — Audit/Events. Central event bus every service publishes to; fans out to the audit log and search indexer. The backbone for decoupled, event-driven scale-out.
- Audit Service — Consumes platform events and writes an immutable audit trail.
- Audit Log — Append-only audit log owned by the Audit Service.
- Secrets Manager — DB credentials and third-party keys, injected at runtime across services.
- CloudWatch — Logs, metrics, and alarms across all services and the gateway.
- X-Ray — Distributed tracing across gateway → authorizer → service → data stores.
Workflow
- Request arrives at gateway
- Extract bearer token — Pull the Authorization header from the incoming request.
- Token present?
- Verify JWT (Cognito JWKS) — Check signature and expiry against the Cognito public keys.
- Signature valid?
- Load policy for route — Read the required permission + caller's role/team scopes from the Policy Store.
- Evaluate access — Match caller's role/team claims against the route's required permission.
- Permitted?
- Policy Store error — Policy Store unreachable after retries.
- Return ALLOW + scoped context — Emit allow policy with userId, role, teamIds for the backend.
- Invoke backend service — Gateway routes to the target service, which trusts the scoped context.
- Return DENY (403) — Fail closed. Any missing/invalid token, failed check, or store error lands here.
- Request served
- Access denied
Event flow
- Auth Service — Publishes identity lifecycle events.
- File Service — Publishes when an upload is stored and ready.
- Domain Service — Project-specific services publish a generic entity-changed envelope.
- Any Service — Any service can request a notification via the bus.
- Platform Event Bus — Central EventBridge bus. All platform events flow through here; rules route them to consumers. Decouples producers from consumers so new consumers attach without touching producers.
- Audit Service — Subscribes to all events; writes an immutable audit trail.
- Search Service — Indexes file and domain-entity events into OpenSearch.
- Notification Service — Consumes notification requests and dispatches via SES/SNS.
- Authorizer / Policy Store — Consumes membership changes to keep the Policy Store in sync.
- user.registered — Emitted by the Auth Service when a new admin or member account is created.
- team.membership.changed — Emitted when a user is added to or removed from a team; drives Policy Store re-sync.
- file.uploaded — Emitted by the File Service once an object is stored and ready. Search indexes it; Audit logs it.
- domain.entity.changed — Generic envelope any project-specific Domain Service emits on create/update/delete, so Search and Audit pick it up without bespoke wiring.
- notification.requested — Any service can request a notification by emitting this event; the Notification Service consumes and dispatches it.
About this design
- # About this design ## What this is A reusable **AWS microservice platform** — the plumbing every product needs, built once. Behind a single API Gateway sit seven platform services (Auth, Notifications, Files, Search, Jobs, Config, Audit), each a Lambda with its own datastore, plus a **Domain Service placeholder** where your actual product logic goes. Fork this when you want to start a real multi-service product without re-deciding auth, eventing, or observability. It is deliberately product-agnostic: nothing here knows what you're building. ## How it works Every request follows the same spine (see the sequence diagram and the authorizer workflow): 1. **Edge:** CloudFront + WAF terminate TLS and filter abuse in front of API Gateway. 2. **Authorization before any service:** a Lambda **Authorizer** verifies the Cognito JWT, evaluates the route against the DynamoDB **Policy Store**, and returns allow/deny plus scoped context. It fails closed. Backend services contain zero auth code. 3. **Services own their data:** database-per-service throughout — Auth on MySQL (RDS Proxy), Files on DynamoDB + S3 signed URLs, Search on OpenSearch, Jobs on DynamoDB with an **SQS queue** feeding a worker Lambda, Config on AppConfig. No cross-service joins, ever. 4. **Events, not calls, between services:** everything publishes to the central **EventBridge bus** (see the event flow for the five event schemas). Audit consumes everything into an append-only log; Search indexes `file.uploaded` and `domain.entity.changed`; `team.membership.changed` re-syncs the Policy Store; any service can emit `notification.requested` and the Notification Service handles email/SMS/push. 5. **Observability:** CloudWatch logs/metrics/alarms and X-Ray traces span gateway → authorizer → service → datastore. The API contract covers the concrete surfaces: `/auth/*` (signup, login, refresh, teams), generic `/domain/{type}` CRUD, `/files` (pre-signed upload/download), and `/jobs` (submit + poll async work). ## How to extend Adding your product is a four-step recipe — no platform service changes: 1. **Duplicate the Domain Service** Lambda and give it its own database (MySQL 8 by default; use DynamoDB if your access patterns are key-value). 2. **Route its path** through API Gateway — the Authorizer covers it automatically; add its permissions to the Policy Store. 3. **Publish `domain.entity.changed`** on every create/update/delete — audit trail and search indexing come for free. 4. **Offload slow work** to the Jobs Service (`POST /jobs`) and request user comms by emitting `notification.requested`. Repeat per domain to scale the platform horizontally. For deeper rationale (roles model, why authz syncs from auth, tradeoffs of database-per-service), read the companion doc **Platform Architecture Decisions**.
Platform Architecture Decisions
- # Reusable Microservice Platform — Architecture Decisions A generic, product-agnostic platform skeleton. New projects attach a domain service and inherit identity, notifications, files, search, jobs, config, and audit for free. ## Scope This is **platform plumbing**, not business domains. The services here are cross-cutting concerns every project needs regardless of what it does. Project-specific logic lives in Domain Services that hang off the same gateway → authorizer backbone. ## Three-tier request separation Every request passes through three distinct concerns, kept in separate components: 1. **Authentication (Auth Service)** — *who are you?* Login, signup, token issuance, admin/member/team management. Isolated from all backend logic. Owns identity as source of truth. 2. **Authorization (Authorizer + Policy Store)** — *what may you do?* A Lambda authorizer runs at the gateway before any service. Verifies the JWT, reads role + team claims, evaluates the requested route/resource against the Policy Store, returns allow/deny + scoped context. 3. **Backend services** — pure API logic. They receive already-authorized requests with scoped context and contain zero auth code. Rationale: keeps authorization logic centralized at the edge rather than scattered across every service, and lets backend services stay focused on their domain. ## Key decisions ### Database-per-service Each service owns its own datastore; there is no shared database. Auth → MySQL 8 (RDS, via RDS Proxy), Notifications → DynamoDB, Files → DynamoDB + S3, Search → OpenSearch, Jobs → DynamoDB, Config → AppConfig, Audit → DynamoDB, each Domain Service → its own DB (MySQL 8 by default). This is what allows services to be deployed, scaled, and evolved independently. The tradeoff is no cross-service joins — data is shared via events, not queries. ### Event-driven backbone A central EventBridge bus decouples producers from consumers. Services publish events; the Audit and Search services (and others) consume without the producers knowing about them. Adding a new consumer never requires touching a producer. This is the mechanism for horizontal scale-out. ### Auth is authentication; authz syncs from it Role/team changes in the Auth Service are synced out to the Policy Store (and Cognito claims) rather than the authorizer calling back into auth on the hot path. Keeps per-request authorization fast (single-digit-ms Policy Store read) with no synchronous dependency on the Auth Service. ### Roles model Two levels exist. A **coarse global role** (admin | member) rides in the JWT as a Cognito group claim — cheap to check on every request. **Per-team roles** live in the `team_members.team_role` column of the Auth DB, so the data layer already supports "admin of team X but member elsewhere." The token currently surfaces only the global role; when per-team enforcement is needed, project team scopes into the token (or the Policy Store) and consider AWS Verified Permissions / Cedar for fine-grained, resource-level rules. No schema migration required — the columns are already there. ## Extending the platform To add a new project domain: 1. Duplicate the **Domain Service** Lambda + its own **Domain DB** (MySQL 8 by default). 2. Route its path through API Gateway (authorization is inherited automatically). 3. Publish `domain.entity.changed` events — Search indexing and Audit logging come for free. 4. Request notifications by emitting `notification.requested`. No platform service needs modifying to onboard a new domain. ## Event catalogue See the Event Flow diagram for full JSON Schemas. Summary: - `user.registered` — Auth → Audit - `team.membership.changed` — Auth → Audit + Authorizer (Policy Store re-sync) - `file.uploaded` — Files → Audit + Search - `domain.entity.changed` — any Domain Service → Audit + Search (generic envelope) - `notification.requested` — any service → Notification Service