logo
veeso_devChristian Visintin
Featured image

wasm-dbms: a relational database for WebAssembly and the Internet Computer

How to use a relational database inside a WASM module or an Internet Computer canister with wasm-dbms: setup, usage, and benchmarks

October 11, 2026 — 7 min read

Premise

If you landed here, chances are you searched for something like "how do I use a database in WASM" or "relational database on the Internet Computer". I've been looking for an answer for years, and since I couldn't find a decent one, I eventually wrote it myself.

I've already written about the journey, back when the project was still called ic-dbms and later when it became wasm-dbms. This article is about the destination: what wasm-dbms is with version 0.11.0, how to set it up, where it runs, and how it performs compared to the other databases you can use in WASM.

Why databases in WASM are hard

A WebAssembly module is a sandbox: a linear memory and whatever the host decides to give it. No filesystem, no threads, no libc, unless the runtime grants them. Most embedded databases, instead, expect files, mmap, fsync and a C toolchain.

So until now you had basically three options: port SQLite with a filesystem shim, use whatever storage your host provides (and stay tied to that host forever), or build tables by hand on a key-value store. On the Internet Computer it's even worse, since a canister has no filesystem at all, just stable memory.

What is wasm-dbms?

wasm-dbms is an embeddable relational database engine written in pure Rust, designed to run inside WASM runtimes:

  • It runs wherever WASM runs: it builds for wasm32-unknown-unknown with plain cargo. No C toolchain, no WASI, no JavaScript glue.
  • Storage is a trait: the engine reads and writes 64 KiB pages through MemoryProvider, so the same database runs on the heap, on a file, on a key-value store, on IC stable memory, or on your own storage.
  • The schema is Rust code: tables are declarative structs expanded with user-friendly derive macros, and a wrong column type is a compile error.
  • It's relational: foreign keys, joins, ACID transactions, B+ tree indexes, aggregates, schema migrations, and SQL are all built in.

How to set it up

Add the crates to your Cargo.toml:

[dependencies]
wasm-dbms = "0.11"
wasm-dbms-api = "0.11"
wasm-dbms-memory = "0.11"

Then define your tables as Rust structs:

use wasm_dbms::prelude::*;
use wasm_dbms_api::prelude::*;

#[derive(Debug, Table, Clone, PartialEq, Eq)]
#[table = "users"]
pub struct User {
    #[primary_key]
    pub id: Uint32,
    #[sanitizer(TrimSanitizer)]
    #[validate(MaxStrlenValidator(100))]
    pub name: Text,
    #[unique]
    #[validate(EmailValidator)]
    pub email: Text,
}

#[derive(Debug, Table, Clone, PartialEq, Eq)]
#[table = "posts"]
pub struct Post {
    #[primary_key]
    pub id: Uint32,
    pub title: Text,
    #[foreign_key(entity = "User", table = "users", column = "id")]
    pub user_id: Uint32,
}

#[derive(DatabaseSchema)]
#[tables(User = "users", Post = "posts")]
pub struct MySchema;

Validators and sanitizers run on every write, #[unique] columns get an index automatically, and you can add more with #[index].

How to use it

Create a context with a memory provider, register the tables and you're ready to go:

let ctx = DbmsContext::new(HeapMemoryProvider::default());
MySchema::register_tables(&ctx)?;

let database = WasmDbmsDatabase::oneshot(&ctx, MySchema);

database.insert::<User>(UserInsertRequest {
    id: 1.into(),
    name: "  Alice ".into(), // trimmed by the sanitizer
    email: "alice@example.com".into(),
})?;

let users = database.select::<User>(
    Query::builder()
        .filter(Filter::eq("email", Value::Text("alice@example.com".into())))
        .build(),
)?;

UserInsertRequest and UserUpdateRequest are generated by the Table macro, so every write is checked by the compiler.

Need atomicity across multiple operations? Open a transaction:

let tx = ctx.begin_transaction();
let database = WasmDbmsDatabase::from_transaction(&ctx, MySchema, tx);

database.insert::<User>(bob)?;
database.insert::<Post>(first_post)?;

database.commit()?;

Transactions are identified by an ID and live in the context, so in call-based runtimes, such as canisters, a client can open a transaction, perform several calls, and commit at the end.

And if you prefer SQL, add the wasm-dbms-sql crate:

let engine = SqlEngine::new(MySchema);

let result = engine.execute(
    &ctx,
    None,
    "SELECT u.name, p.title FROM users u JOIN posts p ON p.user_id = u.id WHERE u.id = ?",
    &[Value::from(1u32)],
)?;

Joins, aggregates with GROUP BY and HAVING, and schema migrations with drift detection are covered in the documentation.

Where does it run?

Since the engine only needs a MemoryProvider, the choice of runtime is up to you:

Environment Memory provider Persistence
Internet Computer canisters ic-dbms stable memory provider Stable memory, survives upgrades
Wasmtime, Wasmer, WasmEdge (WASI) WasiMemoryProvider A single file
Hosts with wasi:keyvalue WasiKeyValueMemoryProvider Key-value bucket, checkpoints
Browsers and custom runtimes Your own MemoryProvider Whatever you implement

Internet Computer

With ic-dbms, you derive two macros, and you get a complete database canister, with a typed Candid API for every table, transactions, access control per principal, and migrations:

#[derive(DatabaseSchema, DbmsCanister)]
#[tables(User = "users", Post = "posts")]
pub struct IcDbmsCanisterGenerator;

ic_cdk::export_candid!();

You then call it with the typed clients of ic-dbms-client, from another canister, from an off-chain application through ic-agent, or from PocketIC tests. Of course, you can also embed wasm-dbms directly in your own canister, which is what I've done in Mastic.

WASI runtimes

On Wasmtime, Wasmer, and WasmEdge, you just swap the memory provider, and the pages are stored in a file:

use wasi_dbms_memory::WasiMemoryProvider;

let ctx = DbmsContext::new(WasiMemoryProvider::new("./data/example.db")?);

Any language, through the Component Model

wasm-dbms also ships a WIT interface (wit/dbms.wit), so you can build your database as a WASM component and use it from a host written in Go, Python, JavaScript, or any language with Component Model tooling. The repository includes a complete example with a Wasmtime host.

How does it perform?

The repository includes a Criterion suite comparing wasm-dbms with SQLite (through rusqlite) and DuckDB, all in memory. It runs natively, since that's the only way to compare against native SQLite and DuckDB on a level playing field.

Workload wasm-dbms SQLite DuckDB
Insert one record 228 µs 3.26 µs 585 µs
Read one record by primary key 11.6 µs 0.77 µs 290 µs
Update one record 531 µs 2.84 µs 391 µs
Delete one record 305 µs 14.6 µs 2.92 ms
Bulk insert of 1,000 records 167 ms 1.09 ms 453 ms
Filtered query on 10,000 records 2.87 ms 1.58 ms 2.02 ms
Join of 100 users with 10,000 posts 57.7 ms 4.81 ms 7.80 ms
Transaction with 100 inserts 13.1 ms 65.4 µs 35.7 ms

Let's be honest: SQLite wins everywhere. It's written in C, and it's been optimized for over 20 years, while wasm-dbms is less than one year old. If raw speed is all you care about and you can run SQLite on your target, and you don't need features that wasm-dbms has and SQLite doesn't, run SQLite.

But wasm-dbms beats DuckDB on most transactional workloads: reads by primary key are about 25 times faster, deletes about 9.5 times, and inserts and transactions about 2.7 times. Reads are its strong point, with a filtered scan less than twice as slow as SQLite. Updates and joins are the weak spots, and that's where I'll focus next.

When should you use it?

Pick wasm-dbms if you want a typed, relational database living inside your module, built with a plain Rust toolchain, that runs on any WASM runtime, including the Internet Computer, where it's the only option that generates a whole database canister from your Rust types.

The full comparison with every alternative is available in the documentation.

Conclusion

When I started working on ic-dbms in November 2025, I just wanted a decent relational database for my canisters. Eleven months later, it's a pure Rust relational engine running on the Internet Computer, on WASI runtimes, behind a WIT interface for any language, with many cool features, and with good overall performance. It won't replace SQLite, and it was never meant to, but if you need a portable relational database inside a WASM module, give it a try:

And if you build something with it, let me know; I'd love to see it. Issues, feedback, and stars are always welcome.