Define Zod tables.
Write raw SQL.
Get typed objects.
The missing link between SQL and typed data β a thin, forward-only layer over Zod and your SQL driver.
npm install @b9g/zen zod better-sqlite3You write SQL directly with tagged templates β no .where().orderBy().limit() chains. Helpers handle the tedious parts without hiding or limiting your queries.
Tables aren't classes. They're Zod-powered singletons that validate writes, generate DDL, and deduplicate joined data. No lazy loading, no session-wide identity map.
ZenDB is an open-source library, not a venture-backed SaaS. There will never be a managed instance or a βZen Studioβ β just a thin wrapper around Zod and your SQL driver.
Insert, update, and query return fully-typed rows inferred straight from your Zod schema β including DB-computed defaults via RETURNING.
all() deduplicates rows by primary key and resolves references() into real object graphs β forward and reverse β with zero N+1 queries.
IndexedDB-style upgradeneeded events with safe, additive-only helpers: ensureTable, ensureConstraints, copyColumn.
One template API across three dialects. Drivers build native placeholders (? / $1) β no SQL parsing, no lock-in.
Every insert and update is validated against your schema. Reads are never re-validated, so queries stay fast.
JOINs, CTEs, window functions, dialect-specific syntax β write whatever SQL you need. Normalization is driven by table metadata, not query shape.
Define tables, open the database, and query. That's the whole loop.
import {z, table, Database} from "@b9g/zen";
import SQLiteDriver from "@b9g/zen/sqlite";
// 1. Define tables with Zod schema
const Users = table("users", {
id: z.string().uuid().db.primary().db.auto(),
email: z.string().email().db.unique(),
name: z.string(),
});
const Posts = table("posts", {
id: z.string().uuid().db.primary().db.auto(),
authorId: z.string().uuid().db.references(Users, "author"),
title: z.string(),
published: z.boolean().db.inserted(() => false),
});
// 2. Open the database and create tables
const db = new Database(new SQLiteDriver("file:app.db"));
db.addEventListener("upgradeneeded", (e) => {
e.waitUntil(db.ensureTable(Users).then(() => db.ensureTable(Posts)));
});
await db.open(1);
// 3. Insert with validation β id is auto-generated
const user = await db.insert(Users, {email: "alice@example.com", name: "Alice"});
// 4. Write raw SQL, get typed & normalized objects back
const posts = await db.all([Posts, Users])`
JOIN "users" ON ${Users.on(Posts)}
WHERE ${Posts.cols.published} = ${true}
`;
posts[0].author?.name; // "Alice" β resolved from the JOIN, fully typed