MicroStack
↑↓ navigate ↵ select esc close
GitHub

Multi-Tenancy

MicroStack supports per-account resource isolation. Multiple AWS account IDs can operate against the same MicroStack instance without seeing each other's resources.

How It Works

Account Detection

When a request arrives, AwsRequestMiddleware extracts the AWS access key ID from the Authorization header:

Authorization: AWS4-HMAC-SHA256 Credential=123456789012/20240101/us-east-1/sqs/aws4_request, ...

The AccountContext.SetFromAccessKey() method checks:

  • If the access key is exactly 12 digits (e.g., 123456789012), it's used directly as the account ID.
  • Otherwise, the default account ID is used (from MICROSTACK_ACCOUNT_ID, defaults to 000000000000).

This means you control the account ID by setting the AWS access key:

// Account 111111111111
var creds1 = new BasicAWSCredentials("111111111111", "test");
// Account 222222222222
var creds2 = new BasicAWSCredentials("222222222222", "test");

Request Scoping via AsyncLocal

AccountContext uses AsyncLocal<string> to store the account ID for the current request:

internal static class AccountContext
{
private static readonly AsyncLocal<string?> _accountId = new();
internal static void SetFromAccessKey(string? accessKeyId) { ... }
internal static string GetAccountId() => _accountId.Value ?? _defaultAccountId;
}

This is the C# equivalent of Python's contextvars.ContextVar. The value flows automatically through async/await chains within a single request, without explicit parameter passing.

AccountScopedDictionary

All service handlers store their data in AccountScopedDictionary<TKey, TValue> collections. This type automatically namespaces every key by the current account ID:

internal sealed class AccountScopedDictionary<TKey, TValue>
{
private readonly ConcurrentDictionary<(string AccountId, TKey Key), TValue> _data = new();
private (string, TKey) Scoped(TKey key) => (AccountContext.GetAccountId(), key);
internal TValue this[TKey key]
{
get => _data[Scoped(key)];
set => _data[Scoped(key)] = value;
}
// ...
}

When account 111111111111 creates an SQS queue named my-queue, the internal key is ("111111111111", "my-queue"). Account 222222222222 creating a queue with the same name gets a separate entry at ("222222222222", "my-queue").

Operations like Keys, Values, Items, and Count are automatically filtered to the current account. Only Clear() and ToRaw() operate across all accounts (used by reset and persistence).

Example

// Account 1 creates a queue
var config1 = new AmazonSQSConfig { ServiceURL = "http://localhost:4566" };
var client1 = new AmazonSQSClient(
new BasicAWSCredentials("111111111111", "test"), config1);
await client1.CreateQueueAsync("shared-name");
// Account 2 creates a queue with the same name - no conflict
var client2 = new AmazonSQSClient(
new BasicAWSCredentials("222222222222", "test"), config1);
await client2.CreateQueueAsync("shared-name");
// Each account sees only their own queue
var queues1 = await client1.ListQueuesAsync(""); // 1 queue
var queues2 = await client2.ListQueuesAsync(""); // 1 queue (different)

ARN Generation

Account IDs appear in ARNs generated by MicroStack:

arn:aws:sqs:us-east-1:111111111111:my-queue
arn:aws:dynamodb:us-east-1:222222222222:table/my-table

This matches real AWS behavior and ensures that cross-account references (e.g., SNS subscriptions to SQS queues) work correctly.

Persistence and Multi-Tenancy

When persistence is enabled, AccountScopedDictionary.ToRaw() serializes all accounts' data together. On restore, FromRaw() rebuilds the full dictionary, preserving account isolation. Each service's JSON state file contains data for all accounts.

Reset Behavior

The POST /_microstack/reset endpoint calls Clear() on all AccountScopedDictionary instances, which removes data for all accounts. There is no per-account reset.