Persistence
By default, all Simfra state lives in memory and is lost when the process exits. Setting SIMFRA_DATA_DIR enables persistence to SQLite, so resources survive restarts.
Persistence is driven by a background reconciler: it periodically sweeps every service's in-memory store, compares it against what was last written, and persists only the delta (new/changed resources are saved, deleted ones are removed). A final sweep runs on graceful shutdown. Reads always come from memory — SQLite is never on the request hot path. This means a service is durable simply by exposing its resources to the reconciler; there is no per-operation write code to forget, so coverage is uniform across all services.
Enabling Persistence
export SIMFRA_DATA_DIR=/var/lib/simfra
simfra
This creates:
$SIMFRA_DATA_DIR/simfra.db- SQLite database for resource metadata$SIMFRA_DATA_DIR/s3/- Filesystem storage for S3 object data$SIMFRA_DATA_DIR/bootstrap/- Terraform state when using Terraform bootstrap$SIMFRA_DATA_DIR/.master_key- Auto-generated encryption key (ifSIMFRA_PERSISTENCE_KEYis not set)
What Persists
Resource metadata and configuration for every stateful service are reconciled to SQLite:
- IAM users, groups, roles, policies, access keys, instance profiles
- EC2 VPCs, subnets, security groups, route tables, instances, volumes, AMIs, ENIs
- S3 buckets (metadata and policies; object data stored on the filesystem)
- SQS queues (configuration, attributes, tags - not messages)
- SNS topics, subscriptions, and platform applications
- DynamoDB tables, GSIs, and item data
- Lambda functions, layers, event source mappings
- KMS keys, aliases, and grants
- RDS instances, clusters, subnet groups, parameter groups
- EKS clusters, node groups, Fargate profiles, add-ons
- ELBv2 load balancers, target groups, listeners, rules
- CloudWatch alarms (including current alarm state and bounded alarm history), metric streams, log groups
- EventBridge rules, targets, event buses
- Secrets Manager secrets and versions
- All other service resources with durable state
What Does NOT Persist
Transient runtime state is intentionally excluded:
- SQS messages - Messages in queues, in-flight state, delayed delivery timers
- FIFO dedup entries - 5-minute deduplication windows
- Data key caches - KMS envelope encryption data keys (regenerated on first use after restart)
- CloudWatch metric data points - Raw time-series datapoint buffers are ephemeral, high-churn data-plane state re-populated by the metric worker (alarm state and alarm history are persisted)
- HTTP retry queues - Pending async deliveries (SNS, EventBridge targets)
- Docker container state - Containers are recreated from persisted resource metadata on startup
- STS session tokens - Temporary credentials are not persisted
- In-flight async executions - Running deployments, pipeline/state-machine executions, ingestion/customization jobs, and live WebSocket connections are transient; terminal/historical records are persisted where AWS retains them
Startup Behavior
On startup with SIMFRA_DATA_DIR set, Simfra:
- Opens
simfra.db(creates it if missing) - Loads all persisted resources into memory stores in dependency order (IAM first, then KMS, EC2, etc.)
- Reconciles Docker infrastructure (if
SIMFRA_DOCKER=true): recreates networks, DNS containers, and service containers from persisted metadata - Skips native bootstrap if persisted state was found (Terraform bootstrap always runs since Terraform state handles idempotency)
- Marks the health endpoint as ready
The server does not accept requests until loading is complete.
Encryption at Rest
Sensitive fields in the SQLite database (secrets, private keys, passwords) are encrypted with AES-256-GCM.
Auto-Generated Key
When SIMFRA_PERSISTENCE_KEY is not set, Simfra generates a random 32-byte key and stores it at $SIMFRA_DATA_DIR/.master_key. This is convenient for single-machine setups but means the key is stored alongside the data.
Explicit Key
For production or shared-storage deployments, provide your own key:
# Generate a 32-byte key
openssl rand -hex 32
# Set it as an environment variable
export SIMFRA_PERSISTENCE_KEY=a1b2c3d4e5f6... # 64 hex characters
The key must be exactly 64 hex characters (32 bytes). If the key changes or is lost, encrypted fields in the database become unreadable.
S3 Object Storage
S3 object data is stored on the filesystem under $SIMFRA_DATA_DIR/s3/, organized by bucket and key. Object metadata (ETags, content types, ACLs) is stored in SQLite alongside other resource metadata. This separation keeps the SQLite database small while supporting arbitrarily large objects.
On-Disk Blob Storage Coverage
Some services store file/blob content on the filesystem under $SIMFRA_DATA_DIR/<namespace>/ rather than in SQLite (object bodies, container images, function code, keys). For a restart these files simply stay on disk. For export/import (/_simfra/export → /_simfra/import, including cross-machine) each location must be covered by a persistence.BlobArchiver so its content travels in the dump. The table below is the authoritative map; every on-disk writer is either archived or explicitly exempt.
| On-disk location | Owner | In export dump? | Mechanism |
|---|---|---|---|
$DATA_DIR/s3/ |
S3 object bodies | Yes | BodyStore BlobArchiver (s3) |
$DATA_DIR/ecr/ |
ECR image layers | Yes | BlobStore BlobArchiver (ecr) |
$DATA_DIR/lambda/ |
Lambda function code | Yes | CodeStore BlobArchiver (lambda) |
$DATA_DIR/ca/ |
Root CA key/cert | Yes | CA AdminAdapter BlobArchiver (ca); ClearBlobs is a no-op so a dump without a CA blob keeps the existing root |
$DATA_DIR/sts/token.key |
STS token-encryption key | Yes | tokenKeyArchiver BlobArchiver (sts); ClearBlobs no-op; import installs the key into the live encryptor so pre-import session tokens still decrypt |
$DATA_DIR/.master_key |
Field-encryption master key | No — deliberate | Security: the exporter decrypts ENC: fields into the dump and the importer re-encrypts with the local key, so the source key must never travel. Dumps are secrets (plaintext credentials, private keys) — protect them accordingly. |
| EFS / Transfer-Family NFS content | internal/nfs, efs |
Yes (Docker mode) | Named volume simfra-nfs-data-<fsID>; VolumeArchiver namespace efs-nfs. In-place restart also survives via the mounted volume. |
| CodeCommit git repositories | Docker volume | Yes (Docker mode) | Per-account git volume (SingletonVolumeName); VolumeArchiver namespace codecommit-git. |
| RDS / Redshift / DSQL / Redshift-Serverless / ElastiCache / Kafka / MQ / OpenSearch engine data | Docker named volumes | Yes (Docker mode) | Each engine mounts a labeled named volume at its data dir (V1); the dump's volumes/ section carries them (dump format v2). Exemptions: memcached (no persistence) and RDS standby (resyncs from primary). Non-Docker mode has no engine volumes. |
Engine/volume data is carried in the dump's volumes/<namespace>/ section (format
v2) via persistence.VolumeArchiver, the volume analogue of BlobArchiver. It is
present only in Docker mode; a dump taken (or imported) with Docker off carries
(applies) resource metadata only and reports skipped volumes. Enable/disable with
SIMFRA_EXPORT_VOLUMES (default true) or ?volumes=false.
Adding a new on-disk writer? Expose a BlobArchiver (implement persistence.BlobSourcer) with a matching BlobNamespace(), or a VolumeArchiver (persistence.VolumeSourcer) for Docker named volumes, or add a row here documenting the exemption. See docs/persistence-fromscratch-recipe.md.
Backup
To back up Simfra state:
- Stop Simfra (or accept a point-in-time snapshot)
- Copy
$SIMFRA_DATA_DIR/to your backup location - Include
simfra.db, thes3/directory, and.master_key(or record yourSIMFRA_PERSISTENCE_KEY)
The SQLite database uses WAL mode, so copying while Simfra is running produces a consistent snapshot as long as you copy both simfra.db and simfra.db-wal.