For two decades, browser-based applications stored data in cookies, localStorage, or IndexedDB. These APIs served their purpose, but they all share a fundamental limitation: they are not relational databases. You cannot JOIN across object stores. You cannot run aggregate queries. You cannot enforce foreign key constraints. Every time a web developer needed something resembling a real query, they ended up writing custom JavaScript code to filter, sort, and combine data that a SQL query could express in a single line.

That changed when SQLite came to the browser via WebAssembly. Today, full-featured SQLite databases run entirely client-side, persisted to disk through the Origin Private File System, surviving page reloads and browser restarts. This is not a toy demo. Production applications are shipping with in-browser SQLite as their primary data layer, and the implications for how we build web apps are significant.

How SQLite Runs in the Browser

SQLite is written in C. The browser runs JavaScript and WebAssembly. The bridge between them is Emscripten, the C-to-Wasm compiler. The SQLite source code compiles to a .wasm module that exposes the full SQLite API — sqlite3_open, sqlite3_exec, sqlite3_prepare, and everything else — callable from JavaScript.

The official SQLite project now maintains its own Wasm build, called sqlite3.wasm (often referenced as the "official SQLite WASM/JS build" or "sqlite-wasm"). This is significant because it means the build is maintained by the same team that maintains SQLite itself, not a third-party wrapper. The Wasm module weighs about 900KB gzipped and includes the full SQLite feature set including FTS5 (full-text search), JSON functions, and window functions.

// Initialize sqlite3 in the browser
import sqlite3InitModule from '@aspect-build/aspect-sqlite3-wasm';

const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB('/mydb.sqlite3', 'ct');

// Create tables, insert data, run queries
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT, body TEXT, created_at TEXT)");
db.exec("INSERT INTO notes (title, body, created_at) VALUES ('First Note', 'Hello world', datetime('now'))");

const results = db.exec({
    sql: "SELECT * FROM notes WHERE title LIKE ?",
    bind: ['%First%'],
    returnValue: 'resultRows',
    rowMode: 'object'
});
console.log(results); // [{id: 1, title: 'First Note', ...}]

The Storage Problem: OPFS to the Rescue

Running SQLite queries in memory is straightforward. The hard part is persistence. SQLite needs byte-level random access to a file on disk — it reads and writes specific pages within the database file. Browser storage APIs were not designed for this access pattern:

The Origin Private File System (OPFS) solved this. OPFS is a browser API that provides a sandboxed filesystem per origin with synchronous read/write access from Web Workers. The critical feature is createSyncAccessHandle(), which returns a handle with read(), write(), truncate(), and flush() methods that behave like POSIX file operations. This is exactly what SQLite's VFS (Virtual File System) layer needs.

// Under the hood: SQLite's OPFS VFS
// The sqlite3.wasm build registers an OPFS-based VFS automatically
// when running in a Web Worker with OPFS access

// From a Worker, the flow is:
// 1. Get OPFS directory handle
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('mydb.sqlite3', { create: true });

// 2. Get synchronous access handle (Worker only)
const accessHandle = await fileHandle.createSyncAccessHandle();

// 3. SQLite reads/writes through this handle
// accessHandle.read(buffer, { at: offset })
// accessHandle.write(buffer, { at: offset })
// accessHandle.flush()

The synchronous access is essential. SQLite's transaction model assumes synchronous I/O — when a COMMIT returns, the data must be on disk. OPFS's synchronous access handles provide this guarantee, but only from within a Web Worker (the main thread gets only async access, which would break SQLite's assumptions).

Why Not Just Use IndexedDB?

IndexedDB works. Millions of web apps use it. But it has friction points that compound as application complexity grows:

None of these issues are bugs in IndexedDB. It was designed as a low-level storage primitive, not a relational database. The mismatch is between what developers need (structured queries over relational data) and what IndexedDB provides (object storage with indexes).

The Local-First Architecture

In-browser SQLite enables a pattern called "local-first" software. The idea is simple: the application's primary data store is on the user's device. The server is a synchronization layer, not the source of truth. When the device is offline, the app works normally. When it reconnects, changes sync bidirectionally.

This inverts the traditional web application architecture. Instead of the browser being a thin client that fetches everything from the server, the browser becomes the primary compute and storage layer. The server's role reduces to authentication, sync, and backup.

Several projects are building on this pattern:

Performance: How Fast Is It Really?

The obvious concern is performance. SQLite compiled to Wasm runs in an interpreted virtual machine inside a browser. How does it compare to native SQLite or to IndexedDB?

Benchmarks consistently show that SQLite-over-Wasm is slower than native SQLite by roughly 2-5x for CPU-bound operations (complex queries, large sorts). However, it is faster than IndexedDB for most real-world access patterns because SQLite can answer complex queries in a single call, while IndexedDB requires multiple round-trips through the browser's async event loop.

Concrete numbers from the official SQLite Wasm benchmarks:

These numbers are fast enough for any interactive application. The bottleneck in practice is not query execution but I/O to OPFS, and even that is measured in low milliseconds for typical operations.

Browser Support and Limitations

The key dependency is OPFS with synchronous access handles. As of early 2026, this is supported in:

Important limitations to be aware of:

When to Use It

In-browser SQLite is not the right choice for every web application. It adds complexity (Worker architecture, sync logic) and a Wasm payload to the initial load. But it is a strong fit for specific categories:

For simple key-value storage, localStorage or IndexedDB remain simpler choices. For applications that always require server-side data (social feeds, e-commerce catalogs), a traditional API-driven architecture makes more sense. SQLite in the browser fills the gap between these extremes — applications that need a real database but do not need (or want) a server for every query.

The Bigger Picture

SQLite in the browser is part of a broader shift. WebAssembly is bringing capabilities to the browser that were previously exclusive to native applications. SQLite is just the most visible example because databases are so fundamental to application development. But the same pattern — take a mature C/C++ library, compile it to Wasm, give it access to browser storage — applies to image processing libraries, compression algorithms, cryptographic tools, and more.

The web platform is no longer a thin rendering layer for server-generated content. It is a capable application runtime, and SQLite running at near-native speed with durable persistence is one of the clearest demonstrations of that evolution.