Living Document Notice
Published 2026-09-12. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.

SQLite in the Browser via OPFS

SQLite in the Browser via OPFS: Stark monochrome P4 paper white vector CRT macro showing concentric disk track sectors and radial block storage read-write vectors

Summary

IndexedDB has historically served as the primary client persistence layer in browser environments. However, its asynchronous key-value design imposes high transaction overhead when executing complex graph traversals or relational queries across personal knowledge vaults. Compiling SQLite to WebAssembly provides relational query capabilities, but persisting database blocks through IndexedDB virtual filesystem adapters degrades write throughput significantly.

The Origin Private File System (OPFS) introduces synchronous, raw byte-level disk access inside Web Workers via FileSystemSyncAccessHandle. By pairing compiled SQLite WebAssembly builds with the OPFS VFS backend, browser applications achieve near-native database execution directly on user storage.

Synchronous I/O in Dedicated Web Workers

The browser security model restricts synchronous disk operations on the main execution thread to avoid blocking layout recalculations and user input events. To circumvent this constraint, SQLite and its OPFS filesystem driver run inside a dedicated background Web Worker.

+-------------------------------------------------------------+
|                      Main UI Thread                         |
|          (React / Svelte / Web Components Dispatch)         |
+-------------------------------------------------------------+
                               |
                               | postMessage({ sql, params })
                               v
+-------------------------------------------------------------+
|               Dedicated SQLite Web Worker                   |
|        +-------------------------------------------+        |
|        |         SQLite3 C Engine (WASM)           |        |
|        +-------------------------------------------+        |
|                              |                              |
|                              | Synchronous POSIX calls      |
|                              v                              |
|        +-------------------------------------------+        |
|        |             OPFS VFS Driver               |        |
|        +-------------------------------------------+        |
+-------------------------------------------------------------+
                               |
                               | FileSystemSyncAccessHandle
                               v
+-------------------------------------------------------------+
|             Origin Private File System (Disk)               |
|            [vault.sqlite3]  [vault.sqlite3-wal]             |
+-------------------------------------------------------------+

Inside the worker thread, FileSystemSyncAccessHandle.read() and write() execute synchronously without event loop scheduling delays, providing the continuous byte-stream access SQLite requires for B-tree updates.

WAL Mechanics and Multi-Tab Locking Constraints

SQLite achieves high concurrent read performance through its Write-Ahead Log (WAL) mode. In native operating system environments, WAL mode relies on shared memory (shm) primitives and POSIX file locks (fcntl) across separate processes. Browsers provide no cross-tab shared memory primitives for OPFS access handles.

When a browser tab opens a FileSystemSyncAccessHandle on a file in OPFS, the browser engine acquires an exclusive lock on that underlying storage handle. A second browser tab attempting to obtain an access handle on the same database file encounters an immediate NoModificationAllowedError.

Execution LayerNative SQLite EnvironmentBrowser OPFS WebAssembly
File Access ModelPOSIX file descriptors (open, read, write)FileSystemSyncAccessHandle in Web Worker
Locking PrimitiveOS-level byte-range locks (fcntl, flock)Exclusive access lock per file handle
Shared MemoryShared memory files (mmap, .shm)No cross-tab shared memory available
Multi-Tab AccessConcurrent processes supported via WALRequires explicit leader broker architecture
Write LatencyDirect kernel page cache (~0.05 ms)Synchronous worker disk dispatch (~0.25 ms)

To support multiple open browser tabs across a single knowledge vault, FreeNext deploys a Dedicated-to-Shared Worker bridge. A single SharedWorker acts as the database lock broker, routing SQL commands from multiple tab clients through the single dedicated worker that holds the OPFS handle.

Dedicated Worker Implementation Pattern

The code snippet below illustrates how FreeNext initializes SQLite within an OPFS-backed Web Worker context:

// sqlite-worker.ts: Initializing OPFS VFS with SQLite WASM
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
 
interface QueryMessage {
  id: string;
  sql: string;
  bindParams?: unknown[];
}
 
let dbInstance: any = null;
 
async function bootstrapDatabase() {
  const sqlite3 = await sqlite3InitModule({
    print: console.log,
    printErr: console.error,
  });
 
  if ('opfs' in sqlite3) {
    // Instantiate persistent database inside Origin Private File System
    dbInstance = new sqlite3.oo1.OpfsDb('/vault-metadata.sqlite3');
    
    // Configure PRAGMA parameters for local flash memory
    dbInstance.exec('PRAGMA journal_mode = WAL;');
    dbInstance.exec('PRAGMA synchronous = NORMAL;');
    dbInstance.exec('PRAGMA cache_size = -8000;'); // 8 MB page cache
    
    dbInstance.exec(`
      CREATE TABLE IF NOT EXISTS notes (
        id TEXT PRIMARY KEY,
        path TEXT NOT NULL UNIQUE,
        title TEXT NOT NULL,
        mtime INTEGER NOT NULL,
        word_count INTEGER NOT NULL,
        content_hash TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_notes_path ON notes(path);
    `);
  } else {
    throw new Error('OPFS VFS is unavailable in this browser environment');
  }
}
 
self.onmessage = async (event: MessageEvent<QueryMessage>) => {
  if (!dbInstance) {
    await bootstrapDatabase();
  }
 
  const { id, sql, bindParams } = event.data;
  try {
    const results: Record<string, unknown>[] = [];
    dbInstance.exec({
      sql,
      bind: bindParams ?? [],
      rowMode: 'object',
      callback: (row: Record<string, unknown>) => {
        results.push(row);
      },
    });
 
    self.postMessage({ id, success: true, rows: results });
  } catch (error: any) {
    self.postMessage({ id, success: false, error: error.message });
  }
};

This worker-confined architecture ensures query processing remains decoupled from DOM painting routines while retaining full transactional durability across application sessions.


  • Directus Target: freenext
  • Garden Source Reference: NXT-1001 - The Application Is Just a Lens, BSN-1004 - Browser Storage Substrates, DAT-1003 - Client-Side Extraction Engines, MOC - Data Liberation Workbenches, MOC - The Plain-Text Longevity Standard, MOC - Local-First Systems and Synchronization, MOC - Bosun PKM Tools