Library / SDK
vincent-herlemont/native_db avatar
vincent-herlemont/native_db

native_db: a typed embedded database for Rust with real-time subscriptions

Drop-in embedded database in Rust

718 stars41 forksRustMIT

At a glance

What is it?
native_db is a Rust embedded database built on redb, aimed at multi-platform applications, where the unusual part is the typed model layer: you derive macros on your structs, the keys become typed queries, and the schema migrates itself as you change your model. The transaction and durability story comes from redb, the ergonomics come from native_model, and the readme is candid that the API is not stable yet.
Who is it for?
Adopt native_db if you are writing a Rust application that needs a local store with a schema you can change without a migration framework, and you value compile-time typed keys over SQL, since the model derive macros turn struct fields into index definitions and the readme points to a major version upgrade guide for handling API breaks.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The model derive macros are the actual API

native_db is described as a drop-in, fast, embedded database for multi-platform apps, server, desktop and mobile, with Rust types synced effortlessly. The API is simple, and the interesting part is how the schema is defined.

A struct becomes a table through two derive attributes. The native_model attribute registers the model with an id and a version, and the native_db attribute tells the database to build the table and its indexes:

rust
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
#[native_db]
struct Item {
    #[primary_key]
    id: u32,
    #[secondary_key]
    name: String,
}

So the primary key and the secondary key are not declared in a schema file or a migration. They are attributes on the fields, and the compiler generates the table definition from them.

The models are then registered once, in a static, with a comment that explains the lifetime requirement, that the lifetime of the models needs to be longer or equal to the lifetime of the database, and that in many cases it is simpler to use a static variable but it is not mandatory:

rust
static MODELS: Lazy<Models> = Lazy::new(|| {
    let mut models = Models::new();
    models.define::<Item>().unwrap();
    models
});

That registration step is the price of the approach. The schema is derived from Rust types, so the types have to exist and be registered before the database can be opened, which means a dynamic plugin that wants its own table has to be registered in the same static set.

Transactions, and the redb underneath

The transaction model is conventional and the durability guarantee is delegated. Thread-safe and fully ACID-compliant transactions are provided by redb, which the Cargo.toml pins to exactly 3.0.2 with an equals sign, and the rest of the dependency list is short: native_db_macro at the same version as the crate, thiserror 2.0.0, serde 1.0, native_model 0.6.2 and semver 1.

The usage pattern is the standard one. A read-write transaction is opened, records are inserted, and the transaction is committed:

rust
    let rw = db.rw_transaction()?;
    rw.insert(Item { id: 1, name: "red".to_string() })?;
    rw.insert(Item { id: 2, name: "green".to_string() })?;
    rw.insert(Item { id: 3, name: "blue".to_string() })?;
    rw.commit()?;

And a read-only transaction retrieves a record by primary key and then scans a secondary index:

rust
    let r = db.r_transaction()?;
    let retrieve_data: Item = r.get().primary(3_u32)?.unwrap();
    println!("data id='3': {:?}", retrieve_data);
    for item in r.scan().secondary::<Item>(ItemKey::name)?.start_with("red")? {
        println!("data name=\"red\": {:?}", item);
    }

The two things to notice are the typed key on the primary lookup, where 3_u32 rather than a bare integer ties the query to the field type, and the range syntax on the secondary key, where start_with("red") is a prefix scan over the name index rather than a string comparison in a filter.

This is an embedded store, which means the database lives in the process that uses it. There is no server to connect to, no wire protocol and no second process sharing the file. That is the trade against redb or SQLite in a mobile or desktop app, and it is the reason the readme calls the project a drop-in.

Automatic model migration, and the upgrade path it forces on you

Automatic model migration is listed among the features with a star, and it is the feature with the most consequences for how you version your code.

The mechanism is visible in the model attribute, where the model is declared with an id and a version. The version is what lets the store know that the on-disk shape of a table is no longer what the code expects, and lets the library update it. The repository ships an examples/upgrade.rs file and a whole examples/major_upgrade/ directory, with a README that the main readme links as the Major Version Upgrade Guide.

That directory is the part to look at before adopting. A major version upgrade of a Rust crate usually means changed APIs, and here it also means a changed on-disk format, because the models themselves carry versions. The Cargo.toml dev-dependencies include a test named breaking_release_migration::from_0_5_x_to_0_6_x, referenced in a comment in the justfile, which is a hint that upgrading across versions is something the project treats as a first-class concern rather than an afterthought.

The status section says it plainly: active development, the API is not stable yet and may change in the future. For an application with data on a user's device, that combination of automatic migration and an unstable API deserves a plan before you ship.

Real-time subscriptions, hot snapshots and the index caveats

Two features distinguish native_db from a plain key-value store. The first is real-time subscription with filters for insert, update and delete operations, which lets a UI subscribe to a table and be told what changed rather than polling.

The second is hot snapshots, which is the mechanism that makes the previous feature affordable. If a subscriber can be woken the moment a write commits, the store does not need to be re-read to notice the change, and a snapshot of the current state can be taken without quiescing the writers.

The index model is where the caveats live. There is support for multiple indexes, primary, secondary, unique, non-unique and optional. The one restriction documented in the readme is specific and worth noting: optional secondary keys with None values cannot be queried using range syntax, with a pointer to the documentation for details. In practice that means a nullable indexed field is fine for exact lookups but cannot participate in a prefix or range scan, which is a real constraint if you model optional data.

Type safety is the other promised property, described as ensuring query type safety to prevent unexpected results caused by selecting with an incorrect type. It is the payoff of deriving the table from the struct, and it is why the query for id 3 has to be written with the right numeric type.

Serialization is your choice, and the benchmarks are comparative

Transparency of serialization and deserialization is handled by native_model, and the readme is explicit that you can use any serialization library you want, naming bincode, postcard, or your own.

That matters for the object model, because the store persists your struct instances rather than rows. The database does not interpret your types, it asks the serializer to turn them into bytes and back, so the format on disk is whatever your chosen library produces. The dev-dependencies include bincode 2.0.1 with the serde feature, and the uuid and chrono crates with serde, which are the test fixtures for that path.

The performance claim is comparative and the readme says so: fast, see sqlite vs redb vs native_db benchmarks, with the benchmarks README in the benches/ directory. The Cargo.toml registers a bench named all with harness = false, which is the convention for criterion benchmarks, and criterion 0.7.0 is in the dev-dependencies. The dev-dependency list also includes rusqlite 0.37.0 with the bundled feature, which is how the SQLite comparison is run.

Treat those numbers as a comparison on the project's own workload. An embedded store's performance depends heavily on the shape of your keys and the size of your values, and the bench directory is where you can look at what was actually measured before deciding.

Cross-platform testing, and who is using it

The badge row at the top of the readme is the build matrix, and it is wider than most Rust libraries manage: separate Linux, macOS, Windows, iOS and Android build and test workflows, each triggered on push to main.

The justfile shows how that matrix is driven locally. The default shell is nu, and there are separate targets for the three feature configurations, since the crate has an optional tokio dependency:

code
build_no_default *args:
    cargo build --no-default-features {{args}}

build_default *args:
    cargo build {{args}}

build_with_optional *args:
    cargo build -F tokio {{args}}

The mobile side uses cargo-dinghy, with a target to list all devices, a target to list all platforms, and per-OS recipes for booting an iOS simulator with xcrun simctl and running tests with cargo dinghy -d iphone test. There is a separate test_ios_lib recipe for running library-only tests, with a comment noting it excludes doctests to avoid cargo-dinghy doctest packaging issues in CI.

The tokio feature is the async story. The Cargo.toml marks it optional with the sync feature only, and the manifest carries TODO comments for channels with futures and for channels with crossbeam, so the async surface is deliberately narrow at this version.

Two projects are listed as using native_db, polly-scheduler and Oku, and the readme invites a pull request to add yours.

Where it does not fit

There is a clear boundary, and it is worth being explicit because embedded databases are often sold as general-purpose.

This is a single-process object store. There is no server, no client protocol and no way for a second process to attach to the same file, so the multi-process patterns that make SQLite attractive in a backend do not apply. There is also no query language, so a question of the shape of joining four tables and aggregating over them is not something you express here.

The typed model layer, which is the appealing part, is also a constraint on schema evolution. Tables are derived from Rust structs, and the registry of models is a static, so a plugin that wants to add its own table has to be known at compile time and registered in that static. For a desktop or mobile application with a fixed set of types, that is a good trade. For a system whose schema is defined at runtime, it is not.

The version stability question is the other one. The readme states the API is not stable yet and may change, and the crates.io listing shows the last release as 0.8.2 from 2025-07-08, with the repository last pushed on 2026-10-19. For an application shipping to devices with data on disk, an unstable API plus an automatic migration system is a combination you should test rather than assume.

The honest summary is that this is a well-shaped library for the case its author had in mind, and that case is an application with a typed object model and a local store, not a general database.

Editorial conclusion

Adopt native_db if you are writing a Rust application that needs a local store with a schema you can change without a migration framework, and you value compile-time typed keys over SQL, since the model derive macros turn struct fields into index definitions and the readme points to a major version upgrade guide for handling API breaks. Do not adopt it if your application needs relational joins across tables, a query language, or a server another process can connect to, because this is a single-process embedded store with an object model rather than a relational engine. Before you build on it, read the major version upgrade guide in examples/major_upgrade, because the readme states the API is not stable yet and may change, and check the sqlite versus redb versus native_db benchmarks in benches/README.md against your own workload before assuming the performance claim applies to you.

Frequently asked questions

How do I add native_db to a Rust project?

Add it to your Cargo.toml, with native_db = "0.8.1" and native_model = "0.4.20" as the readme shows. The crate itself is native_db 0.8.1, and the Cargo.toml in the repository depends on native_model 0.6.2, so check which pair you want to pin.

How does native_db define tables and indexes?

With derive macros on your structs. The native_model attribute registers the model with an id and a version, the native_db attribute creates the table, and the primary_key and secondary_key field attributes define the indexes. The models are then registered in a static Models set before the database is opened.

Is native_db ACID compliant and is it safe to use from multiple threads?

Yes. The readme states the transactions are thread-safe and fully ACID-compliant, and they are provided by redb, which the Cargo.toml pins to exactly 3.0.2. Transactions are opened as read-write with rw_transaction and read-only with r_transaction, and committed explicitly.

What limits does native_db have on secondary indexes?

Optional secondary keys with None values cannot be queried using range syntax, and the readme points to the documentation for details. Beyond that, the library supports primary, secondary, unique, non-unique and optional secondary indexes, with typed queries that prevent selecting with an incorrect type.

Is the native_db API stable?

Not yet. The status section says active development, that the API is not stable yet and may change in the future. The repository ships a major version upgrade guide in examples/major_upgrade and a Major Version Upgrade Guide is linked from the readme, and the crates.io listing shows the last release as 0.8.2 from 2025-07-08.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. vincent-herlemont/native_db on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vincent-herlemont-native-db.svg)](https://hysenlabs.com/projects/vincent-herlemont-native-db)

Was this useful?