Documentation menu

Embedded SDK

Dynoxide can be used as a Rust library, calling the database directly without any HTTP overhead. Operations run in-process using typed requests, without the HTTP round trip.

It also runs on platforms where DynamoDB Local can't, including iOS.

Building the crate needs Rust 1.88 or later.

Add the dependency

[dependencies]
dynoxide-rs = { version = "3.1", default-features = false, features = ["native-sqlite"] }

For encrypted storage:

[dependencies]
dynoxide-rs = { version = "3.1", default-features = false, features = ["encryption"] }

Basic usage

use dynoxide::Database;

// In-memory database (great for tests)
let db = Database::memory().unwrap();

// Persistent, file-backed
let db = Database::new("data.db").unwrap();

// Encrypted (requires the encryption feature)
let db = Database::new_encrypted("data.db", "your-hex-key").unwrap();

The API uses typed DynamoDB-compatible request and response structs from dynoxide::actions. You can construct them directly or deserialise DynamoDB JSON into them with serde_json::from_value. You call the same operations you'd call over HTTP - CreateTable, PutItem, Query, and so on - but without the network round trip.

Serving HTTP from your own program

With the http-server feature (on by default), server::serve_with_shutdown serves the DynamoDB API on a listener you bind until a future you supply resolves. It then stops accepting, closes idle keep-alive connections, lets running requests finish and returns, so the database is released when it does. It arrived in crate 3.1.0.

use dynoxide::Database;

#[tokio::main]
async fn main() -> Result<(), String> {
    let db = Database::new("data.db").map_err(|e| e.to_string())?;
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8000")
        .await
        .map_err(|e| e.to_string())?;
    let (stop, stopped) = tokio::sync::oneshot::channel::<()>();

    // Hand `stop` to whatever decides when to shut down.
    tokio::spawn(async move {
        tokio::time::sleep(std::time::Duration::from_secs(60)).await;
        let _ = stop.send(());
    });

    dynoxide::server::serve_with_shutdown(listener, db, async {
        let _ = stopped.await;
    })
    .await
}

Before it existed, stopping an embedded server meant aborting its task. That left the connection tasks holding the database, so a file-backed database's files stayed open, and Windows wouldn't let you delete them.

It installs no signal handler; server::start is the one that stops on Ctrl-C or SIGTERM, and now runs on serve_with_shutdown itself. If you delete a file-backed database straight after it returns, drop your own handle first and retry the delete briefly: the last connection task can let go of its clone a moment after the return. A request that never finishes holds the shutdown for as long as it runs. The rustdoc on serve_with_shutdown has the detail.

Upgrading to crate 3.1.0

Product 1.4.0 ships with crate 3.1.0. It adds server::serve_with_shutdown and changes no public types, but a few requests built in Rust are now checked the way the same request sent over HTTP is checked:

  • Database::scan and Database::query refuse a Limit of 0. Database::scan also refuses a Select or ReturnConsumedCapacity outside its enum and a Segment above 999999. Scan's member errors now come from Database::scan rather than from decoding the request, so a ScanRequest with a Select of BAD decodes.
  • An empty UpdateExpression comes back as DynoxideError::EnvelopedValidation rather than ValidationException. The message is unchanged.
  • Database::search_vectors refuses an IndexName shorter than three characters, an empty SearchVector or a TopK below 1 with the same message as over HTTP, where it used to let them through to fail later.

The database schema stays at version 11, so a file 1.3.0 opened needs no further upgrade. Coming from crate 2.x, read the 3.0.0 notes below as well.

Upgrading to crate 3.0.0

Product 1.3.0 ships with crate 3.0.0. The two version streams are separate: use the crate number for Cargo, and the product number for binaries, npm and containers.

The Rust API changes are:

  • expressions and partiql are private. Send expressions and PartiQL statements through Database and the public request types in actions. Code importing the parsers needs to move to those entry points.
  • actions::execute_transaction::ParameterizedStatement and actions::execute_statement::ExecuteStatementRequest gain return_values_on_condition_check_failure.
  • actions::batch_execute_statement::BatchStatementError gains item. Add the fields to struct literals, or use ..Default::default() where appropriate.
  • actions::index_capacity::WriteCapacity::condition_check takes the table's vector indexes so it can charge a vector at its billed size.

The minimum supported Rust version remains 1.88 for the crate and every build of it. Running the test suite needs Rust 1.94.1 because of its AWS SDK development dependencies; that requirement does not apply to installing or embedding Dynoxide.

AttributeValue serialisation also sorts set members into DynamoDB's order, and write normalisation sorts sets in place, so don't rely on their construction order surviving serde.

Product 1.3.0 brings persistent databases to schema version 11. Keep a copy before upgrading if the file has a multi-attribute GSI or a vector index and you may return to 1.2.x. Files without either still read correctly in 1.2.x. The storage guide explains the affected key shapes, encrypted and browser files, newer-schema refusals and restoring older snapshots.

Embedded server output

ServerOptions::quiet() suppresses informational startup and shutdown messages. Pass it to server::start_with_options or mcp::serve_http_with_options; ServerOptions is available at the crate root and as server::ServerOptions. Warnings and errors still print. This is the embedded equivalent of --log quiet.

Table and index statistics

Storage::item_count_and_size, gsi_item_count_and_size, lsi_item_count_and_size and vector_item_count_and_size return the live counts and sizes used by DescribeTable. See capacity and statistics for how vectors are sized.

Upgrading to crate 2.0.0

The crate moved to 2.0.0 while the product stayed at 1.2.0. They are separate version streams, so cargo install dynoxide-rs --version 1.2.0 finds nothing - 1.2.0 is the product number.

One source-breaking change, and only for library consumers:

  • ImportCommand gained a data_model field, carrying the OneTable schema that rebuilds keys after anonymisation. Struct literals constructing one need the field adding.

The DynamoDB wire API and the CLI, server and MCP surfaces are unchanged.

Upgrading to 0.12

0.12.0 is source-breaking for library consumers, and only for them. The DynamoDB wire API and the CLI, server and MCP surfaces are unchanged, so if you run the binary rather than depend on the crate, nothing here affects you.

Two public types gained a field and became #[non_exhaustive]:

  • partiql::parser::Statement - the Update and Delete variants carry a returning field, which is what makes the new PartiQL RETURNING support work.
  • actions::batch_execute_statement::BatchStatementResponse - gained a table_name field.

This is historical guidance for 0.12. The partiql parser module is private from crate 3.0.0; use the public action request types instead.

Feature flags

Flag Default Description
native-sqlite Yes Bundles plain SQLite. No OpenSSL.
http-server Yes The axum HTTP server exposing the DynamoDB JSON API.
mcp-server Yes The MCP server, over stdio and Streamable HTTP.
import Yes The dynoxide import CLI, with anonymisation.
encryption No SQLCipher with vendored OpenSSL. Adds Database::new_encrypted().
encryption-cc No As encryption, but Apple CommonCrypto instead of OpenSSL. For macOS and iOS.
encrypted-full No encryption plus the server, MCP and import features.
wasm-sqlite No The wasm32 browser backend. Pulls in neither native SQLite nor the CLI.

native-sqlite, encryption and wasm-sqlite are mutually exclusive - they select different SQLite backends.

This one bites in workspaces. Cargo unifies features across a workspace, so if one crate depends on dynoxide-rs with default features (picking up native-sqlite) and another asks for encryption, both activate and the build fails with a backend conflict. Set default-features = false on every dynoxide-rs dependency in the workspace.

Testing

The embedded mode is particularly useful for integration tests. Each test can spin up its own Database::memory() instance - there's no shared state, no port conflicts, and startup is roughly 0.2ms.

#[test]
fn test_user_creation() {
    let db = Database::memory().unwrap();
    // create table, put item, assert...
}

No test fixtures, no cleanup, no waiting for a server to start. The database exists for the lifetime of the Database value and is dropped automatically.

For the full API surface and feature flags, see the crate documentation on crates.io.