Services
MiniStack emulates each service’s behaviour, not just its API: S3 keeps the objects you put, SQS delivers and redelivers messages, and Kinesis stores records on shards.
These work as they do in MiniStack. Pocket Region’s tests have run each one here and found nothing that behaves differently. Each links to an example you can run below, and where Pocket Region runs part of a service its own way, such as MiniStack’s background threads, its section says so.
Other services:
| Service | Status |
|---|---|
| Lambda | Node and Python functions run, in Node and in a page. Lambda lists where they differ from AWS |
| RDS, ElastiCache, ECS, EKS, Batch, OpenSearch, Athena | Stubs: they answer, but nothing runs behind them. Databases, caches, and clusters report ready with no endpoint, tasks and jobs report running or done, and Athena returns made-up rows |
| The rest of MiniStack’s services | Not run here yet. They may work, but nothing here checks them |
Examples
Section titled “Examples”The examples are written as they would be for AWS, in JavaScript with clients built from {} and
in Python with boto3. The JavaScript and Python buttons under an example switch every example on
this page, and the choice is remembered. Clicking Run executes an example here in your browser
tab, against a region that’s emptied before each run, as Examples in your docs
describes. To run a JavaScript one in your own code, give its clients a region first:
import { clientConfig, createRegion } from 'pocket-region/browser';
const region = await createRegion();const config = clientConfig(region);// then new S3Client(config) wherever an example has new S3Client({})Without that, a client built from {} outside this page reaches AWS with whatever credentials
your environment has. A Python example reaches a region from your own code over HTTP: serve
one from Node and build each client with its URL as endpoint_url.
Uploading an object sends a notification to a queue. MiniStack sends notifications from a
background thread, which Pocket Region runs inside the upload request instead, so the notification
is already queued when PutObject returns. MiniStack’s S3 page
lists what it supports.
import { S3Client, CreateBucketCommand, PutBucketNotificationConfigurationCommand, PutObjectCommand } from '@aws-sdk/client-s3';import { SQSClient, CreateQueueCommand, GetQueueAttributesCommand, ReceiveMessageCommand } from '@aws-sdk/client-sqs';
const s3 = new S3Client({});const sqs = new SQSClient({});
const { QueueUrl } = await sqs.send(new CreateQueueCommand({ QueueName: 'uploads' }));const { Attributes } = await sqs.send(new GetQueueAttributesCommand({ QueueUrl, AttributeNames: ['QueueArn'] }));
await s3.send(new CreateBucketCommand({ Bucket: 'photos' }));await s3.send(new PutBucketNotificationConfigurationCommand({ Bucket: 'photos', NotificationConfiguration: { QueueConfigurations: [{ QueueArn: Attributes.QueueArn, Events: ['s3:ObjectCreated:*'] }], },}));await s3.send(new PutObjectCommand({ Bucket: 'photos', Key: 'cat.jpg', Body: 'meow' }));
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10 }));for (const record of Messages.flatMap((message) => JSON.parse(message.Body).Records ?? [])) { console.log(record.eventName, record.s3.bucket.name, record.s3.object.key);}import json
import boto3
s3 = boto3.client('s3')sqs = boto3.client('sqs')
queue_url = sqs.create_queue(QueueName='uploads')['QueueUrl']queue_arn = sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['QueueArn'])['Attributes']['QueueArn']
s3.create_bucket(Bucket='photos')s3.put_bucket_notification_configuration( Bucket='photos', NotificationConfiguration={ 'QueueConfigurations': [{'QueueArn': queue_arn, 'Events': ['s3:ObjectCreated:*']}], },)s3.put_object(Bucket='photos', Key='cat.jpg', Body=b'meow')
messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get('Messages', [])for message in messages: for record in json.loads(message['Body']).get('Records', []): print(record['eventName'], record['s3']['bucket']['name'], record['s3']['object']['key'])Messages sent to a queue come back from a receive. MiniStack’s SQS page lists what it supports.
import { SQSClient, CreateQueueCommand, SendMessageCommand, ReceiveMessageCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
const { QueueUrl } = await sqs.send(new CreateQueueCommand({ QueueName: 'orders' }));await sqs.send(new SendMessageCommand({ QueueUrl, MessageBody: 'order 41' }));await sqs.send(new SendMessageCommand({ QueueUrl, MessageBody: 'order 42' }));
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10 }));console.log(Messages.map((message) => message.Body));import boto3
sqs = boto3.client('sqs')
queue_url = sqs.create_queue(QueueName='orders')['QueueUrl']sqs.send_message(QueueUrl=queue_url, MessageBody='order 41')sqs.send_message(QueueUrl=queue_url, MessageBody='order 42')
messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get('Messages', [])print([message['Body'] for message in messages])DynamoDB
Section titled “DynamoDB”An item put in a table comes back from a scan. An item whose TTL has passed is removed within about a minute, as scheduled work describes. MiniStack’s DynamoDB page lists what it supports.
import { DynamoDBClient, CreateTableCommand, PutItemCommand, ScanCommand } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDBClient({});
await dynamodb.send(new CreateTableCommand({ TableName: 'sessions', KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }], AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }], BillingMode: 'PAY_PER_REQUEST',}));
await dynamodb.send(new PutItemCommand({ TableName: 'sessions', Item: { id: { S: 'alice' } } }));
const { Items } = await dynamodb.send(new ScanCommand({ TableName: 'sessions' }));console.log('sessions:', Items.map((item) => item.id.S));import boto3
dynamodb = boto3.client('dynamodb')
dynamodb.create_table( TableName='sessions', KeySchema=[{'AttributeName': 'id', 'KeyType': 'HASH'}], AttributeDefinitions=[{'AttributeName': 'id', 'AttributeType': 'S'}], BillingMode='PAY_PER_REQUEST',)
dynamodb.put_item(TableName='sessions', Item={'id': {'S': 'alice'}})
items = dynamodb.scan(TableName='sessions')['Items']print('sessions:', [item['id']['S'] for item in items])A message published to a topic arrives in a subscribed queue. MiniStack’s SNS page lists what it supports.
import { SNSClient, CreateTopicCommand, SubscribeCommand, PublishCommand } from '@aws-sdk/client-sns';import { SQSClient, CreateQueueCommand, GetQueueAttributesCommand, ReceiveMessageCommand } from '@aws-sdk/client-sqs';
const sns = new SNSClient({});const sqs = new SQSClient({});
const { QueueUrl } = await sqs.send(new CreateQueueCommand({ QueueName: 'order-emails' }));const { Attributes } = await sqs.send(new GetQueueAttributesCommand({ QueueUrl, AttributeNames: ['QueueArn'] }));
const { TopicArn } = await sns.send(new CreateTopicCommand({ Name: 'orders' }));await sns.send(new SubscribeCommand({ TopicArn, Protocol: 'sqs', Endpoint: Attributes.QueueArn }));await sns.send(new PublishCommand({ TopicArn, Subject: 'placed', Message: 'order 42' }));
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl }));const { Subject, Message } = JSON.parse(Messages[0].Body);console.log(Subject, Message);import json
import boto3
sns = boto3.client('sns')sqs = boto3.client('sqs')
queue_url = sqs.create_queue(QueueName='order-emails')['QueueUrl']queue_arn = sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['QueueArn'])['Attributes']['QueueArn']
topic_arn = sns.create_topic(Name='orders')['TopicArn']sns.subscribe(TopicArn=topic_arn, Protocol='sqs', Endpoint=queue_arn)sns.publish(TopicArn=topic_arn, Subject='placed', Message='order 42')
messages = sqs.receive_message(QueueUrl=queue_url)['Messages']notification = json.loads(messages[0]['Body'])print(notification['Subject'], notification['Message'])EventBridge
Section titled “EventBridge”Only the event that matches the rule reaches its queue. MiniStack’s EventBridge page lists what it supports.
import { EventBridgeClient, PutRuleCommand, PutTargetsCommand, PutEventsCommand } from '@aws-sdk/client-eventbridge';import { SQSClient, CreateQueueCommand, GetQueueAttributesCommand, ReceiveMessageCommand } from '@aws-sdk/client-sqs';
const events = new EventBridgeClient({});const sqs = new SQSClient({});
const { QueueUrl } = await sqs.send(new CreateQueueCommand({ QueueName: 'large-orders' }));const { Attributes } = await sqs.send(new GetQueueAttributesCommand({ QueueUrl, AttributeNames: ['QueueArn'] }));
await events.send(new PutRuleCommand({ Name: 'large-orders', EventPattern: JSON.stringify({ source: ['shop'], detail: { total: [{ numeric: ['>', 100] }] } }),}));await events.send(new PutTargetsCommand({ Rule: 'large-orders', Targets: [{ Id: 'queue', Arn: Attributes.QueueArn }] }));await events.send(new PutEventsCommand({ Entries: [ { Source: 'shop', DetailType: 'order placed', Detail: JSON.stringify({ total: 20 }) }, { Source: 'shop', DetailType: 'order placed', Detail: JSON.stringify({ total: 250 }) }, ],}));
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10 }));console.log(Messages.map((message) => JSON.parse(message.Body).detail));import json
import boto3
events = boto3.client('events')sqs = boto3.client('sqs')
queue_url = sqs.create_queue(QueueName='large-orders')['QueueUrl']queue_arn = sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['QueueArn'])['Attributes']['QueueArn']
events.put_rule( Name='large-orders', EventPattern=json.dumps({'source': ['shop'], 'detail': {'total': [{'numeric': ['>', 100]}]}}),)events.put_targets(Rule='large-orders', Targets=[{'Id': 'queue', 'Arn': queue_arn}])events.put_events( Entries=[ {'Source': 'shop', 'DetailType': 'order placed', 'Detail': json.dumps({'total': 20})}, {'Source': 'shop', 'DetailType': 'order placed', 'Detail': json.dumps({'total': 250})}, ])
messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get('Messages', [])print([json.loads(message['Body'])['detail'] for message in messages])Secrets Manager
Section titled “Secrets Manager”A second value becomes the secret’s current version. MiniStack’s Secrets Manager page lists what it supports.
import { SecretsManagerClient, CreateSecretCommand, PutSecretValueCommand, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secrets = new SecretsManagerClient({});
await secrets.send(new CreateSecretCommand({ Name: 'db-password', SecretString: 'first' }));await secrets.send(new PutSecretValueCommand({ SecretId: 'db-password', SecretString: 'second' }));
const { SecretString, VersionStages } = await secrets.send(new GetSecretValueCommand({ SecretId: 'db-password' }));console.log(SecretString, VersionStages);import boto3
secrets = boto3.client('secretsmanager')
secrets.create_secret(Name='db-password', SecretString='first')secrets.put_secret_value(SecretId='db-password', SecretString='second')
secret = secrets.get_secret_value(SecretId='db-password')print(secret['SecretString'], secret['VersionStages'])SSM Parameter Store
Section titled “SSM Parameter Store”Parameters listed by path, with a SecureString decrypted. MiniStack’s SSM page lists what it supports.
import { SSMClient, PutParameterCommand, GetParametersByPathCommand } from '@aws-sdk/client-ssm';
const ssm = new SSMClient({});
await ssm.send(new PutParameterCommand({ Name: '/app/api-key', Value: 'abc', Type: 'SecureString' }));await ssm.send(new PutParameterCommand({ Name: '/app/region', Value: 'us-east-1', Type: 'String' }));
const { Parameters } = await ssm.send(new GetParametersByPathCommand({ Path: '/app', WithDecryption: true }));console.log(Parameters.map(({ Name, Value }) => `${Name} = ${Value}`));import boto3
ssm = boto3.client('ssm')
ssm.put_parameter(Name='/app/api-key', Value='abc', Type='SecureString')ssm.put_parameter(Name='/app/region', Value='us-east-1', Type='String')
parameters = ssm.get_parameters_by_path(Path='/app', WithDecryption=True)['Parameters']print([f"{parameter['Name']} = {parameter['Value']}" for parameter in parameters])Text encrypted with a new key decrypts back. MiniStack’s KMS page lists what it supports.
import { KMSClient, CreateKeyCommand, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms';
const kms = new KMSClient({});
const { KeyMetadata } = await kms.send(new CreateKeyCommand({}));const { CiphertextBlob } = await kms.send(new EncryptCommand({ KeyId: KeyMetadata.KeyId, Plaintext: new TextEncoder().encode('hello'),}));const { Plaintext } = await kms.send(new DecryptCommand({ CiphertextBlob }));console.log(new TextDecoder().decode(Plaintext));import boto3
kms = boto3.client('kms')
key_id = kms.create_key()['KeyMetadata']['KeyId']ciphertext = kms.encrypt(KeyId=key_id, Plaintext=b'hello')['CiphertextBlob']print(kms.decrypt(CiphertextBlob=ciphertext)['Plaintext'].decode())CloudWatch Logs
Section titled “CloudWatch Logs”A filter pattern finds the matching log event. MiniStack’s CloudWatch Logs page lists what it supports.
import { CloudWatchLogsClient, CreateLogGroupCommand, CreateLogStreamCommand, PutLogEventsCommand, FilterLogEventsCommand } from '@aws-sdk/client-cloudwatch-logs';
const logs = new CloudWatchLogsClient({});
await logs.send(new CreateLogGroupCommand({ logGroupName: '/app/web' }));await logs.send(new CreateLogStreamCommand({ logGroupName: '/app/web', logStreamName: 'instance-1' }));await logs.send(new PutLogEventsCommand({ logGroupName: '/app/web', logStreamName: 'instance-1', logEvents: [ { timestamp: Date.now(), message: 'GET / 200' }, { timestamp: Date.now(), message: 'GET /cart 500' }, ],}));
const { events } = await logs.send(new FilterLogEventsCommand({ logGroupName: '/app/web', filterPattern: '500' }));console.log(events.map((event) => event.message));import time
import boto3
logs = boto3.client('logs')
logs.create_log_group(logGroupName='/app/web')logs.create_log_stream(logGroupName='/app/web', logStreamName='instance-1')now = int(time.time() * 1000)logs.put_log_events( logGroupName='/app/web', logStreamName='instance-1', logEvents=[ {'timestamp': now, 'message': 'GET / 200'}, {'timestamp': now, 'message': 'GET /cart 500'}, ],)
events = logs.filter_log_events(logGroupName='/app/web', filterPattern='500')['events']print([event['message'] for event in events])Kinesis
Section titled “Kinesis”A record put on a stream reads back from its shard. MiniStack’s Kinesis page lists what it supports.
import { KinesisClient, CreateStreamCommand, PutRecordCommand, ListShardsCommand, GetShardIteratorCommand, GetRecordsCommand } from '@aws-sdk/client-kinesis';
const kinesis = new KinesisClient({});
await kinesis.send(new CreateStreamCommand({ StreamName: 'clicks', ShardCount: 1 }));await kinesis.send(new PutRecordCommand({ StreamName: 'clicks', PartitionKey: 'user-1', Data: new TextEncoder().encode('click'),}));
const { Shards } = await kinesis.send(new ListShardsCommand({ StreamName: 'clicks' }));const { ShardIterator } = await kinesis.send(new GetShardIteratorCommand({ StreamName: 'clicks', ShardId: Shards[0].ShardId, ShardIteratorType: 'TRIM_HORIZON',}));const { Records } = await kinesis.send(new GetRecordsCommand({ ShardIterator }));console.log(Records.map((record) => new TextDecoder().decode(record.Data)));import boto3
kinesis = boto3.client('kinesis')
kinesis.create_stream(StreamName='clicks', ShardCount=1)kinesis.put_record(StreamName='clicks', PartitionKey='user-1', Data=b'click')
shard_id = kinesis.list_shards(StreamName='clicks')['Shards'][0]['ShardId']shard_iterator = kinesis.get_shard_iterator( StreamName='clicks', ShardId=shard_id, ShardIteratorType='TRIM_HORIZON',)['ShardIterator']records = kinesis.get_records(ShardIterator=shard_iterator)['Records']print([record['Data'].decode() for record in records])Step Functions
Section titled “Step Functions”A Task state prices an order with a Lambda function. Pocket Region runs the function as
Lambda describes, and MiniStack runs the state machine.
MiniStack’s Step Functions page lists what it
supports.
import { LambdaClient, CreateFunctionCommand, waitUntilFunctionActiveV2 } from '@aws-sdk/client-lambda';import { SFNClient, CreateStateMachineCommand, StartExecutionCommand, DescribeExecutionCommand } from '@aws-sdk/client-sfn';import { strToU8, zipSync } from 'fflate';
const lambda = new LambdaClient({});const sfn = new SFNClient({});
const code = 'export const handler = async (order) => ({ ...order, total: order.price * order.quantity });';const { FunctionArn } = await lambda.send(new CreateFunctionCommand({ FunctionName: 'price', Runtime: 'nodejs22.x', Handler: 'index.handler', Role: 'arn:aws:iam::000000000000:role/lambda', Code: { ZipFile: zipSync({ 'index.mjs': strToU8(code) }) },}));await waitUntilFunctionActiveV2({ client: lambda, maxWaitTime: 30 }, { FunctionName: 'price' });
const { stateMachineArn } = await sfn.send(new CreateStateMachineCommand({ name: 'price-order', roleArn: 'arn:aws:iam::000000000000:role/states', definition: JSON.stringify({ StartAt: 'Price', States: { Price: { Type: 'Task', Resource: FunctionArn, End: true } } }),}));const { executionArn } = await sfn.send(new StartExecutionCommand({ stateMachineArn, input: JSON.stringify({ item: 'tea', price: 4, quantity: 2 }),}));
let execution;do { await new Promise((resolve) => setTimeout(resolve, 200)); execution = await sfn.send(new DescribeExecutionCommand({ executionArn }));} while (execution.status === 'RUNNING');console.log(execution.status, JSON.parse(execution.output));import ioimport jsonimport timeimport zipfile
import boto3
lambda_ = boto3.client('lambda')sfn = boto3.client('stepfunctions')
package = io.BytesIO()with zipfile.ZipFile(package, 'w') as archive: archive.writestr('index.py', "def handler(order, context):\n return {**order, 'total': order['price'] * order['quantity']}\n")function_arn = lambda_.create_function( FunctionName='price', Runtime='python3.13', Handler='index.handler', Role='arn:aws:iam::000000000000:role/lambda', Code={'ZipFile': package.getvalue()},)['FunctionArn']lambda_.get_waiter('function_active_v2').wait(FunctionName='price')
state_machine_arn = sfn.create_state_machine( name='price-order', roleArn='arn:aws:iam::000000000000:role/states', definition=json.dumps({'StartAt': 'Price', 'States': {'Price': {'Type': 'Task', 'Resource': function_arn, 'End': True}}}),)['stateMachineArn']execution_arn = sfn.start_execution( stateMachineArn=state_machine_arn, input=json.dumps({'item': 'tea', 'price': 4, 'quantity': 2}),)['executionArn']
while (execution := sfn.describe_execution(executionArn=execution_arn))['status'] == 'RUNNING': time.sleep(0.2)print(execution['status'], json.loads(execution['output']))IAM and STS
Section titled “IAM and STS”A region keeps IAM users, roles, and policies, but by default it allows every request. Create it
with enforceIam: true and each request is checked against the caller’s policies, as MiniStack
checks them with AUTH=true. Enforcement is chosen when a region is created, so this example
creates its own rather than using the page’s.
MiniStack’s IAM page lists what it supports.
import { clientConfig, createRegion } from 'pocket-region/browser';import { IAMClient, CreateUserCommand, PutUserPolicyCommand, CreateAccessKeyCommand } from '@aws-sdk/client-iam';import { S3Client, CreateBucketCommand, PutObjectCommand } from '@aws-sdk/client-s3';
const region = await createRegion({ enforceIam: true });const iam = new IAMClient(clientConfig(region));await new S3Client(clientConfig(region)).send(new CreateBucketCommand({ Bucket: 'uploads' }));
await iam.send(new CreateUserCommand({ UserName: 'uploader' }));await iam.send(new PutUserPolicyCommand({ UserName: 'uploader', PolicyName: 'put-uploads', PolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: [{ Effect: 'Allow', Action: 's3:PutObject', Resource: 'arn:aws:s3:::uploads/*' }], }),}));const { AccessKey } = await iam.send(new CreateAccessKeyCommand({ UserName: 'uploader' }));const uploader = new S3Client({ ...clientConfig(region), credentials: { accessKeyId: AccessKey.AccessKeyId, secretAccessKey: AccessKey.SecretAccessKey },});
await uploader.send(new PutObjectCommand({ Bucket: 'uploads', Key: 'cat.txt', Body: 'meow' }));console.log('put uploads/cat.txt');try { await uploader.send(new CreateBucketCommand({ Bucket: 'photos' }));} catch (error) { console.log(error.name, error.message);}
await region.stop();test, the key clientConfig uses, is the account’s root user and
is allowed everything, so a denial needs a user or a role.
Scheduled work
Section titled “Scheduled work”MiniStack runs scheduled EventBridge rules, EventBridge Scheduler schedules, and the DynamoDB TTL
reaper on background threads. Pocket Region runs each thread as a task that sleeps as MiniStack’s
code says, so they keep MiniStack’s timing: a one-time schedule in the past fires within ten
seconds, and an expired TTL is removed within a minute. rate() and cron() wait real time, so a
rate(1 minute) rule first fires a minute after it’s created. Schedules that invoke Lambda are
untested.