Every time I build an internal tool that needs local storage, the default suggestion is IndexedDB. And every time, I end up reaching for SQLite compiled to WASM instead.
Here's why.
The IndexedDB Problem
IndexedDB is fine for key-value storage. But the moment you need relational queries, it falls apart:
- No SQL. You write verbose cursor-based code for what should be a
SELECT ... WHERE ... JOIN. - Schema migrations are painful. You manage version numbers and write imperative upgrade logic.
- No easy export. Want to give the user their data as a file? Good luck serializing IndexedDB.
- Performance degrades. Large datasets with complex queries slow down noticeably.
For a simple cache or offline queue, IndexedDB is fine. For an ops tool where users query, filter, and export data? It's the wrong abstraction.
SQLite via WASM
sql.js compiles SQLite to WebAssembly. You get a real relational database in the browser:
SELECT c.name, COUNT(j.id) as job_count, SUM(j.total) as revenue
FROM clients c
JOIN jobs j ON j.client_id = c.id
WHERE j.status = 'completed'
GROUP BY c.id
ORDER BY revenue DESC
Try writing that with IndexedDB cursors.
The Tradeoffs
Bundle size: ~1MB for the WASM binary. For a public-facing marketing site, that's too much. For an internal tool used daily, it's nothing.
Persistence: SQLite runs in memory by default. You need to serialize the database to localStorage, IndexedDB (ironic), or the Origin Private File System (OPFS) for persistence. OPFS is the best option on modern browsers.
No concurrent writes: One tab at a time can write. For single-user internal tools, this is fine. For multi-tab scenarios, you need a locking strategy.
When to Use It
Use SQLite in the browser when:
- Your app is data-heavy (hundreds or thousands of records)
- Users need to filter, search, and aggregate
- You want single-file export/backup
- The app is an internal tool, not a public-facing website
Stick with IndexedDB when:
- You're caching API responses
- You need a simple offline queue
- Bundle size is critical
- You don't need relational queries
Export Is the Killer Feature
The single best thing about SQLite in the browser: the user can download their entire database as one file.
No API calls. No serialization logic. Just db.export() and hand them a .db file they can open with any SQLite tool.
For ops tools where data ownership matters, this is a huge trust signal. The user knows they're not locked in.
Building a data-heavy internal tool? Let's talk about the right architecture.