{"id":51106707,"url":"https://github.com/ul0gic/sqlite-deno","last_synced_at":"2026-06-24T14:31:08.953Z","repository":{"id":361257622,"uuid":"1253744653","full_name":"ul0gic/sqlite-deno","owner":"ul0gic","description":"A true-Deno SQLite. WASM-based, no FFI, honors Deno permission model. Build-in-public: engine (Deno-FS VFS + both concurrency modes + crash recovery) proven; public API in progress.","archived":false,"fork":false,"pushed_at":"2026-06-08T02:58:27.000Z","size":815,"stargazers_count":3,"open_issues_count":14,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T04:22:15.706Z","etag":null,"topics":["database","deno","edge","sqlite","typescript","vfs","wal","wasm","webassembly"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ul0gic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T19:16:05.000Z","updated_at":"2026-06-08T02:58:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ul0gic/sqlite-deno","commit_stats":null,"previous_names":["ul0gic/sqlite-deno"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ul0gic/sqlite-deno","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ul0gic%2Fsqlite-deno","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ul0gic%2Fsqlite-deno/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ul0gic%2Fsqlite-deno/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ul0gic%2Fsqlite-deno/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ul0gic","download_url":"https://codeload.github.com/ul0gic/sqlite-deno/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ul0gic%2Fsqlite-deno/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34737397,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","deno","edge","sqlite","typescript","vfs","wal","wasm","webassembly"],"created_at":"2026-06-24T14:31:07.873Z","updated_at":"2026-06-24T14:31:08.943Z","avatar_url":"https://github.com/ul0gic.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sqlite-deno\n\n\u003e **A true-Deno SQLite, WASM-based, with zero compromises to Deno's permission model.** No FFI. No\n\u003e network. No native modules. No runtime downloads. The same single artifact runs everywhere Deno\n\u003e runs, including Deno Deploy and the edge.\n\nThis is a **build-in-public** repository. The engine, a pure-TypeScript Deno-filesystem VFS over the\nSQLite team's official WebAssembly build, with two concurrency modes and crash recovery, is built\nand tested against a deterministic crash/concurrency harness. The **public API has landed** —\n`openDatabase`, `Database`, typed `Statement\u003cRow\u003e`, transactions — and as of Phase 8 the **full\nL1–L5 test suite** drives that shipped surface (functional, permission, crash/durability,\nconcurrency, and fuzz). It is **not yet published to JSR** (that is Phase 10), so you run it from a\ncheckout, not an install. Please read the status section before anything else, so you know exactly\nwhat works today.\n\n---\n\n## Status: tested \u0026 proven (Phase 8), not yet published\n\n**Phase 8 of 10 complete.** You can `import` from [`src/mod.ts`](./src/mod.ts) and drive a real,\ntyped `openDatabase` / `Database` / `Statement\u003cRow\u003e` surface, and the full L1–L5 test suite now\nproves that shipped surface end to end. What is **not** done is the reproducible byte-identical wasm\nbuild (Phase 9) and the JSR release (Phase 10) — so there is **no `jsr:` install to point at yet**.\nThe usage below is the real API shape; it runs from a clone.\n\n|                                  |                                                                                                                                                                                                                                                                                                                                   |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Working and tested**           | The public API (`openDatabase`, `Database`, typed `Statement\u003cRow\u003e`, transactions, typed errors). The Deno-filesystem VFS (pure TypeScript, over `Deno.*Sync`). Both concurrency modes. Crash / power-loss recovery, on Linux (model-bounded, see [the caveats](#the-honest-capability-envelope-the-asterisks-each-with-the-why)). |\n| **Not built**                    | The reproducible byte-identical wasm build (Phase 9). The JSR release (Phase 10) — not published yet, so no `jsr:` install.                                                                                                                                                                                                       |\n| **Vendored, not yet self-built** | The wasm is the official `@sqlite.org/sqlite-wasm` `3.53.0-build1`, committed in-package (see [`WASM_VENDOR.md`](./WASM_VENDOR.md)). Building our own byte-for-byte from pinned source is Phase 9 and is **0% done**.                                                                                                             |\n\n```\nv0 - prove the path\n  Phase 1  Scaffold \u0026 toolchain        ████████████████████  100%  done\n  Phase 2  WASM integration spike      ████████████████████  100%  done\n  Phase 3  Deno-FS VFS (file-backed)   ████████████████████  100%  done\nv1 - public launch\n  Phase 4  Crash-sim harness           ████████████████████  100%  done\n  Phase 5  Mode 1 - rollback locks     ████████████████████  100%  done\n  Phase 6  Mode 2 - WAL (exclusive)    ████████████████████  100%  done\n  Phase 7  Public API \u0026 bindings       ████████████████████  100%  done\n  Phase 8  Full test suite (L1–L5)     ████████████████████  100%  done  (L6 build byte-compare lands with Phase 9)\n  Phase 9  Reproducible build \u0026 CI     ░░░░░░░░░░░░░░░░░░░░    0%\n  Phase 10 JSR publish \u0026 docs          ░░░░░░░░░░░░░░░░░░░░    0%\nv2 - multi-process WAL                 ░░░░░░░░░░░░░░░░░░░░    0%  (gated on Deno core)\n```\n\n### Quickstart\n\nNot on JSR yet, so import from the local source (a `jsr:@scope/sqlite-deno` specifier will arrive\nwith Phase 10). The grant below is the entire permission footprint for durable writes — read **and**\nwrite on the directory holding the database, nothing else. (Why the directory and not just the file\nis the [durability caveat](#the-honest-durability-caveat-read-this).)\n\n```typescript\n// quickstart.ts\nimport { openDatabase } from \"./src/mod.ts\";\n\n// `using` disposes the database at scope end: open statements are finalized,\n// the file handle is closed. Default mode is rollback, durable-by-default.\nusing db = await openDatabase(\"./app.db\");\n\ndb.exec(`\n  CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);\n`);\n\nusing insert = db.prepare(\"INSERT INTO users (name) VALUES (?)\");\ninsert.run(\"Ada\");\ninsert.run(\"Grace\");\n\ninterface User {\n  id: number;\n  name: string;\n}\nusing byName = db.prepare\u003cUser\u003e(\"SELECT id, name FROM users WHERE name = ?\");\nconst ada = byName.get(\"Ada\"); // User | undefined — inferred, no cast\nconsole.log(ada); // { id: 1, name: \"Ada\" }\n\n// A transaction is a SAVEPOINT; a `using tx` that throws before commit rolls back.\nconst tx = db.transaction();\ndb.exec(\"UPDATE users SET name = 'Ada Lovelace' WHERE name = 'Ada'\");\ntx.commit();\n```\n\n```bash\n# durable writes need the directory, not just the file (see the durability caveat)\ndeno run --allow-read=. --allow-write=. quickstart.ts\n# { id: 1, name: \"Ada\" }\n```\n\nThat is the whole grant: no `--allow-ffi`, no `--allow-net`, no `--allow-env`.\n\n### How to read this repo right now\n\n```bash\ngit clone \u003cthis-repo\u003e \u0026\u0026 cd sqlite-deno\ndeno task check     # fmt, lint, type-check, and the full proof suite (224 tests)\n```\n\n- The public API lives in [`src/mod.ts`](./src/mod.ts) — `openDatabase` and the `Database` /\n  `Statement\u003cRow\u003e` / `Transaction` types, plus the typed `SqliteError` hierarchy.\n- The engine entry points live in [`src/vfs/`](./src/vfs/); `installDenoVfs` (in\n  [`src/vfs/deno.ts`](./src/vfs/deno.ts)) is what registers our VFS against the wasm. `openDatabase`\n  installs it for you.\n- The proofs are in [`test/`](./test/); the crash and concurrency harnesses under\n  [`test/harness/`](./test/harness/) are the most important code in the project, and they now drive\n  the **shipped `openDatabase` surface**, not the engine alone.\n\n---\n\n## What it is, and why it exists\n\nThere are several good SQLite options for Deno already, and each is a sensible choice for a\ndifferent problem. The combination none of them offers in one package is **permission-respecting,\nWAL-capable, and able to run everywhere Deno runs**, and that is the niche this project aims to\nfill. Each existing option makes a different, reasonable trade-off:\n\n- **FFI options** (`@db/sqlite`) are fast and full-featured. The trade-off is that they require\n  `--allow-ffi` (which widens Deno's permission model back toward the Node default), download a\n  prebuilt `.so` at first run, and cannot run on Deno Deploy or the edge, because there is no FFI\n  there.\n- **The existing WASM lineage** (`dyedgreen/deno-sqlite`) respects the permission model and has\n  served a lot of projects well. It predates SQLite's official wasm build and does not have WAL,\n  file locking, or shared memory.\n- **`node:sqlite`**, built into Deno, is a great fit if you want a batteries-included native engine\n  with the Node-shaped API. It is a native engine rather than a permission-model-first or edge\n  story.\n\nThe bet behind this project is a smaller, more specific one: Deno's permission model is most useful\nwhen infrastructure-grade packages can honor it without an escape hatch, and a SQLite that stays\ninside the permission model is a worthwhile thing to have for the cases where that matters. That is\nthe goal, to cover that specific gap well, not to replace anything that already works.\n\n**The permission model is the design constraint everything else serves.**\n\n---\n\n## The permission story\n\nThis is the part the project cares most about getting right. The wasm has **no ambient authority**.\nAll of SQLite's I/O flows back out through our VFS callbacks, and those reach the filesystem only\nthrough path-scoped `Deno.*Sync` calls. The module cannot open a file the host did not hand it. If\nthis package were supply-chain-compromised tomorrow, its blast radius would still be **exactly** the\npaths you granted, no FFI to abuse, no network to phone home, no ambient filesystem.\n\nWhat the package needs, and nothing more:\n\n```bash\n# read access to the directory holding the database - no --allow-ffi, no --allow-net, no --allow-env\ndeno run --allow-read=./data your_program.ts\n```\n\nThe grant is scoped to the database's **parent directory**, not the file alone, because the VFS\ncanonicalizes paths before touching them (the symlink guard below) and a crash-safe commit fsyncs\nthe directory — both of which read the directory path. A file-only grant still works for the\nplainest read path but **fails closed with a typed error** naming the grant it needs the moment it\nmust canonicalize or directory-fsync; it never silently downgrades and never widens what you\ngranted. The [durability caveat](#the-honest-durability-caveat-read-this) covers the write side.\n\n### Symlink escape: the guard, and its one residual\n\nDeno's permission check is **lexical** — it checks the path you pass, not the canonical target. So a\nsymlink that lives _inside_ your grant but points _outside_ it is followed by Deno (verified on Deno\n2.8.1), and naïvely that would let I/O land outside the granted prefix. The VFS closes this in\nuserland: before any filesystem op it canonicalizes the path with `Deno.realPathSync` (resolving\nsymlinked directory components, a symlinked final component, and the parent of a path being created)\nand re-checks the **canonical** target against Deno's own grant via `Deno.permissions.querySync` — a\nquery, never a request, so it can only ever refuse, never widen your grant. If the canonical target\nisn't granted, the open refuses with a typed `SqliteCantOpenError` and **zero files are created,\nread, deleted, or fsynced outside the grant**. This is the canonicalize-then-recheck Deno itself\nomits, applied uniformly to all four filesystem doors (open, delete, access, directory-sync).\n\n\u003e **The one residual (honest):** a TOCTOU window exists between canonicalizing the path and opening\n\u003e it. Exploiting it requires an attacker who already holds write access _into_ the granted directory\n\u003e — who can therefore already corrupt the database directly — and it cannot reach outside the grant\n\u003e in any way that in-grant write access can't already. Tracked as\n\u003e [SEC-002](https://github.com/ul0gic/sqlite-deno/issues/21) (Low); the complete fix is upstream\n\u003e Deno doing the canonicalize-before-check itself.\n\n### The honest durability caveat, read this\n\nThe headline \"`--allow-read=./app.db` is the entire grant\" is true for **reading**. It is **not**\nthe whole story for **durable writes**:\n\n- A crash-safe commit requires SQLite to `fsync` the **directory** that contains the database (so a\n  journal's deletion or a file's creation survives a power cut). Opening a directory handle to fsync\n  it is a _read of the directory path_, which a **file-only** grant does not cover.\n- So **durable** operation needs a read (and write) grant on the **parent directory**:\n\n  ```bash\n  # durable writes: grant the directory, not just the file\n  deno run --allow-read=./data --allow-write=./data your_program.ts\n  ```\n\n- Under a file-only grant the package still works and **fails closed**, it never widens your grant,\n  but the directory-fsync is denied, so the last-commit durability guarantee is unavailable. We\n  surface this as an error, never as a silent downgrade.\n\nThe engine behavior is fail-closed and never grant-widening. The full durability model — what each\ncommit mode guarantees, and the one knob that trades a sync for speed — is in\n[Durability](#durability) below.\n\n---\n\n## Architecture\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│  Your code  →  public API: openDatabase, Database, Statement │  ← built (Phase 7)\n├──────────────────────────────────────────────────────────────┤\n│  JS↔WASM glue (src/glue.ts): marshals values, owns memory    │\n├──────────────────────────────────────────────────────────────┤\n│  Official @sqlite.org/sqlite-wasm 3.53.0 (vendored, pinned)  │  ← the SQLite engine\n├──────────────────────────────────────────────────────────────┤\n│  Deno-FS VFS (pure TypeScript, src/vfs/*)                    │  ← honors the grant\n│     installed at runtime via sqlite3.vfs.installVfs          │\n│     I/O → Deno.openSync / readSync / writeSync / tryLockSync │\n└──────────────────────────────────────────────────────────────┘\n```\n\nThe key facts:\n\n- **No recompile.** We register a pure-JS VFS against the _prebuilt_ official wasm via `installVfs`\n  , the same mechanism SQLite's own browser OPFS VFS uses. We add **no C**.\n- **The VFS is simpler than the browser's.** OPFS is async but SQLite's VFS is synchronous, so the\n  browser VFS needs a `SharedArrayBuffer` + `Atomics.wait` async-proxy dance. **Deno's file API is\n  already synchronous**, so our VFS calls Deno I/O directly.\n- **Runs on Deno Deploy and the edge** by construction, one wasm, every target, no native binaries\n  to build, sign, and re-download.\n\n### VFS dispatch\n\n```mermaid\nflowchart TD\n  C[\"SQLite C engine (wasm)\"]\n  IO[\"src/vfs/io.ts\"]\n  NS[\"src/vfs/namespace.ts\"]\n  LK[\"src/vfs/lock.ts (Mode 1)\"]\n  DENO[\"Deno.*Sync\u003cbr/\u003e(read / write / sync / truncate)\"]\n  FLOCK[\"Deno FsFile.tryLockSync / unlockSync\u003cbr/\u003e(whole-file flock)\"]\n  FS[(\"granted paths only\")]\n\n  C --\u003e|xOpen xRead xWrite xSync xClose| IO\n  C --\u003e|xAccess xFullPathname xDelete| NS\n  C --\u003e|xLock xUnlock xCheckReservedLock| LK\n  IO --\u003e DENO\n  NS --\u003e DENO\n  LK --\u003e FLOCK\n  DENO --\u003e FS\n  FLOCK --\u003e FS\n\n  classDef engine fill:#a8b1ff,stroke:#111,color:#111,stroke-width:1px;\n  classDef vfs fill:#70ffaf,stroke:#111,color:#111,stroke-width:1px;\n  classDef host fill:#bff7d4,stroke:#111,color:#111,stroke-width:1px;\n  classDef disk fill:#ffd479,stroke:#111,color:#111,stroke-width:1px;\n\n  class C engine;\n  class IO,NS,LK vfs;\n  class DENO,FLOCK host;\n  class FS disk;\n```\n\nEvery VFS callback catches all errors and returns a SQLite result code; a throw across into C is a\nbug, never an error path. Colors only group the layers: blue is the SQLite engine, green is our\npure-TypeScript VFS, light green is the Deno host API it calls, and amber is the filesystem boundary\nthat stays inside the granted paths.\n\n### The lock ladder (Mode 1, as shipped)\n\nSQLite drives a five-state ladder. Native SQLite implements it with **byte-range** locks at three\nindependent offsets, which is what lets readers coexist. Deno exposes only **whole-file** `flock`,\nso v1 collapses the ladder to SQLite's own `unix-flock` protocol: take a whole-file `LOCK_EX` for\nthe first lock of _any_ level, hold it through the transaction, release it only at `UNLOCKED`. (Why\nwe did _not_ try to be cleverer is in\n[The story](#what-we-ran-into-and-decided-the-engineering-story).)\n\n```mermaid\nstateDiagram-v2\n  [*] --\u003e UNLOCKED\n  UNLOCKED --\u003e SHARED: xLock(SHARED) → tryLockSync(exclusive) takes whole-file LOCK_EX\n  SHARED --\u003e RESERVED: xLock(RESERVED) → no syscall (already hold LOCK_EX)\n  RESERVED --\u003e PENDING: xLock(PENDING) → no syscall\n  PENDING --\u003e EXCLUSIVE: xLock(EXCLUSIVE) → no syscall\n  EXCLUSIVE --\u003e SHARED: xUnlock(SHARED) → keep LOCK_EX (like unix-flock)\n  SHARED --\u003e UNLOCKED: xUnlock(NONE) → unlockSync()\n  note right of SHARED\n    OS lock held is only ever {none, exclusive} - never shared.\n    No upgrade path exists, so the flock-upgrade hazard cannot occur.\n    Consequence: multi-process SERIALIZED - one accessor at a time.\n  end note\n\n  classDef unlocked fill:#bff7d4,stroke:#111,color:#111,stroke-width:1px;\n  classDef shared fill:#70ffaf,stroke:#111,color:#111,stroke-width:1px;\n  classDef exclusive fill:#ffd479,stroke:#111,color:#111,stroke-width:1px;\n  class UNLOCKED unlocked\n  class SHARED shared\n  class RESERVED,PENDING,EXCLUSIVE exclusive\n```\n\n### WAL write / checkpoint (Mode 2, single-process exclusive)\n\n```mermaid\nsequenceDiagram\n  box rgb(168,177,255) app + SQLite engine\n    participant App\n    participant SQLite as SQLite (wasm)\n  end\n  box rgb(112,255,175) our VFS\n    participant VFS as Deno-FS VFS\n  end\n  box rgb(255,212,121) files (granted paths)\n    participant WAL as -wal file\n    participant DB as main db file\n  end\n\n  Note over SQLite: PRAGMA locking_mode=EXCLUSIVE *before* journal_mode=WAL\u003cbr/\u003e→ wal-index in heap, NO -shm, xShm* never called\n  App-\u003e\u003eSQLite: COMMIT\n  SQLite-\u003e\u003eVFS: append commit frame\n  VFS-\u003e\u003eWAL: writeSync + syncSync (the commit point lives in -wal)\n  Note over WAL,DB: commit is the synced commit frame - there is no journal-unlink point\n  App-\u003e\u003eSQLite: PRAGMA wal_checkpoint(TRUNCATE)\n  SQLite-\u003e\u003eVFS: read frames, write pages\n  VFS-\u003e\u003eDB: writeSync committed pages + syncSync\n  SQLite-\u003e\u003eVFS: truncate -wal\n  Note over SQLite: crash recovery rebuilds the heap wal-index by scanning -wal frame headers\n```\n\n---\n\n## What's tested, and how to verify it yourself\n\nThe correctness claims here are meant to be **executable rather than taken on faith**, please run\nthem and check. The whole suite runs from a clean checkout:\n\n```bash\ndeno task check        # fmt --check, lint, type-check, type-aware lint, full test suite - 224 tests\ndeno task test:soak    # Mode 1: N real OS processes, Jepsen bank, CPU-oversubscribed (env-gated)\ndeno task test:soak:wal # Mode 2: multi-seed WAL crash sweep (env-gated)\n```\n\nThe L3 crash, L4 concurrency, and WAL crash-sweep harnesses now drive the **shipped `openDatabase`\nsurface** — belt-and-suspenders: the engine-floor proofs are retained, and the same harnesses are\nre-pointed at the public `Database` / `Statement` / `Transaction` path so a regression in the\nbindings is caught by the same crash and concurrency invariants, all negative-control-gated.\n\nWhat the suite proves:\n\n- **Crash / power-loss recovery (Linux).** A deterministic fault-injection VFS records every write\n  and sync, then a power-loss model reconstructs the disk at each crash point (synced data exact;\n  unsynced data dropped, applied, or torn at sector granularity) and reopens through the _real_ VFS.\n  Invariants checked at every crash point: `integrity_check = ok`, committed transactions present,\n  uncommitted absent. A **negative control** (a lying no-op `xSync`) is proven to be _caught_, a\n  harness that stays green with durability disabled proves nothing. There is also a real\n  `SIGKILL`-mid-write subprocess test.\n- **Mode 1 concurrency.** N real OS subprocesses against one shared file, a Jepsen-style bank\n  workload (balance-sum conservation, monotonic commit counter, no torn reads, periodic\n  `integrity_check`), SIGKILL crash-recovery (exactly one process replays a hot journal), and a\n  negative control (no-op locks → corruption detected). Soaked at 36k serialized commits under CPU\n  oversubscription.\n- **Mode 2 WAL crash recovery.** Crash sweep over the `-wal` op stream: torn-tail recovery to a\n  consistent committed prefix, crash-during-checkpoint, salt-advance anti-stale-replay, recovery\n  from `{db, -wal}` with the `-shm` deleted (the heap wal-index is rebuilt from frame headers -\n  there is no `-shm`). Two negative controls caught.\n\nThese run on the **vendored** wasm, the **real** Deno-FS VFS, and the **shipped public API** — never\na stubbed SQLite.\n\n---\n\n## The honest capability envelope (the asterisks, each with the why)\n\nThese are the limitations, up front, each with the reason behind it. If any of these rule the\npackage out for your use case, that is genuinely useful to know before you invest time in it.\n\n### Mode 1, rollback journal, multi-process: **serialized**\n\nOne accessor at a time. **No concurrent readers.** A reader excludes other readers _and_ writers for\nas long as it holds the file.\n\n**Why:** Deno's userland exposes only whole-file `flock`, not byte-range `fcntl`. The obvious \"many\nreaders XOR one writer\" design (shared locks for readers, upgrade to exclusive for a writer) is\n**verified-unsafe** on whole-file `flock`: a Linux `flock` upgrade is non-atomic, a failed\n`LOCK_SH → LOCK_EX` upgrade _drops_ the shared lock while returning failure, and SQLite's\nchange-counter revalidation does not fire on the busy-retry path. That is a real (rare)\nsilent-stale-read / stale-commit corruption window. There is no event-loop-safe way to close it\nwithout byte-range locks. So v1 ships SQLite's own `unix-flock` protocol verbatim (`LOCK_EX` for\nevery level), which is **provably correct by construction**, at the cost of serialization. True\n\"many readers XOR one writer\" needs byte-range `fcntl` and is **v2**.\n\n\u003e **The multi-process Mode-1 contract:** a contending caller gets a `SqliteBusyError` (the lock is\n\u003e held). Pass a `busyTimeout` open option (milliseconds) to let SQLite block-and-retry the contended\n\u003e lock for you — it is applied before the open-time mode pragmas, so it covers `openDatabase` itself\n\u003e as well as later `db.transaction()` calls, and a multi-process caller no longer has to wrap the\n\u003e open in its own retry. All lock calls are non-blocking, so there is no OS deadlock. The default is\n\u003e `0` (immediate `SqliteBusyError`, fully backward-compatible); a non-zero timeout is not a\n\u003e guarantee — a sufficiently contended serialized workload can still exhaust it, so a caller-side\n\u003e retry loop on `SqliteBusyError` stays the ultimate backstop.\n\n### Mode 2, WAL: **single-process exclusive only**\n\nReal WAL, with the wal-index in heap. **No `-shm` file, no shared-memory methods.** One process owns\nthe file exclusively.\n\n**Why:** multi-process WAL needs a memory-mapped `-shm` wal-index _and_ byte-range `fcntl` for\ncross-process coordination, neither available from Deno userland today. Exclusive locking mode runs\nWAL with the index in heap, which needs only the whole-file exclusive lock Deno already has. This is\nexactly what the official `sqlite-wasm` does, and it covers the dominant Deno shape: one\nlong-running server process owning its database. Multi-process WAL is **v2**.\n\n\u003e Setting `journal_mode=WAL` without `locking_mode=EXCLUSIVE` first **fails closed**, SQLite returns\n\u003e `\"delete\"` (no WAL, no crash, no corruption) because the VFS has no shm.\n\n### Durability\n\n**Commit durability is separate from integrity.** Every mode and every durability level stays\n**corruption-free** across the modeled power loss — `PRAGMA integrity_check` is always `ok`. The\nonly thing that varies is whether the _latest committed_ transaction survives a power cut. That one\naxis is the `durability` option, and the two modes default it differently:\n\n| Mode (option)        | Default `durability` | What the default means                                                                                |\n| -------------------- | -------------------- | ----------------------------------------------------------------------------------------------------- |\n| `rollback` (default) | **`\"full\"`**         | Durable-by-default: the last committed txn survives modeled power loss. `synchronous=FULL`.           |\n| `wal`                | **`\"normal\"`**       | SQLite-recommended WAL default: consistency-safe, but the last commit(s) may roll back on power loss. |\n\n- **Rollback defaults `durability: \"full\"`** (`synchronous=FULL`) — durable-by-default, the last\n  committed transaction survives the modeled power loss. Pass `{ durability: \"normal\" }` for the\n  faster opt-in (one fewer journal sync per commit); it stays consistency-safe, but the latest\n  commit can be lost on a power cut. This was a real footgun the harness caught — see\n  [the engineering story below](#bug-004-re-pointing-the-harness-at-the-public-api-found-a-default-that-silently-dropped-the-last-commit).\n- **WAL defaults `durability: \"normal\"`** (`synchronous=NORMAL`), the SQLite-recommended WAL\n  default: consistency-safe, but a transaction that returned `COMMIT` can roll back after a power\n  cut (it survives an _application_ crash, just not a _power_ loss). This is documented SQLite\n  behavior, not a corruption bug — `integrity_check` stays `ok`. Pass\n  `{ mode: \"wal\", durability: \"full\" }` for power-loss durability in WAL.\n- **Directory-fsync durability is shipped and crash-proven on Linux for both modes** — rollback\n  _and_ WAL (it makes journal creation/deletion survive a power cut). It needs the\n  **parent-directory** grant, see [the permission caveat](#the-honest-durability-caveat-read-this)\n  above; under a file-only grant the package fails closed rather than silently dropping the sync.\n- **Platform support: Linux only, for the durability claim.** Linux directory-fsync durability is\n  crash-proven (rollback + WAL). **Windows** fsync semantics are **unverified** — there is no\n  Windows test rig yet, so do not rely on the durability guarantee there. **NFS and other networked\n  filesystems are explicitly unsupported**, same as native SQLite.\n- The crash proofs are model-bounded (a worst-legal-device power-loss model + Linux\n  `strace`-verified primitives), **not** real-hardware power-cut testing. A hardware rig is a later\n  release-hardening layer.\n\n---\n\n## What we ran into and decided (the engineering story)\n\nThe decisions below are the durable record of _why_ the thing is built the way it is, including the\ntimes the first attempt was wrong. The guiding rule: **no concurrency or durability mode ships until\nits harness proves it, for a database, \"mostly works\" is silent corruption waiting to happen.**\nTwice this discipline caught a corruption hole that would otherwise have shipped, and both are\nwritten up below, mistakes included.\n\n### WASM, not FFI, to keep the permission model intact\n\nFFI would be faster and simpler to build. The trade-off is that it requires `--allow-ffi` (which\nwidens Deno's permission model), downloads a native binary at first run, and does not run on Deno\nDeploy. This project takes the other side of that trade: WASM with a VFS, so the engine has **no\nambient authority** and the blast radius is exactly the granted paths. The cost is some write\nthroughput; the gain is the permission model, the edge, and a verifiable supply chain. That trade is\nlaid out plainly in the matrix below, including where it loses.\n\n### A pure-TypeScript VFS, no recompile, edge-compatible\n\nRather than fork an existing lineage or compile our own wasm, the project consumes the official\n`@sqlite.org/sqlite-wasm` and registers a VFS against it in pure TypeScript via `installVfs`. v1\ncarries **no C toolchain**, the package is auditable TypeScript over a pinned wasm blob. We did not\nfork `@db/sqlite` (its architecture is FFI-first, a different and valid design) nor the `dyedgreen`\nlineage (it predates the official wasm build that makes this VFS approach practical); building fresh\non the official wasm was the cleanest fit for the permission-first goal here.\n\n### The crash harness as a gate, built _before_ locking and WAL\n\nThe most load-bearing code in the project is a deterministic crash-simulation VFS, built _before_\nany locking or WAL so every later mode can be checked against it. A locking or WAL mode is exposed\nonly after its crash + concurrency harness is green, including a **negative control** that proves\nthe harness can actually fail (a harness that stays green with durability disabled would prove\nnothing). This is the one rule we hold firmly: a red harness means the mode stays\nsingle-process-only or unshipped, rather than \"ship it and watch.\"\n\n### BUG-001, the crash harness found _silent_ committed-data loss, and the first fix was wrong\n\nThe harness found that in DELETE journal mode a committed transaction could be **silently lost**\n(`integrity_check` still `ok`!) after a power cut: a \"zombie\" hot journal reappears and SQLite rolls\nthe committed pages back. The first diagnosed root cause, _\"Deno cannot fsync a directory\"_, was\n**wrong**. A 30-second `strace` probe showed `Deno.openSync(dir).syncSync()` _is_ a directory fsync\n(`openat` + `fsync`); the VFS simply wasn't issuing it. The real fixes: default to\n`journal_mode=PERSIST` (durable via file-content fsync, no directory round-trip) **and** issue the\ndirectory fsync in the VFS where SQLite expects it. Both are harness-proven on Linux. The lesson:\nverify the root cause against the artifact before designing around it.\n\n### BUG-004, re-pointing the harness at the public API found a default that silently dropped the last commit\n\nThe moment the L3 crash harness was re-pointed off the engine floor and onto the shipped\n`openDatabase` path, it caught another **silent** committed-data loss. The engine-floor harness had\nrun SQLite's own `synchronous=FULL` default and stayed green; the public API had shipped the\nrollback default at `synchronous=NORMAL`, and at `NORMAL` a torn next-transaction journal can be\nresurrected over a prior commit after a power cut — `integrity_check` still `ok`, the last commit\nsilently gone. The same workload at `FULL` survived with zero losses. The fix: default the rollback\nenvelope to `synchronous=FULL` (durable-by-default), with `{ durability: \"normal\" }` kept as an\nexplicit, documented opt-in. Like BUG-001, the harness earned its keep precisely _because_ a new\ncode path exercised a window the old one never did — the regression guard now sweeps both levels\nexplicitly.\n\n### The X-strict pivot, we _retreated_ from an unproven concurrent-reader design\n\nThe first Mode-1 draft was the clever \"many readers XOR one writer\" design. Verifying it against the\npinned SQLite source (`os_unix.c`, `pager.c`) and probing Linux `flock` revealed the\nnon-atomic-upgrade corruption hazard described above, and that the SQLite revalidation we were\ncounting on to save us **does not fire** on the relevant path. Rather than ship an unproven\nconcurrency win, we _retreated_ to the provably-safe serialized design (SQLite's own `unix-flock`\nprotocol) and deferred concurrent readers to v2. Shipping less, proven, beat shipping more,\nunproven.\n\n---\n\n## Honest comparison matrix\n\nA fair read of where this sits among good alternatives. It is slower on raw write throughput, and\ntrades that for the permission model and edge support. None of the alternatives are strawmen, each\nis a reasonable choice, and for many projects the right one.\n\n|                                 | **sqlite-deno** (this)                         | `@db/sqlite` (FFI)                 | `node:sqlite` (built-in)                        | `dyedgreen/deno-sqlite` (WASM)     | `@sqlite.org/sqlite-wasm` (official)  |\n| ------------------------------- | ---------------------------------------------- | ---------------------------------- | ----------------------------------------------- | ---------------------------------- | ------------------------------------- |\n| Engine                          | WASM (official)                                | native (FFI)                       | native (built into Deno)                        | WASM (own build)                   | WASM (official)                       |\n| Permission grant                | `--allow-read`/`-write` only                   | **needs `--allow-ffi`**            | engine is native; not the Deno permission story | `--allow-read`/`-write` only       | read of wasm only (browser-first)     |\n| Runtime download                | none (in-package)                              | **downloads a `.so` at first run** | none (in Deno)                                  | none                               | none                                  |\n| Runs on Deno Deploy / edge      | **yes**                                        | **no** (no FFI)                    | yes                                             | yes                                | designed for the browser, not Deno FS |\n| WAL                             | **yes** (single-process, v1)                   | yes (full)                         | yes (native)                                    | **no**                             | yes (browser/OPFS)                    |\n| Multi-process                   | yes, **serialized** (v1); full WAL in v2       | yes (full byte-range)              | yes (native)                                    | yes (rollback, readers-XOR-writer) | n/a (browser)                         |\n| Bulk-write throughput           | slower (WASM), **we lose here**                | **fastest**                        | fast (native)                                   | slower (WASM)                      | slower (WASM)                         |\n| Reproducible build / provenance | **planned** (Phase 9–10, OIDC), vendored today | binary from a release              | ships with Deno                                 | own build                          | npm-published                         |\n| Deno-filesystem VFS             | **yes** (pure TS)                              | n/a (native)                       | n/a (native)                                    | yes                                | no (OPFS-oriented)                    |\n| Public API                      | **yes** (`openDatabase`, typed `Statement`)    | yes                                | yes (`node:sqlite`)                             | yes                                | yes (browser `oo1`)                   |\n| Published / installable         | **not yet** (Phase 10 JSR; runs from a clone)  | yes (JSR)                          | yes (built into Deno)                           | yes (deno.land/x)                  | yes (npm)                             |\n\nTake-away: if you need maximum bulk-write speed and `--allow-ffi` is acceptable, `@db/sqlite` is an\nexcellent choice. If what you need is an embedded SQLite that **keeps Deno's permission model intact\nand runs on the edge**, that is the gap this project fills — the API is here today; the JSR release\nis Phase 10.\n\n---\n\n## Roadmap\n\n**v1 (public launch):**\n\n- **Phase 7, Public API — done.** `openDatabase`, `Database`, typed `Statement\u003cRow\u003e`, transactions,\n  `using`/`Symbol.dispose` lifetimes, Web-Streams result streaming. Mode selection is explicit and\n  constrained at open (illegal combos unrepresentable — `{ readonly: true, mode: \"wal\" }` is\n  rejected), so a caller cannot accidentally leave the tested engine envelope. No user-defined SQL\n  functions in v1 (that JS-callback-reentrancy surface is one place native engines have historically\n  hit use-after-free; it waits until the reentrancy model is tested). Two additive `OpenOptions`\n  shipped in the post-Phase-8 burndown: a `busyTimeout` (ms) so multi-process Mode-1 callers can let\n  SQLite block-and-retry instead of hand-rolling `SqliteBusyError` retry\n  ([ENH-005](https://github.com/ul0gic/sqlite-deno/issues/18)), and a `signal` (`AbortSignal`) to\n  cancel a slow cold-start open ([ENH-004](https://github.com/ul0gic/sqlite-deno/issues/17)).\n- **Phase 8, Full L1–L5 test suite — done.** Functional, permission, crash/durability, concurrency,\n  and a generative SQL fuzzer, all driving the shipped `openDatabase` surface (the L3/L4/WAL\n  harnesses are re-pointed at the public path). L6, the reproducible-build byte-compare, lands with\n  Phase 9 because it needs the build pipeline that doesn't exist yet; the borrowed-TCL/`dbsqlfuzz`\n  corpus was ruled out of scope, a native C shim would bypass the very wasm artifact under test.\n- **Phase 9, Reproducible byte-identical wasm build.** Today the wasm is the **vendored** official\n  artifact; compiling our own byte-for-byte from pinned SQLite source + pinned toolchain, with a\n  `verify-build.sh` a stranger can run, is **not done**.\n- **Phase 10, JSR publish** with OIDC provenance, immutable versions, API reference.\n\n**v2, multi-process WAL.** Gated on contributing byte-range `fcntl(F_OFD_SETLK)` locking to Deno\ncore (`ext/io/fs.rs`) and exposing mmap for a real `-shm`. With those, Mode 1 gets the faithful\nthree-byte-range ladder (true concurrent readers) and WAL goes multi-process. The hope is that this\nturns into a focused, useful contribution upstream to Deno itself, help on that front is very\nwelcome.\n\n---\n\n## Contributing\n\nContributions, issues, questions, and code review are all genuinely welcome, this is built in public\npartly so others can poke at it. Whether you want to fix a bug, add a proof to the harness, improve\nthe docs, or just ask how something works, please jump in. **See\n[CONTRIBUTING.md](./CONTRIBUTING.md) for a full guide** to getting oriented, running the suite, and\nwhat a good PR looks like.\n\nThe quick version, the gate every change keeps green:\n\n```bash\ndeno task check         # fmt --check, lint, type-check, type-aware lint, test\ndeno task test          # full suite\ndeno task test:soak     # Mode 1 multi-process soak (SQLITE_DENO_SOAK=1, CPU-oversubscribed)\ndeno task test:soak:wal # Mode 2 WAL crash-sweep soak\ndeno task bench         # hot-path measurement\n```\n\nTwo principles guide the project, and they exist to keep it trustworthy rather than to gatekeep:\n\n- **A database must not lose your data.** So no concurrency or durability mode is exposed until its\n  crash/concurrency harness is green, including a negative control that proves the harness can\n  actually catch corruption. Adding new proofs here is one of the most valuable things you can\n  contribute.\n- **The permission model stays intact.** No change may require a grant beyond\n  `--allow-read`/`--allow-write` on the target database, and no code path acquires a permission the\n  caller did not pass in. No FFI, no network, no ambient filesystem.\n\nThe toolchain is entirely Deno's built-ins (`deno check` / `lint` / `fmt` / `test` / `bench`) plus a\npinned, checksum-verified Biome for type-aware lint (dev/CI only, never shipped). The\n[roadmap](#roadmap) and the issue tracker are the best places to find where help is wanted.\n\n---\n\n## License\n\n[MIT](./LICENSE) © 2026 ul0gic\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ful0gic%2Fsqlite-deno","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ful0gic%2Fsqlite-deno","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ful0gic%2Fsqlite-deno/lists"}