SQLite CRUD

dbQuery(path, sql, params) runs against an embedded SQLite file in the bext bridge. The schema is created lazily on first request; the loader reads, the action writes, and a 303 redirect re-runs the loader so the list reflects the mutation.

Tip
dbQuery returns rows as positional arrays (not objects) — zip them with r.columns. The .db file is created automatically under .bext/data/; no migrations or configuration needed.

Add a note

Notes (0)

no notes yet — add one above.

src/app/examples/db-crud/page.tsxTSX
// src/app/examples/db-crud/page.tsx
// SQLite via the bext bridge — bundled SQLite, no extra deps.
import { dbQuery } from "@bext-stack/framework/bridge";

const DB = ".bext/data/notes.db";

dbQuery(DB, `CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  body TEXT NOT NULL,
  created_at INTEGER NOT NULL
)`);

export async function loader() {
  const r = dbQuery(DB, "SELECT id, body, created_at FROM notes ORDER BY id DESC LIMIT 20");
  // rows come back as positional arrays — zip with r.columns:
  const notes = r.rows.map((row) =>
    Object.fromEntries(r.columns.map((c, i) => [c, row[i]]))
  );
  return { notes };
}

export async function action({ request }) {
  const form = await request.formData();
  if (form.get("op") === "add") {
    dbQuery(DB, "INSERT INTO notes (body, created_at) VALUES (?, ?)",
      [form.get("body"), Date.now()]);
  } else if (form.get("op") === "delete") {
    dbQuery(DB, "DELETE FROM notes WHERE id = ?", [form.get("id")]);
  }
  return new Response(null, { status: 303, headers: { Location: "/examples/db-crud" } });
}