A typed, checksummed, single-file database. Zero dependencies,
no_std, and it runs in your browser through WebAssembly.
A whole database in one portable file. Every column has a declared type, every value is checked before it is stored, and every file carries a checksum so corruption is reported rather than silently loaded.
Columns declare a type. A value that does not fit is refused, not coerced.
Magic number, format version and CRC-32. A damaged file is an error, never bad data.
The Rust crate pulls in nothing. Neither does the JavaScript package.
no_stdNeeds only
alloc. Runs on bare metal and in the browser.
A snapshot plus an append-only journal, so a crash loses nothing you were told was written.
Group writes so they all happen or none do — one journal record, atomic by construction.
use safe_en::{database, query, row};
let mut db = database! {
name: "shop",
orders {
id: I64,
customer: String,
city: String,
total: F64,
coupon: String?, // nullable
tags: [String], // array
visits: I64 = 0_i64, // default
}
};
let orders = db.table("orders").unwrap();
orders.insert(row![1_i64, "Ahmet", "İzmir", 249.90_f64, "WELCOME", vec!["vip"], 0_i64])?;
for order in orders.get_where(query!(city == "İzmir" && total > 100.0)) {
println!("{}", order);
}
db.save("shop.sfn")?;
use safe_en::storage::{FileStorage, SyncPolicy};
use safe_en::{fields, Durable, Filter};
let mut db = Durable::open(FileStorage::new("app.sfn"))?
.with_sync_policy(SyncPolicy::Always);
db.transaction(|tx| {
tx.set("accounts", &Filter::col("name").eq("ahmet"), fields! { balance: 40_i64 })?;
tx.set("accounts", &Filter::col("name").eq("ayse"), fields! { balance: 60_i64 })?;
Ok(())
})?;
A transaction is written as one journal record, so the length-and-checksum framing that already wraps every record gives atomicity for free. Kill the process mid-write and the group is discarded whole.
The same database compiled to WebAssembly. No bindgen, no bundler, no runtime dependency — the module has zero imports, so one build runs unchanged in the browser, Node, Deno and Bun.
npm install safe-en
import { create } from "safe-en";
const db = await create("shop");
db.createTable("orders", {
id: "I64 primary", // unique and never null
customer: "String",
total: "F64 range(0, 100000)", // inclusive bounds
coupon: "String? max_len(32)",
placed: "Timestamp", // takes a Date, reads back as ISO-8601
});
db.table("orders").insert({ id: 1, customer: "Ahmet", total: 249.9,
coupon: null, placed: new Date() });
db.table("orders").select({ total: { gt: 100 } });
db.table("orders").select(c => c("coupon").isNull().or(c("total").gt(1000)));
const bytes = db.toBytes(); // the whole persistence interface
The demo is a small database browser — create tables, insert rows, edit cells, run queries, and store the result in your own browser's IndexedDB or localStorage. Everything runs locally; nothing is uploaded.
Open the demo| Target | How |
|---|---|
| Native Rust | Default features; save and load use the filesystem |
| Bare metal | --no-default-features; verified against thumbv7em-none-eabihf |
| Browser | WebAssembly; persist to IndexedDB or localStorage |
| Node / Bun | The npm package, or the filesystem directly |
| Deno | The same package; filesystem or Deno KV |
Without a filesystem the whole interface is
to_bytes and from_bytes. Anywhere you can keep
bytes, you can keep a database.
On the web the stored bytes belong to the user, who can edit them. The loader never panics on malformed input — which matters most in WebAssembly, where a panic aborts the whole module with no unwinding to catch it. Every byte of a database is corrupted at every offset in the test suite, and truncated at every length.
The binary format is not a security measure. It is readable in DevTools in seconds, and the CRC-32 catches accidental corruption but is trivially recomputed, so it is not tamper-evidence. If you need that, verify an HMAC server-side; a key cannot be hidden in a WebAssembly bundle.