How Simfra Works
Simfra is a single Go binary. You start it, and it listens on one HTTP port (default 4599). Every AWS service - SQS, S3, IAM, Lambda, DynamoDB, all 88 of them - is handled by that same process on that same port. There are no separate processes per service, no sidecar containers for the control plane, and no external dependencies for the core simulation. (Docker is only needed for services that run real software, like Lambda functions or RDS databases.)
This page explains the key architectural decisions and what they mean for you as someone running Simfra.
Single Binary, Single Port
When you point the AWS CLI, SDK, or Terraform at http://localhost:4599, every request hits the same HTTP server regardless of which AWS service you're calling. Simfra determines which service you're targeting by inspecting the request itself - the same way AWS's own front-door routing works.
This means configuration is simple: set AWS_ENDPOINT_URL=http://localhost:4599 and every SDK call routes to Simfra automatically. Your Terraform provider block needs no endpoints {} block, no skip_* flags - just a region.
The tradeoff is that Simfra must handle multiple AWS wire protocols on the same port. AWS services don't all speak the same protocol - they evolved over 18 years and use at least five different serialization formats. Simfra handles all of them.
Request Routing
Every request goes through the same dispatch pipeline. Simfra determines the target service from the request headers, in this order:
- Host header -
sqs.us-east-1.localhost:4599routes to SQS,s3.us-east-1.localhost:4599routes to S3. This is how most AWS SDKs send requests when configured with a custom endpoint. - URL path prefix - Some services are identified by path:
/2013-04-01/routes to Route53,/v20180820/routes to S3 Control,/model/routes to Bedrock Runtime. - S3 virtual-hosted style -
mybucket.s3.us-east-1.localhost:4599routes to S3 with the bucket name extracted from the hostname. - X-Amz-Target header - JSON-protocol services like DynamoDB send
X-Amz-Target: DynamoDB_20120810.PutItem. Simfra maps the prefix (DynamoDB_20120810) to the service and the suffix (PutItem) to the operation. - SigV4 credential scope - The
Authorizationheader containsCredential=.../sqs/aws4_request. The service name in the signing scope is the last-resort fallback.
Once the service is identified, the operation is resolved (from X-Amz-Target, the Action query/body parameter, or REST route matching), and the request is dispatched to the appropriate handler.
Protocol Support
AWS services use different wire protocols. Simfra implements all of them on the same port, selecting the correct one based on the target service and request headers:
| Protocol | Serialization | Services |
|---|---|---|
| Query | XML request/response, form-encoded parameters | SQS, SNS, IAM, STS, Auto Scaling, CloudFormation |
| EC2 Query | XML with EC2-specific parameter naming | EC2 |
| JSON | JSON request/response, X-Amz-Target header |
DynamoDB, KMS, CloudWatch Logs, ECS, Step Functions |
| REST-XML | XML response, URL-based routing | S3, Route53, CloudFront |
| REST-JSON | JSON response, URL-based routing | Lambda, API Gateway, EKS, Bedrock, most newer services |
| Smithy RPC v2 CBOR | Binary CBOR encoding, path-based routing | CloudWatch Metrics |
The protocol determines how Simfra parses the request body into a typed Go struct and how it serializes the response. For example, when SQS receives Action=CreateQueue&QueueName=my-queue as a form-encoded body, the Query protocol parser maps those parameters to fields on a CreateQueueInput struct using XML struct tags. When DynamoDB receives {"TableName": "my-table"} as a JSON body, the JSON protocol parser unmarshals it directly.
Error responses also differ by protocol. A "queue not found" error is XML in SQS, JSON in DynamoDB, and CBOR in CloudWatch. Simfra uses the same error types internally and serializes them differently depending on which protocol the request used.
State Model
All state lives in memory. When you create an SQS queue, the queue metadata and its messages exist in Go data structures protected by mutexes. When you put an item in DynamoDB, it's stored in an in-memory table. Reads always come from memory - there's no disk I/O in the read path.
State is organized by account and region, mirroring how AWS isolates resources:
- Regional services (SQS, EC2, Lambda, etc.) use an
AccountRegionStore- a map ofaccountID -> region -> serviceState. Creating a queue inus-east-1for account123456789012doesn't affectus-west-2or a different account. - Global services (IAM, Route53, Organizations) use an
AccountStore- a map ofaccountID -> serviceState. IAM users are visible across all regions within the same account.
Stores are created lazily. The first time you make a request to a service in a particular account+region, Simfra creates the store for that combination. This means you don't need to pre-configure accounts or regions - they appear on demand.
Persistence
By default, state only exists in memory. When you stop Simfra, everything is gone.
If you set SIMFRA_DATA_DIR, Simfra enables write-through persistence to SQLite. Every mutation (create, update, delete) writes through to a SQLite database after updating the in-memory state. On startup, persisted resources are loaded from SQLite back into memory.
What persists: resource metadata - queues, topics, tables, IAM entities, security groups, policies, tags, configurations. What does not persist: transient state - SQS messages, in-flight delivery state, FIFO deduplication entries, DynamoDB Streams records. This matches the distinction between "infrastructure" (durable) and "runtime data" (ephemeral).
The persistence layer is nil-safe. When SIMFRA_DATA_DIR is empty, the *persistence.DB is nil and all persistence methods are no-ops. Service code doesn't need to check whether persistence is enabled - it calls the same methods either way.
IAM Enforcement
Every request goes through IAM policy evaluation before reaching the service handler. This is not optional, and it's not a bolt-on check - it's an integral part of the request pipeline, matching how AWS works.
The evaluation follows the same logic documented in the AWS IAM policy evaluation reference:
- Explicit deny - if any applicable policy explicitly denies the action, the request is denied immediately.
- Service Control Policies (SCPs) - if the account is part of an Organization, SCPs constrain maximum permissions.
- Resource-based policies - policies attached to the target resource (S3 bucket policies, SQS queue policies, Lambda function policies) can grant access, including cross-account.
- Permission boundaries - if set on the IAM entity, the action must be allowed by both identity policies and the boundary.
- Identity-based policies - policies attached to the IAM user, group, or role.
- Session policies - for assumed-role sessions, the session policy further constrains effective permissions.
The default is implicit deny. Access is granted only when an Allow is found and no Deny overrides it.
Root credentials (the default SIMFRA_ROOT_ACCESS_KEY / SIMFRA_ROOT_SECRET_KEY) get implicit full access, subject to SCPs in Organizations member accounts. IAM users and assumed roles are subject to the full evaluation chain.
This means that if your Terraform apply fails with an AccessDenied error, the cause is the same as it would be on real AWS: the principal's policies don't grant the required permissions. You debug it the same way - check the identity policies, resource policies, and permission boundaries.
Credential Validation
Simfra validates AWS credentials, not just the access key ID but the actual SigV4 signature. When the AWS SDK signs a request with your secret key, Simfra:
- Extracts the access key ID from the
Authorizationheader. - Looks up the corresponding secret key - first in root account credentials, then in STS temporary credentials (for
ASIA*prefixed keys), then in IAM access keys (forAKIA*prefixed keys). - Recomputes the SigV4 HMAC-SHA256 signature using the looked-up secret key.
- Compares it against the signature in the request. If they don't match, the request is rejected with
SignatureDoesNotMatch.
This means you can't use arbitrary credentials. The access key must exist (either as a root key, an IAM user's key, or a valid STS session), and the secret key must match.
Cross-Service Integration
Services in Simfra interact with each other at runtime, not just at creation time. When SNS delivers a message to an SQS subscription, it calls SQS's internal enqueue method directly - not through the HTTP gateway. When Lambda polls an SQS queue via an Event Source Mapping, the Lambda worker calls SQS's receive method in-process.
These internal calls go through delivery authorization: the target resource's resource policy is evaluated to determine whether the source service is permitted to perform the action. For example, when SNS tries to deliver to an SQS queue, the queue's policy must allow sns.amazonaws.com to call sqs:SendMessage.
Background workers drive asynchronous interactions. Each service that needs periodic processing (SQS message visibility timeouts, Lambda ESM polling, CloudWatch alarm evaluation, EC2 state transitions) has a worker goroutine that runs on a configurable interval.
Why It Matters
When you run terraform apply against Simfra, here is what actually happens:
- The Terraform AWS provider sends a real SigV4-signed HTTP request to
localhost:4599. - Simfra verifies the signature against stored credentials.
- IAM policies are evaluated - identity policies, SCPs, resource policies, permission boundaries.
- The service handler runs: it validates inputs the way AWS does, creates the resource with correct defaults and side effects, stores it in memory, and writes through to SQLite if persistence is enabled.
- A CloudTrail event is recorded.
- Cross-service effects fire: EventBridge events are emitted, dependent resources are created, subscriptions are notified.
- The response is serialized in the exact wire format the AWS SDK expects.
The Terraform provider doesn't know it's not talking to AWS. It's not getting canned responses - it's interacting with a system that runs the logic, enforces the rules, and maintains the state.