allez-orm
v1.2.0
Published
AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI
Downloads
97
Maintainers
Readme
AllezORM
Ship a real SQLite database — in the browser — in under five minutes. Zero servers. Zero migrations. Zero "your data is gone because the user refreshed."
Built for the next era of software: client-first apps, AI-generated code, and teams that ship faster than infrastructure can keep up.
Why this exists (read this part)
You've felt all three of these. Recently.
- Your "simple" app needs a real query layer in the browser, and you're
one library choice away from
localStorage.setItem(JSON.stringify(...))creeping into production. Again. - Your AI agent confidently rewrote your schema on Tuesday and you didn't notice until staging. The spec said one thing. The code did another. Nobody owned the diff.
- You shipped a "client-side prototype" that became the product, and now you're trying to graft IndexedDB persistence, foreign keys, and migrations onto something that was never designed for them.
AllezORM is a small, sharp answer to all three.
Open it, point it at a schema, and you have a real SQLite database running in the browser tab — persisted to IndexedDB on every write, queried with prepared statements, protected by inline foreign keys. The schema generator writes idiomatic, version-pinned files that your agents cannot silently drift away from. Because every file is anchored — by SHA-256 — back to the spec it came from.
That's the whole pitch. The rest of this README shows you the receipts.
What you get in 60 seconds
npm install allez-orm// app.js
import { AllezORM } from "allez-orm";
import users from "./schemas/users.schema.js";
import posts from "./schemas/posts.schema.js";
const orm = await AllezORM.init({ dbName: "myapp.db", schemas: [users, posts] });
// Real prepared statements. Real foreign keys. Real persistence.
await orm.table("users").upsert({ id: 1, email: "[email protected]", display_name: "Ada" });
await orm.table("posts").insert({ user_id: 1, title: "Hello world" });
const recent = await orm.query(
"SELECT u.email, p.title FROM posts p JOIN users u ON u.id = p.user_id ORDER BY p.id DESC LIMIT 10"
);That's it. The database is alive in the tab. Refresh the page — it's still there.
Open DevTools → Application → IndexedDB → myapp.db. The whole SQLite file is sitting there as a Uint8Array.
No backend. No service worker. No build step. No "but does it work in Safari" anxiety.
The five things that make it different
1. Spec-anchored schemas your agents can't silently break
Here's what a generated schema file looks like:
// users.schema.js (generated by allez-orm)
// DO NOT EDIT — regenerate by editing the spec, then running:
// allez-orm from-json ../spec.json
// SPEC ../spec.json
// SPEC_SHA256 0f1ab429adc01ddeaa093a4b18a87836547f437e1bc31f5d1babddf9935c47dc
// TABLE_SHA256 531b42a728116ec1332467a5fb83b93f230afaf5ea75f4b4019ef2cf5d997adc
const usersSchema = {
table: "users",
version: 1,
createSQL: `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT
);`
};
export default usersSchema;Every generated file carries two cryptographic anchors back to the spec it came from. When an agent (or a junior dev, or future-you at 2am) opens this file, the very first thing they see is "DO NOT EDIT — regenerate from the spec."
Don't trust the comment? Don't have to. Run drift detection:
$ allez-orm verify schemas.spec.json --dir=./schemas
✔ verify: 4 table(s) match spec schemas.spec.jsonHand-edit a generated file:
$ allez-orm verify schemas.spec.json --dir=./schemas
✗ Drift detected in 1 file(s):
- ./schemas/users.schema.js
To resync, run: allez-orm from-json schemas.spec.json --dir=./schemas -fWire that into CI. Wire it into your pre-commit hook. Your agents do not get to quietly mutate your contract anymore.
You also get an AGENTS.md auto-emitted next to the schemas — picked up
natively by Claude Code, Cursor, and every IDE that respects the convention —
telling future agents exactly where the source of truth lives and how to
regenerate.
This is the part Luca asked about. Now it ships.
2. A SQLite database in the browser, persisted, with FKs that actually enforce
This is not a Promise-flavored wrapper around localStorage. AllezORM runs
real SQLite via sql.js (WASM) — the same engine in your phone, your browser, every
plane in the air, and most cars sold this decade.
PRAGMA foreign_keys = ON— and stays on, even across saves. (We found and fixed a subtle sql.js bug wheredb.export()resets connection PRAGMAs. Now they don't.)- Prepared statements for every helper — no string concatenation, no
injection holes. SQL identifier validation rejects anything that
doesn't match
/^[A-Za-z_][A-Za-z0-9_]*$/, and we test it against the OWASP injection corpus. - Debounced auto-save to IndexedDB after every write. Configurable.
- Versioned
onUpgrade(db, from, to)hook per schema, so you can ship v2 of your app and migrate users in-place.
3. A CLI that generates schemas the way you'd write them by hand
# Ad-hoc — one table, fast:
allez-orm create table users email:text!+ display_name:text! --stamps
# Or declarative — the source-of-truth path:
allez-orm from-json schemas.spec.jsonCompact field syntax that fits in your head: name:type with flag suffixes
(! = NOT NULL, + = UNIQUE, ->target = inline foreign key, --onDelete=
for every FK at once). One mental model. No DSL to memorize.
Spec a referenced table that doesn't exist yet? The CLI auto-generates an ID-only stub for it so your DDL compiles immediately and you can fill it in later.
4. AllezORM Studio — a visual workbench for your in-browser DB
npm run dev opens Studio: a clean, single-page workbench
with searchable tables, sortable columns, a SQL scratchpad, and a
Create table / Add column dialog that calls the CLI for you
and live-reloads the ORM.
It's the kind of tool you wish you had during the first week of a new project. Now you do.
5. Tested with the seriousness of a database
This repo currently ships with 67 automated tests across six suites:
| Suite | What it covers |
|---|---|
| test-cli.mjs | CLI smoke tests, force/overwrite, FK stubs |
| test-security.mjs | SQL identifier injection corpus, prepared-statement integration |
| test-spec-anchor.mjs | Spec-anchor headers, AGENTS.md, drift detection |
| test-generator-thorough.mjs | 21 edge-case generator scenarios, real sql.js compile-and-run, idempotency |
| test-fk-persistence.mjs | Regression: FK pragma survives db.export() and repeated saves |
| test-e2e-studio.mjs | Playwright E2E: full Studio flow including IndexedDB reload, FK violations |
npm test # everything except the browser E2E
npm run test:e2e # browser E2E (requires playwright + chromium)
npm run test:all # bothInstall
npm install allez-ormYou don't need a bundler. You don't need a framework. You don't need a server. The package is pure ESM, runs in browsers natively, and exposes a single default entry plus a CLI:
import { AllezORM } from "allez-orm"— the ORMnpx allez-orm …— the schema generatorimport "allez-orm/cli"— programmatic CLI access
WASM is loaded from sql.js's CDN by default. Want to
self-host the WASM? Pass wasmLocateFile to AllezORM.init.
Quickstart: from zero to a real database in 5 commands
# 1. Init a project
npm init -y && npm install allez-orm
# 2. Generate a schema declaratively
cat > schemas.spec.json <<'JSON'
{
"outDir": "./schemas",
"defaultOnDelete": "cascade",
"tables": [
{ "name": "users", "stamps": true,
"fields": [
{ "name": "email", "type": "text", "unique": true, "notnull": true },
{ "name": "display_name", "type": "text", "notnull": true }
] },
{ "name": "posts", "stamps": true,
"fields": [
{ "name": "title", "type": "text", "notnull": true },
{ "name": "user_id", "type": "integer", "fk": { "table": "users" } }
] }
]
}
JSON
# 3. Generate the schema files (and AGENTS.md)
npx allez-orm from-json schemas.spec.json
# 4. Verify alignment any time you like
npx allez-orm verify schemas.spec.json
# 5. Use it from your app
echo 'import { AllezORM } from "allez-orm";
import users from "./schemas/users.schema.js";
import posts from "./schemas/posts.schema.js";
const orm = await AllezORM.init({ schemas: [users, posts] });
await orm.table("users").upsert({ id: 1, email: "[email protected]", display_name: "Ada", created_at: new Date().toISOString(), updated_at: new Date().toISOString() });
console.log(await orm.query("SELECT * FROM users"));
' > app.mjsYou now have a typed, persistent, foreign-key-enforced database running in any modern browser, generated from a single source-of-truth file.
The CLI
allez-orm create table <name> [fields...] [options]
allez-orm from-json <config.json> [--dir=<outDir>] [-f|--force]
allez-orm verify <config.json> [--dir=<outDir>]
allez-orm --print-json-schemaField syntax (CLI form)
col[:type][flags][->target]
Examples:
email:text!+ email TEXT UNIQUE NOT NULL
user_id:integer->users user_id INTEGER REFERENCES users(id)
parent_id:integer->nodes self-referencing FK; no stub generated
body body TEXT (type defaults to TEXT)Flags: ! = NOT NULL, + = UNIQUE.
Type aliases (case-insensitive): text|string, int|integer, real|float, numeric|number, blob, bool.
Options
| Flag | What it does |
|---|---|
| --dir=<outDir> | Output directory (defaults to schemas_cli, or outDir from spec) |
| --stamps | Add created_at (NOT NULL), updated_at (NOT NULL), deleted_at (nullable) |
| --onDelete=<mode> | Apply ON DELETE behavior to every FK: cascade\|restrict\|setnull\|noaction |
| -f, --force | Overwrite existing files (also: ALLEZ_FORCE=1) |
| --help | Show help |
from-json config shape
Print the canonical JSON Schema:
npx allez-orm --print-json-schemaThe config supports both rich field objects and compact string tokens in the same array — pick whichever fits the table:
{
"tables": [
{ "name": "items", "fields": ["sku:text!+", "qty:integer!"] },
{ "name": "memberships", "fields": [
{ "name": "user_id", "type": "integer", "fk": { "table": "users" } },
{ "name": "org_id", "type": "integer", "fk": { "table": "orgs" } }
]}
]
}verify — your CI safety net
verify re-runs generation in memory and byte-compares against what's on disk.
It exits non-zero on:
- Drift — a generated file was hand-edited.
- Stale files — the spec moved forward but files weren't regenerated.
- Missing files — the spec references a table whose file isn't there.
# .github/workflows/ci.yml
- run: npx allez-orm verify schemas.spec.jsonOne line. No more 2am Slack messages about schema drift.
The ORM API
Init
const orm = await AllezORM.init({
dbName: "myapp.db", // IndexedDB key; defaults to "allez.db"
autoSaveMs: 1500, // debounce window for IndexedDB writes
wasmLocateFile: f => `/wasm/${f}`, // self-host sql-wasm.wasm
schemas: [usersSchema, postsSchema]
});Per-table helpers (parameterized, FK-aware)
const users = orm.table("users");
await users.insert({ email: "[email protected]", display_name: "Ada" });
await users.upsert({ id: 1, email: "[email protected]", display_name: "Ada" });
await users.update(1, { display_name: "Ada Lovelace" });
await users.findById(1); // single row
await users.searchLike("ada", ["display_name"]); // LIKE %ada% across columns
await users.deleteSoft(1); // sets deleted_at / deletedAt
await users.remove(1); // hard deleteRaw SQL — when you need it
const rows = await orm.query("SELECT * FROM users WHERE id IN (?, ?)", [1, 2]);
const one = await orm.get("SELECT * FROM users LIMIT 1");
await orm.execute("UPDATE users SET display_name = ? WHERE id = ?", ["X", 1]);Schema versioning
const usersSchema = {
table: "users",
version: 2,
createSQL: `CREATE TABLE IF NOT EXISTS users (...);`,
async onUpgrade(db, from, to) {
if (from < 2) db.exec(`ALTER TABLE users ADD COLUMN avatar_url TEXT`);
}
};Manual save
await orm.saveNow(); // force a write to IndexedDB right nowSecurity
This package treats SQL identifiers as an attack surface. Every table and
column name passed to the table() helpers goes through safeIdent(),
which validates against a strict character class and rejects every
payload in the OWASP injection corpus. You can audit the test for yourself
at tests/test-security.mjs (13 assertions).
All values are bound via prepared statements. There is no string concatenation of user input into SQL anywhere in the runtime.
If you find a security issue, please open a private advisory in the GitHub repo. We respond.
What you avoid by using this
This is the part that's hard to feel until you've been burned. So picture it:
- No "the IndexedDB schema is one thing, the SQL schema is another" debugging at midnight. One file is the truth. One command verifies it.
- No agents quietly mutating your contract. The header on every generated file is unmissable. The verify command is automatic.
- No
localStorage.setItem(JSON.stringify(...))sprawling across your codebase as you slowly reinvent indexes. The Studio is the indexed database you wanted on day one. - No "we'll add a real backend later" technical debt. You may discover you never need one. Plenty of apps don't.
- No surprise FK behavior. We tested the pragma persistence so you don't have to.
Roadmap & status
AllezORM is production-shape for client-only apps: tests gate every release, the API surface is small and stable, and the schema generator's output format is now SHA-anchored (so we'll bump major if we ever change it).
Coming soon:
- WAL-style append journaling for crash-safe writes
- Optional Web Worker mode for off-main-thread queries
- An
allez-orm synccommand for two-way reconciliation with a remote SQL database (so the same schema works client-side and server-side)
Want one of those sooner? Open an issue and say so.
Contributing
git clone https://github.com/AllezMarketing/allez-orm.git
cd allez-orm
npm install
npm test # unit + integration
npx playwright install chromium
npm run test:e2e # browser E2E
npm run dev # launch Studio at http://localhost:5174PRs that touch generator output must keep the existing schema files
byte-stable (or bump version and update the SHA anchors deliberately).
npm test will tell you immediately.
License
ISC © Allez Marketing LLC
One last thing
If you read this far, you already know what to do. The install is one command. The first table is one config. The verify is one CI line.
The friction between you and a working database is smaller than it has ever been. Use the next ten minutes well.
npm install allez-orm