Dynoxide
A lightweight DynamoDB emulator backed by SQLite. Runs as an HTTP server compatible with the AWS DynamoDB API, as an MCP server for coding agents, or embeds directly into Rust and iOS applications as a library.
Why Dynoxide?
DynamoDB Local requires Docker and a JVM. It takes 3–4 seconds to cold-start, uses ~163 MB of memory at idle, and pulls a ~225MB Docker image. For CI pipelines running hundreds of integration tests, that overhead adds up. For local development, it means waiting for Docker and burning resources in the background.
Dynoxide is a native binary. It starts in milliseconds, idles at ~4.9 MB, and ships as a ~5MB download. Point any DynamoDB SDK at it and run your tests — same API, faster feedback loop.
For Rust projects, Dynoxide also offers an embedded mode: direct API calls via Database::memory() with no HTTP layer at all. Each test gets an isolated in-memory database with zero startup cost. And because it compiles to a native library with no runtime dependencies, it runs on platforms where DynamoDB Local can't — including iOS.
Performance
Local Development (Apple Silicon)
| Metric | Dynoxide (embedded) | Dynoxide (HTTP) | DynamoDB Local |
|---|---|---|---|
| Cold startup | ~0.2ms | ~15ms | ~2,287ms |
| GetItem (p50) | 9µs | 0.1ms | 0.8ms |
| PutItem throughput | ~51,613 ops/s | ~6,703 ops/s | ~945 ops/s |
| 50-test suite (sequential) | ~484ms | ~569ms | ~2,407ms |
| 50-test suite (4x parallel) | ~203ms | ~235ms | ~1,189ms |
CI (GitHub Actions)
Numbers from ubuntu-latest (2-core AMD EPYC 7763, 8GB RAM). Commit 006fa80.
| Metric | Dynoxide (embedded) | Dynoxide (HTTP) | DynamoDB Local | LocalStack (all services) |
|---|---|---|---|---|
| Cold startup | <1ms | ~3ms | ~3,715ms | ~5,243ms |
| GetItem (p50) | 16µs | 0.4ms | 0.9ms | — |
| 50-test CI suite | 775ms | 784ms | 3,156ms | — |
| Full workload (10K items) | — | 3.2s | 15.4s | — |
| Binary / Docker image | 5 MB | 5 MB | 225 MB | 1.1 GB |
| Idle memory (RSS) | ~4.9 MB | ~8 MB | ~163 MB | ~259 MB |
The gap is wider on Apple Silicon because the faster CPU amplifies the difference between native code and JVM overhead. Both are real measurements of the same benchmark suite. Full methodology and per-operation breakdowns →
Conformance
Verified against real DynamoDB by the dynamodb-conformance suite:
| Target | Tests | Pass Rate |
|---|---|---|
| DynamoDB | 526 | 100% |
| Dynoxide | 526 | 100% |
| DynamoDB Local | 526 | 92.0% |
See full results by tier.
How It Compares
| Dynoxide | DynamoDB Local | LocalStack (all services) | dynalite | |
|---|---|---|---|---|
| Conformance (526 tests) | 100% | 92% | 93% | 81% |
| Language | Rust | Java | Python + Java | Node.js |
| Storage | SQLite | SQLite | SQLite (via DDB Local) | LevelDB |
| Docker required | — | ✓ | ✓ | — |
| JVM required | — | ✓ | ✓ | — |
| Embeddable (Rust / iOS) | ✓ | — | — | — |
| MCP server for agents | ✓ | — | — | — |
LocalStack uses DynamoDB Local internally as its DynamoDB engine — its startup and memory overhead includes DynamoDB Local's JVM plus LocalStack's own Python routing layer.
Installation
Homebrew (macOS)
Pre-built binaries
Download from GitHub Releases for Linux (x86_64, aarch64, musl), macOS (Intel, Apple Silicon), and Windows.
# Example: Linux x86_64
|
Cargo
# With encryption support (SQLCipher + vendored OpenSSL)
As a library (Rust)
[]
# Minimal — just the embedded database, no server or CLI dependencies
= { = "0.9", = false, = ["native-sqlite"] }
# Or with encryption:
# dynoxide-rs = { version = "0.9", default-features = false, features = ["encryption"] }
GitHub Actions
- uses: nubo-db/dynoxide@v1
with:
snapshot-url: https://example.com/test-data.db.zst # optional
port: 8000
See action/action.yml for all inputs and outputs.
HTTP Server
Start the server:
With a persistent database:
With encryption (requires the encrypted-server build):
# Generate a key
# Start with key file
# Or via environment variable
DYNOXIDE_ENCRYPTION_KEY=
Then use the AWS CLI or any DynamoDB SDK pointed at localhost:
Works with any language or SDK that supports custom endpoints — Python (boto3), Node.js (AWS SDK v3), Go, Java, etc.
MCP Server
Dynoxide includes an MCP server that exposes DynamoDB operations as tools for coding agents (Claude Code, Cursor, etc.).
stdio transport (default)
Streamable HTTP transport
Claude Code configuration
Add to your mcp.json:
Or with a persistent database:
With a OneTable data model for single-table designs:
Available tools (33)
| Category | Tools |
|---|---|
| Tables | list_tables, describe_table, create_table, delete_table, update_table |
| Items | get_item, put_item, update_item, delete_item |
| Batch | batch_get_item, batch_write_item, bulk_put_items |
| Query | query, scan |
| Transactions | transact_get_items, transact_write_items |
| PartiQL | execute_partiql, batch_execute_partiql |
| TTL | update_time_to_live, describe_time_to_live, sweep_ttl |
| Tags | tag_resource, untag_resource, list_tags_of_resource |
| Streams | list_streams, describe_stream, get_shard_iterator, get_records |
| Snapshots | create_snapshot, restore_snapshot, list_snapshots, delete_snapshot |
| Info | get_database_info |
Safety options
# Read-only mode — rejects all write operations
# Limit query/scan results
Snapshots
The MCP server supports database snapshots for safe experimentation:
create_snapshot— saves a point-in-time copy of the databaserestore_snapshot— rolls back to a previous snapshotlist_snapshots— lists available snapshots- Auto-snapshot before
delete_table(last 10 kept automatically)
Data Model Context
For single-table designs, raw DynamoDB metadata (pk is type S, GSI1 exists) tells an agent almost nothing. The --data-model flag loads a OneTable schema so the agent sees entity names, key templates, GSI mappings, and type discriminator attributes.
With --data-model, the MCP instructions include a compact entity summary and get_database_info returns the full model:
The agent knows which entity types exist, how their keys are structured, and which GSI to query for a given access pattern — before making a single query.
Index name resolution: OneTable uses shorthand keys internally (e.g. gs1). If the index definition includes a name field (e.g. "name": "GSI1"), the parser uses the DynamoDB-facing name so it matches describe_table output and works directly with query --index-name.
Options:
# Control how many entities appear in MCP instructions (default: 20, 0 = suppress)
# With serve --mcp (uses --mcp-data-model prefix)
The data model is context-only — dynoxide does not validate writes against the schema. The instructions note this explicitly so agents don't assume enforcement.
DynamoDB Streams
Dynoxide supports DynamoDB Streams with all four view types: NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES, and KEYS_ONLY.
Enabling streams
Streams are enabled per-table via StreamSpecification in CreateTable or UpdateTable, exactly like real DynamoDB:
# Via AWS CLI
# Enable on an existing table
Via the MCP server, pass stream_specification to create_table or update_table.
Reading stream records
# List streams
# Describe a stream to get shard IDs
# Get a shard iterator and read records
Streams with import
If the --schema file (DescribeTable JSON) contains a StreamSpecification, streams are automatically enabled on the imported table. No extra flags needed — the import faithfully reproduces the source table's configuration:
Note: Imported items do not generate stream records by default (bulk import bypasses stream recording for performance). Stream recording begins for writes made after import completes.
Import CLI
Import data from DynamoDB Export (JSON Lines format) into a Dynoxide database, with optional anonymisation.
Basic import
The --source directory should follow DynamoDB Export structure:
export-data/
├── Users/
│ └── data/
│ └── 00000000.json.gz
└── Orders/
└── data/
└── 00000000.json.gz
The --schema file contains DescribeTable JSON (the output of aws dynamodb describe-table):
Table filtering
Anonymisation
Create a rules file (rules.toml):
[[]]
= "attribute_exists(email)"
= "email"
= { = "fake", = "safe_email" }
[[]]
= "attribute_exists(phone)"
= "phone"
= { = "mask", = 4, = "*" }
[[]]
= "attribute_exists(ssn)"
= "ssn"
= { = "hash", = "ANON_SALT" }
[[]]
= "attribute_exists(notes)"
= "notes"
= { = "redact" }
[]
= ["userId", "email"]
ANON_SALT=my-secret-salt
Action types:
| Action | Description |
|---|---|
fake |
Replace with generated data (safe_email, name, phone_number, address, company_name, sentence, word, first_name, last_name) |
mask |
Keep last N characters, mask the rest (keep_last, mask_char) |
hash |
SHA-256 hash with salt from env var (salt_env, required) |
redact |
Replace with [REDACTED] |
null |
Replace with NULL |
Consistency: Fields listed in [consistency].fields produce the same anonymised value across all tables in a single import run. Same input + same salt = same output.
Options
# Overwrite an existing output file
# Continue importing when a batch fails instead of aborting
# Compress output with zstd
# Produces snapshot.db.zst
Library Usage (Rust)
use Database;
// In-memory (for tests)
let db = memory.unwrap;
// Persistent (backed by SQLite file)
let db = new.unwrap;
// Encrypted (requires `encryption` feature)
// cargo add dynoxide-rs --features encryption
let db = new_encrypted.unwrap;
Operations use DynamoDB-compatible request/response types:
use Database;
use json;
let db = memory.unwrap;
// Create a table
let req = from_value.unwrap;
db.create_table.unwrap;
// Put an item
let req = from_value.unwrap;
db.put_item.unwrap;
// Query
let req = from_value.unwrap;
let resp = db.query.unwrap;
Testing with Embedded Mode
Each test gets a fully isolated database with no shared state:
No Docker. No port conflicts. No table name prefixes. Tests run in parallel without coordination.
Feature Flags
| Flag | Default | Description |
|---|---|---|
native-sqlite |
Yes | Bundles plain SQLite. No OpenSSL. |
http-server |
Yes | Adds axum-based HTTP server exposing the DynamoDB JSON API. |
mcp-server |
Yes | Adds MCP server for coding agents (stdio and Streamable HTTP transports). |
import |
Yes | Adds dynoxide import CLI for importing DynamoDB Export data with anonymisation. |
encryption |
No | Bundles SQLCipher + vendored OpenSSL. Adds Database::new_encrypted() for encryption at rest. |
encrypted-server |
No | Convenience: enables encryption + http-server. |
encrypted-full |
No | Convenience: enables encryption + http-server + mcp-server + import. |
full |
— | Alias for default features (backward compatibility). |
native-sqlite and encryption are mutually exclusive — they select different SQLite backends. To use encryption:
= { = "0.9", = false, = ["encryption"] }
Workspace note: Cargo unifies features across a workspace. If any crate depends on dynoxide-rs with default features (getting native-sqlite) and another uses encryption, both activate and the build fails. Use default-features = false on all dynoxide-rs dependencies in the workspace.
Supported Operations
| Category | Operations |
|---|---|
| Table | CreateTable, DeleteTable, DescribeTable, ListTables, UpdateTable |
| Item | PutItem, GetItem, DeleteItem, UpdateItem |
| Query & Scan | Query, Scan |
| Batch | BatchGetItem, BatchWriteItem |
| Transactions | TransactWriteItems, TransactGetItems |
| PartiQL | ExecuteStatement, BatchExecuteStatement |
| Streams | DescribeStream, GetShardIterator, GetRecords, ListStreams |
| TTL | UpdateTimeToLive, DescribeTimeToLive |
| Tags | TagResource, UntagResource, ListTagsOfResource |
Expression Support
- KeyConditionExpression
- FilterExpression
- ConditionExpression (attribute_exists, attribute_not_exists, begins_with, contains, size, between, in)
- ProjectionExpression
- UpdateExpression (SET, REMOVE, ADD, DELETE)
Additional Features
- Global Secondary Indexes (GSI)
- DynamoDB Streams (NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES, KEYS_ONLY)
- TTL with background sweep
- ReturnConsumedCapacity (TOTAL and INDEXES)
- ReturnValuesOnConditionCheckFailure
- ClientRequestToken idempotency for TransactWriteItems
- PartiQL SELECT, INSERT, UPDATE, DELETE with EXISTS/BEGINS_WITH functions
- Pagination with LastEvaluatedKey/ExclusiveStartKey (1MB page limit)
- Item size validation (400KB limit)
- Transaction size validation (4MB aggregate, 100 action limit)
- Batch size limits (16MB response, 100 keys for get, 25 items for write)
Acknowledgements
Dynoxide's DynamoDB API semantics and validation logic were informed by dynalite, the excellent DynamoDB emulator built on LevelDB by Michael Hart and now maintained by the Architect team.
Dynoxide is a clean-room Rust implementation — no code was ported directly — but dynalite's thorough approach to matching live DynamoDB behaviour, including edge cases and error messages, was an invaluable reference.
Dynoxide utilises SQLite as its storage layer. (A choice validated by AWS's DynamoDB Local, which also uses SQLite internally.)
License
Dual-licensed under MIT and Apache 2.0. See LICENSE-MIT and LICENSE-APACHE.