I turned off my Wi-Fi, refreshed the page, and my data was still there. By running SQLite inside the browser (via WASM), we can build apps with 0ms latency. Here is the code to prove it.
Last week, I wrote about "The Architecture Shift to Local-First", and the response was overwhelming. The debate in the comments made one thing clear: Developers are tired of network latency.
Theory is great, but implementation proves the concept.
Today, I am sharing "Part 1" of my build log. I built a simple Snippet Manager ("Dev-Brain Dump"), but under the hood, it uses a radical architecture: It runs a full SQL database inside the browser tab.
The Goal: 0ms Latency
I set three strict constraints for this Proof of Concept (PoC):
- No API calls for saving data.
- Persistence even if I close the browser.
- Offline-ready (Must work with Wi-Fi turned off).
The Stack
I chose a stack optimized for speed and stability:
- Frontend: React + Vite
- Database: wa-sqlite (A high-performance WebAssembly port of SQLite)
-
Persistence: IndexedDB (via
IDBBatchAtomicVFS) - Styling: Tailwind CSS
Why wa-sqlite?
Because it supports the Origin Private File System (OPFS) and IndexedDB persistence out of the box. We aren't just keeping data in memory, we are writing binary SQL files to the user's disk.
The Architecture
Traditional web apps treat the browser as a "dumb view" that fetches data from a smart server.
In this Local-First approach, we flip the script:
- The "Smart Database" lives inside the Client.
- The Server (eventually) just becomes a backup/sync target.
Implementation
Setting up SQLite in the browser used to require complex Webpack configurations. With Vite, it is surprisingly clean.
1. The Database Layer (db.js)
Here is how we initialize the database. Note the use of IDBBatchAtomicVFS—this is the component that ensures our writes to IndexedDB are atomic and safe from corruption.
// A simplified view of my db.js
import { SQLite4WASM } from "wa-sqlite";
import { IDBBatchAtomicVFS } from "wa-sqlite/src/examples/IDBBatchAtomicVFS.js";
export async function initDB() {
// 1. Load the WASM Module
const sqlite3 = await SQLite4WASM();
// 2. Set up the File System for persistence
const vfs = new IDBBatchAtomicVFS("my-local-db");
await vfs.ready;
sqlite3.register_vfs(vfs);
// 3. Open the DB
const db = await sqlite3.open_v2(
"my-db",
sqlite3.SQLITE_OPEN_READWRITE | sqlite3.SQLITE_OPEN_CREATE,
vfs.name
);
// 4. Create Table
await sqlite3.exec(db, `
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY,
content TEXT
)
`);
return { db, sqlite3 };
}
2. Security Matters (Even Locally)
Just because the DB is local doesn't mean we ignore security. I used Prepared Statements to handle inserts. This prevents SQL injection if the data is ever synced to a backend later.
export async function addSnippet(text) {
const { db, sqlite3 } = await initDB();
// Always prepare your statements!
const sql = "INSERT INTO snippets (content, created_at) VALUES (?, ?)";
const prepared = await sqlite3.prepare_v2(db, sql);
// Bind data safely
sqlite3.bind_text(prepared, 1, text);
sqlite3.bind_text(prepared, 2, new Date().toISOString());
await sqlite3.step(prepared);
sqlite3.finalize(prepared);
}
The Result
The most satisfying moment was turning off my Wi-Fi and hitting "Save".
The UI looks simple, but that ID 1 comes from a local SQL engine, not a fetch request.
I reloaded the page. The data was there instantly. No loading spinner. No fetch() waterfall in the Network tab. Just instant data access.
Proof of life: The SQLite binary file stored safely in IndexedDB.
Next Steps
Right now, this data lives and dies on my specific device. If I open this on my phone, it is empty.
Part 2 will tackle the hardest problem: Sync.
I plan to explore how to get this local data out of the browser and onto a server without breaking that "Local-First" feeling.
Have you tried running WASM databases in production yet? Let me know in the comments.
Follow me to catch Part 3 where we tackle the sync engine.
Top comments (2)
Really appreciate this post — and this is so needed 🙏🏿. Latency is one of those pain points that quietly accumulates, and I am tired of juggling latency issues and compensating with UX patches and sprinkles of loaders all over the UI. Once API calls get involved, even a few extra milliseconds add friction. Browser-side SQLite is a big win — this feels like a great direction!
"Sprinkles of loaders" is painfully accurate. We spend so much engineering effort masking latency with skeletons and spinners, instead of just eliminating the network dependency for user interactions. Glad to hear the approach resonates with you.