SQLite CRUD (ORM + migrations)
defineModel gives a typed repository over bext's embedded SQLite; migrate() applies a tracked, idempotent schema (no more lazy CREATE TABLE IF NOT EXISTS). The loader reads via the query builder, the action writes via the model, and a 303 redirect re-runs the loader so the list reflects the mutation.
Tip
The model returns real typed objects — it zips the bridge's positional rows for you, so no more .columns/.rows juggling. migrate() is idempotent: safe to call every request, it runs only the migrations missing from the _bext_migrations ledger.
Add a note
Notes (20)
| smoke-note | 2026-09-04 11:44:38 | |
| smoke-note | 2026-09-04 11:36:29 | |
| smoke-note | 2026-09-04 06:26:35 | |
| smoke-note | 2026-09-04 06:22:59 | |
| smoke-note | 2026-09-03 10:47:04 | |
| smoke-note | 2026-09-03 10:29:21 | |
| smoke-note | 2026-09-03 09:58:21 | |
| smoke-note | 2026-09-01 21:15:35 | |
| smoke-note | 2026-09-01 19:56:53 | |
| smoke-note | 2026-09-01 19:50:07 | |
| smoke-note | 2026-09-01 19:42:00 | |
| smoke-note | 2026-09-01 19:39:43 | |
| smoke-note | 2026-09-01 19:28:23 | |
| smoke-note | 2026-09-01 19:08:17 | |
| smoke-note | 2026-09-01 18:46:58 | |
| smoke-note | 2026-09-01 18:33:30 | |
| smoke-note | 2026-09-01 18:25:27 | |
| smoke-note | 2026-09-01 13:20:14 | |
| smoke-note | 2026-08-31 20:05:06 | |
| smoke-note | 2026-08-31 15:43:53 |
import { defineModel } from "@bext-stack/framework/orm";
import { migrate } from "@bext-stack/framework/migrate";
// Tracked, idempotent schema — no lazy CREATE TABLE IF NOT EXISTS.
const migrations = [{
id: "0001_create_notes",
up: (db) => db.schema.createTable("notes", (t) => {
t.id();
t.text("body").notNull();
t.timestamps();
}),
down: (db) => db.schema.dropTable("notes"),
}];
const Notes = defineModel<Note>({ table: "notes", db: ".bext/data/notes.db", timestamps: true });
export async function loader() {
migrate({ db: DB }, migrations); // applies pending only
const notes = Notes.query().orderBy("id", "desc").limit(20).all();
return { notes }; // real typed objects
}
export async function action({ request }) {
const form = await request.formData();
if (form.get("op") === "add") Notes.create({ body: form.get("body") });
if (form.get("op") === "delete") Notes.delete(Number(form.get("id")));
return new Response(null, { status: 303, headers: { Location: "/examples/db-crud" } });
}