Architecture
MiniStack runs as a single hypercorn ASGI process on one port. Every AWS service shares that port. This page explains how a request flows from SDK to service handler.
Single ASGI gateway
The entry point is an ASGI3 application at ministack.app:app. It serves all 46 services from one port — 4566 by default (GATEWAY_PORT). There are no per-service ports, no reverse proxy, no sidecars. The gateway:
- Parses the HTTP request (
http.requestbody frames merged into one buffer). - Runs tiered detection to pick a service.
- Lazy-imports that service's handler module the first time it's needed.
- Calls the handler and writes the response.
Concurrency is cooperative — async handlers run on the event loop, sync handlers offload to a thread-pool executor sized by MINISTACK_WORKER_THREADS (default 64).
Startup lifecycle
On lifespan.startup:
- Banner logged (
version+port). - Thread-pool executor installed.
init.dscripts executed synchronously (blocks startup).- If
PERSIST_STATE=1, on-disk state is restored into every multi-tenant map. - Gateway signals
lifespan.startup.complete— traffic starts flowing. ready.dscripts fire as a background task;/_ministack/readyflips to 200 when they finish.
On lifespan.shutdown: if persistence is enabled, each service flushes its maps to STATE_DIR. Sidecar containers (RDS, EKS, ElastiCache, ECS) are left running — reaping is the Testcontainers module's responsibility (or yours, in plain Docker).
Request routing tiers
Routing happens in four tiers. Earlier tiers win — important for endpoints that would otherwise collide with service namespaces.
Tier 1 — pre-body routes
Handled without reading the request body. Health checks, OPTIONS (CORS), Lambda code downloads, Cognito well-known / JWKS, SES message inspection, admin reset.
Tier 2 — post-body shortcuts
Body is parsed, then special endpoints resolve before generic dispatch: Cognito OAuth2 (/oauth2/*, /login, /logout), runtime config.
Tier 3 — special data-plane
Host- and path-based routing for services that must dispatch before the generic step: S3 Control, RDS Data API, SES v2 REST, API Gateway execute-api, virtual-hosted S3, ALB data-plane.
Tier 4 — generic service dispatch
Router picks a service by inspecting, in order:
X-Amz-Targetheader (JSON services — e.g.DynamoDB_20120810.PutItem).Authorizationheader credential scope (SigV4 service segment — e.g..../us-east-1/s3/aws4_request).Action=query parameter or form field (Query-protocol services — IAM, EC2, CFN, SNS, SQS legacy).Hostheader (for*.amazonaws.com-style virtual hosts).- URL path pattern (REST services — Route53
/2013-04-01, API Gateway/restapis, etc.).
Once a service is resolved, the gateway calls service_module.handle(scope, request) (or the per-service equivalent). That handler dispatches to an operation via a _HANDLERS dict, action map, or if/elif chain — see each service page for specifics.
Service registry
The SERVICE_REGISTRY dict in ministack/app.py maps the service name → module path. Modules are loaded on first use (_get_module()), not at startup, so cold-start cost is bounded. Unused services add nothing to RSS.
SERVICES=s3,sqs,lambda shrinks the registry to a subset — handy for test matrices that exercise a narrow slice.
State & persistence
All service state is in-memory Python dicts. Every multi-tenant service wraps its root dicts in AccountScopedDict (see Multi-tenancy), which adds transparent (account_id, key) partitioning.
Persistence layers — see Configuration → Persistence for details:
- Service state (
PERSIST_STATE=1) — JSON snapshots of each service's maps toSTATE_DIR. - S3 object bodies (
S3_PERSIST=1) — object bytes toS3_DATA_DIR. - RDS database data (
RDS_PERSIST=1) — Docker named volumes instead of tmpfs.
Reset
POST /_ministack/reset calls _reset_all_state(), which iterates every registered service and wipes its in-memory state. A global reset lock serializes concurrent resets so no service sees a half-reset world. Add ?init=1 to re-run init.d scripts after the wipe.
Init & ready scripts
MiniStack scans two directory pairs for executables on startup. The LocalStack-compatible pair and the MiniStack-native pair both work; scripts in either are executed.
| Phase | Paths | When |
|---|---|---|
init.d | /docker-entrypoint-initaws.d/, /etc/localstack/init/boot.d/ | Before the gateway accepts traffic. Synchronous — startup blocks until done. |
ready.d | /docker-entrypoint-initaws.d/ready.d/, /etc/localstack/init/ready.d/ | After startup, as a background task. /_ministack/ready flips to 200 once all finish. |
Scripts inherit a pre-populated environment: AWS_ACCESS_KEY_ID=test, AWS_SECRET_ACCESS_KEY=test, AWS_DEFAULT_REGION=us-east-1, AWS_ENDPOINT_URL=http://localhost:4566. Point the AWS CLI at those and your seed scripts just work.
LOG_LEVEL=DEBUG — every request logs the detected service, operation, and account ID.