OPFS & WASM-SQLite
The browser's native file-system access API — and how it makes a real SQLite database possible entirely client-side.
Overview
The OPFSOrigin Private File System — a browser API giving Web Workers synchronous, high-performance file access on disk.Learn more (Origin Private File System) is a browser API that gives JavaScript — specifically, synchronous access from inside a Web Worker — direct, high-performance access to a private, sandboxed filesystem. It's the piece of infrastructure that makes it practical to run WASM-SQLiteA full SQLite database engine compiled to WebAssembly and run entirely inside the browser.Learn more, a full copy of SQLite compiled to WebAssembly, entirely inside the browser with real file-backed persistence, instead of a database that lives only in memory.
Why It Exists
IndexedDB is genuinely useful, but its API is awkward for anything that expects a real file interface, and it isn't fast enough for the kind of heavy, random-access reads and writes a database engine performs constantly. OPFS exists to give web apps an actual file system primitive — synchronous read/write handles, fast enough that porting an existing native database engine to the browser stops being a novelty and starts being genuinely usable in production.
Real World Example
This is the exact technology behind the SQL Playground built earlier in this journey. That playground keeps its database in memory for simplicity — reload the page, and it's gone. Point that same WASM-SQLite engine at OPFS instead, and the database file persists to disk inside the browser across reloads and even browser restarts — which is how real local-first apps (offline note-taking tools, browser-based dev utilities) ship a full relational database with zero backend server at all.
How the Pieces Fit Together
OPFS — a Private Filesystem per Origin
Every origin gets its own sandboxed directory tree that JavaScript can read and write like real files — but it's invisible through the OS's normal file explorer, and exists purely for that origin's own use.
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('app.db', { create: true });Synchronous Access Requires a Web Worker
The fast, synchronous read/write handle OPFS offers is only available inside a dedicated Web Worker, never the main thread — a deliberate restriction so that disk I/O can never block page rendering.
// inside a Worker
const accessHandle = await fileHandle.createSyncAccessHandle();
accessHandle.write(data, { at: 0 });WASM-SQLite — the Real Engine, Compiled to Run in the Browser
SQLite's actual C source code, compiled to WebAssembly — the same battle-tested engine used in countless native mobile and desktop apps, running with near-native speed inside a browser tab.
Persisting to OPFS Instead of Memory
A WASM-SQLite build can be configured to use OPFS as its storage backend (its 'VFS', or virtual file system), so every write actually lands on disk through the mechanism above — the difference between a database that survives a page reload and one that doesn't.
Diagram
The main thread never touches the disk directly — the worker does
Main thread
renders the page, posts messages to the worker
Web Worker
runs WASM-SQLite
OPFS sync access handle
fast, synchronous file I/O
Data persists on disk
survives reloads and browser restarts
Syntax
// Inside a dedicated Web Worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("app.db", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
accessHandle.write(new TextEncoder().encode("hello"), { at: 0 });
accessHandle.flush();
accessHandle.close();
// Requesting durability so the browser is less likely to evict this origin's storage
const granted = await navigator.storage.persist();Common Mistakes
Calling createSyncAccessHandle() from the main thread
Why: It's only available inside a dedicated Web Worker by specification — calling it on the main thread throws, since synchronous file I/O there would risk freezing the page.
Fix: Move all direct OPFS file access into a Web Worker, and communicate with it from the main thread via postMessage.
Assuming OPFS files are visible to the user or synced anywhere
Why: OPFS storage is origin-private and local to that one browser profile — it's invisible in the regular file system, and nothing about it is automatically backed up or synced across devices.
Fix: Don't rely on OPFS for anything the user should be able to see or export without building an explicit export feature yourself.
Treating OPFS storage as guaranteed to persist forever
Why: Like IndexedDB, browsers can evict an origin's storage under disk pressure unless that origin has been granted persistent storage — clearing browser data wipes it entirely either way.
Fix: Call `navigator.storage.persist()` for apps that genuinely need durability, and always have a real recovery or sync path for data that matters.
Interview Questions
What is OPFS, in plain terms?
A browser API that gives a website its own private, sandboxed filesystem — invisible to the user through the normal OS file explorer, and used purely for that origin's own storage needs.
Why does OPFS's fast, synchronous file access only work inside a Web Worker?
Synchronous disk I/O can take a noticeable amount of time, and running it on the main thread would block page rendering and user interaction. Restricting synchronous access handles to Web Workers keeps that I/O off the thread responsible for the page's UI.
What would it take to make a WASM-SQLite-backed web app genuinely durable, and what are the honest limits of that?
You'd back the SQLite instance with OPFS for real file persistence, request persistent storage via `navigator.storage.persist()` to reduce the chance of eviction, and run the database inside a Web Worker for performance. The honest limit is that none of this is a substitute for a real backup: a wiped browser profile, a user switching devices, or a rare-but-real storage eviction still means total data loss unless the app also syncs that data somewhere else, like a server.
Production Best Practices
Do
✓Do all direct OPFS file access inside a dedicated Web Worker.
✓Request persistent storage for apps where losing local data would be a real problem.
✓Build an explicit export/sync path for any data the user should be able to recover elsewhere.
Don't
✗Don't call createSyncAccessHandle() from the main thread — it's worker-only.
✗Don't treat OPFS as visible, synced, or backed-up storage — it's none of those by default.
✗Don't assume persistent storage guarantees are permanent — a cleared browser profile still wipes it.
Comparison
| IndexedDB | OPFS + WASM-SQLite | |
|---|---|---|
| Access pattern | Key/value + indexes | Full relational SQL |
| Query capability | Object store queries, cursors | Real SQL — joins, aggregates, subqueries |
| Runs on main thread? | Yes | No — requires a Web Worker for sync access |
| Best for | Simple structured storage | Apps that need a genuine embedded database |