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.
Since 1.4.0, state is additionally isolated per region. As of 1.5.4 every regional service is region-isolated except S3 and Aurora DSQL — two clients signing with different regions see fully independent resources, and cross-region ARN references return the same errors real AWS returns. IAM, STS, CloudFront, Route 53, and Organizations are global services in AWS, so their state is account-scoped and shared across regions by design — that is correct, not a pending gap. For S3 and Aurora DSQL, use unique resource names if you exercise two regions within one account. (The per-release rollout, 1.4.0 through 1.4.12, is in the changelog.)
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
The account leaks tracked here in earlier releases (CloudWatch metrics, EventBridge internal maps, the SES message store, and ElastiCache cluster tracking) were all resolved by the 1.4.x account- and region-isolation work — each now uses an account- and region-scoped store. If you find a service where two accounts still see each other's data, please file an issue.
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.