Multi-tenancy
MiniStack is a single process, but each request is tagged with an account ID and region. State is partitioned per account — giving you LocalStack-Pro-style namespace isolation without spinning up multiple emulators.
Account inference
MiniStack picks an account ID from (in order):
- Access key ID — if the request carries a SigV4
Authorizationheader and the access key is a 12-digit string, that's the account ID. Lets you drive separate accounts with e.g.AWS_ACCESS_KEY_ID=111111111111vs222222222222. - Custom tokens with an embedded account — some tooling passes session tokens that include the account.
- Default —
000000000000(the historical LocalStack default). Some code paths still use123456789012; both normalize to the same pool under AccountScopedDict.
Regions
The region comes from the SigV4 credential scope (.../us-east-1/s3/aws4_request). Unsigned callers fall back to MINISTACK_REGION (default us-east-1).
Regional ARNs, endpoint URLs, and STS GetCallerIdentity responses all honor the per-request region. Two callers signing with different regions from the same process get region-correct responses — no single global region setting is used at request time.
State partitioning is per-account; most services share state across regions within an account. If you exercise two regions from the same account, use unique resource names to avoid collisions. This matches LocalStack's default and is the pragmatic choice for most tests.
AccountScopedDict
AccountScopedDict (in ministack/core/) is a dict wrapper that transparently partitions keys by the current request's account ID. A service module writes self._tables[table_name] = … as if it's a flat dict; the wrapper stores it under (account_id, table_name). When account 111…111 reads _tables, it sees only its own entries.
# Inside a service module self._queues = AccountScopedDict() self._queues[name] = queue_data # scoped to request.account_id automatically
The request's account is set via a context-local (set_request_account_id()) at the top of the router, so handlers can stay oblivious.
Leaky services
Not every service is wrapped. A handful still use plain dicts — meaning two accounts can see each other's data. These are the known leaks as of 1.3.14:
| Service | Nature of the leak |
|---|---|
| CloudWatch metrics | Plain-dict metric store; alarms and metric data bleed across accounts. |
| EventBridge (parts) | Some internal maps (event bus, rule state) are not fully scoped. |
| SES | Sent-message store is per-account-filterable at /_ministack/ses/messages, but underlying maps are shared. |
| ElastiCache | Container tracking is by logical cluster name — two accounts creating the same name collide. |
See Known limitations for the full cross-service gap list.
Using multiple accounts
Set distinct access keys for each logical account. No other configuration is needed.
export AWS_ACCESS_KEY_ID=111111111111 aws --endpoint-url=http://localhost:4566 s3 mb s3://account-one-bucket export AWS_ACCESS_KEY_ID=222222222222 aws --endpoint-url=http://localhost:4566 s3 mb s3://account-two-bucket aws --endpoint-url=http://localhost:4566 s3 ls # only sees account-two-bucket
In Boto3/SDKs, construct one client per account with its own credential pair. The secret key is ignored but must be set — test is fine.