Library / SDK
DallasHoff/sqlocal avatar
DallasHoff/sqlocal

sqlocal: SQLite3 in the browser on the origin private file system

SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system.

747 stars38 forksTypeScriptMIT

At a glance

What is it?
SQLocal is a TypeScript library that runs the WebAssembly build of SQLite3 inside a web worker and persists the database file to the origin private file system, so a browser app gets a real SQL engine with durable local storage and no server. It suits local-first applications that want relational queries, typed ORMs such as Kysely and Drizzle, and live updates across tabs, but it requires cross-origin isolation headers on the page, which is a deployment constraint that rules out plain shared hosting.
Who is it for?
Adopt SQLocal if you are building a local-first browser application that needs SQL queries and durable storage without a backend, and you control the web server so you can set the isolation headers.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 41 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A real SQLite engine in a worker, not an IndexedDB shim

SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system. It wraps the WebAssembly build of SQLite3 and gives you a simple interface to interact with databases running on device.

The distinction that matters is that this is not a JavaScript reimplementation of SQL and not a query layer over IndexedDB. The actual SQLite3 engine, compiled to WebAssembly, does the work. That means the SQL dialect is SQLite's SQL dialect, the type system is SQLite's type system, and the behaviour of a query, including its edge cases around NULL and collation and integer overflow, is the behaviour you would get from the sqlite3 command line tool. For an application that already has SQL in it, that is the whole point.

The engine runs in a web worker so queries do not block the main thread. This is not a nice-to-have detail. A large aggregate over a few hundred thousand rows takes real time, and running it on the main thread would freeze the interface for the duration. Off the main thread, the page stays responsive and the worker can be reused across queries.

The storage layer is the origin private file system, described as optimized for fast file I/O. The database is a real file, named by you, living in the browser's private storage for the origin. That is what separates this from storing rows in IndexedDB or localStorage: you get a file, SQLite gets a file, and the durability and performance characteristics follow from the browser's file system implementation rather than from a key-value store.

The library is MIT licensed, published on npm as `sqlocal`, and the version in the package manifest is 0.18.0.

A tagged template for SQL and parameters in one string

The API is deliberately small. You create a client with a name for the SQLite file to save in the origin private file system, and the client hands you a `sql` tagged template function.

javascript
import { SQLocal } from 'sqlocal';

// Create a client with a name for the SQLite file to save in
// the origin private file system
const { sql } = new SQLocal('database.sqlite3');

// Use the "sql" tagged template to execute a SQL statement
// against the SQLite database
await sql`CREATE TABLE groceries (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)`;

Because it is a tagged template, parameters are inserted into the SQL string directly, and the library handles the binding. The documentation describes this as executing a parameterized statement just by inserting parameters in the SQL string:

javascript
const items = ['bread', 'milk', 'rice'];
for (let item of items) {
  await sql`INSERT INTO groceries (name) VALUES (${item})`;
}

That is the same ergonomics as a tagged template in any other library, and it means the value never becomes part of the SQL text, which is the property that keeps a loop of inserts safe when the data comes from a user.

The return value depends on the statement. SELECT queries and queries with the RETURNING clause return the matched records as an array of objects, so a select on the table created above gives you plain JavaScript objects:

javascript
const data = await sql`SELECT * FROM groceries`;
console.log(data);

which logs:

javascript
[
  { id: 1, name: 'bread' },
  { id: 2, name: 'milk' },
  { id: 3, name: 'rice' }
]

So the whole first-use path is four steps: create a client, run a CREATE TABLE, insert with parameters, select and get objects back.

Live updates across tabs through subscription

One feature distinguishes SQLocal from a plain SQLite wrapper, and it is the reason the library is described as local-first rather than merely client-side.

The feature list says you can subscribe to query results and receive changes, even across tabs. This is a change notification layer on top of the query results, so a component that has rendered a table can be told when the underlying data changed rather than polling.

The across-tabs part is the interesting part, and the documentation does not describe the mechanism. What it states is the behaviour: you subscribe to query results and receive changes, even across tabs. So a change written in one tab reaches subscribers in another tab of the same application without any server taking part.

Two consequences follow from that, and both are limits rather than features. A change made in one tab reaches the others without a server, which is what a local-first application wants, since the server is not involved in the data at all. And a change made in a completely different browser, on a different machine, is invisible, because there is no channel between them. So this is synchronisation across views of one device, not across devices. If your application needs the second, the data has to leave the browser.

The other line in the feature list is worth reading as a requirement rather than a feature: each user can have their own private database instance. Combined with the origin private file system, that means the database belongs to the origin and the browser profile, not to a server account, and it is not reachable by anything outside that origin's storage.

Kysely and Drizzle drivers on top of the same engine

SQLocal can be used as a driver for Kysely or for Drizzle ORM to make fully typed queries, and the package manifest confirms both are separate entry points rather than optional extras. The exports map lists `.`, `./drizzle`, `./kysely`, `./angular`, `./react`, `./vue` and `./vite`.

The Kysely integration is a dialect. You construct `SQLocalKysely` with a database name and pass the dialect to Kysely:

typescript
import { SQLocalKysely } from 'sqlocal/kysely';
import { Kysely, Generated } from 'kysely';

// Initialize SQLocalKysely and pass the dialect to Kysely
const { dialect } = new SQLocalKysely('database.sqlite3');
const db = new Kysely<DB>({ dialect });

The database type is declared as a TypeScript type, so the schema is the source of truth for the query builder:

typescript
type DB = {
  groceries: {
    id: Generated<number>;
    name: string;
  };
};

With that in place the query builder is fully typed, and a column that does not exist is a compile error rather than a runtime error. The Drizzle integration works the other way round, with a driver rather than a dialect, and the schema is declared with Drizzle's own table builders:

typescript
import { SQLocalDrizzle } from 'sqlocal/drizzle';
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { sqliteTable, int, text } from 'drizzle-orm/sqlite-core';

// Initialize SQLocalDrizzle and pass the driver to Drizzle
const { driver } = new SQLocalDrizzle('database.sqlite3');
const db = drizzle(driver);

The Drizzle driver is built on the `sqlite-proxy` entry point, which is Drizzle's way of routing queries through a driver you provide. The schema then uses `sqliteTable` with column definitions such as `int('id').primaryKey({ autoIncrement: true })` and `text('name').notNull()`, and the query builder produces the same kind of typed results.

The angular, react and vue entry points are for the hooks the feature list mentions: get set up quickly with hooks for UI frameworks. They exist so a component in any of those frameworks can subscribe to query results and re-render when they change.

Cross-origin isolation is the price of OPFS persistence

There is one deployment requirement that decides whether this library works at all in your application, and it is not optional.

To persist data to the origin private file system, the package relies on APIs that require cross-origin isolation, so the page you use this package on must be served with specific HTTP headers. Otherwise, the browser will block access to the origin private file system.

http
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

How this is configured depends on the web server or hosting service your application uses. The reason the browser requires it is that the origin private file system exposes capabilities that a cross-origin isolated context is allowed to have and a normal page is not, so the isolation headers are the browser's way of saying that only same-origin content is loaded into this page.

The practical consequence is that SQLocal is not deployable on any static host where you cannot set response headers, and that any third-party resource you load on the page, an image from a CDN, an analytics script, a font, has to be served with a permissive cross-origin resource policy, or the page stops being cross-origin isolated and the storage layer stops working. This is the single most common way to get a blank failure from this library, and the failure is not a clean error, it is the browser refusing the storage API.

The Vite plugin exists precisely to remove that friction in development. Vite needs some additional configuration to handle web worker files correctly, and the plugin handles that as well as enabling cross-origin isolation for the Vite development server by default. The documentation is emphatic about the gap: you also have to configure your production web server to use the same HTTP headers.

javascript
import { defineConfig } from 'vite';
import sqlocal from 'sqlocal/vite';

export default defineConfig({
  plugins: [sqlocal()],
});

Against Dexie and against a server-side SQLite over HTTP

The comparison worth making is with the two things people actually do today when they want SQL in a browser.

The first is a browser database library such as Dexie, which stores structured records in IndexedDB and gives you a query builder. Its API is shaped around IndexedDB object stores and a JavaScript query builder rather than a SQL engine, so the ergonomics for relational data, joins and aggregates are not the same as writing SQL against a real SQLite engine.

The second is a server-side SQLite exposed over HTTP. That gives you the same SQL dialect and the same mature engine, and the client is a thin RPC layer. The cost is that the data is not local, the app needs a backend, and the offline case is a special case you have to build. For a local-first application, that is a contradiction, since the whole point is that the data lives on the device.

SQLocal sits between the two. It gives you the real SQL engine and real files, locally, with no server. What you give up is the ease of deployment, because the isolation headers and the same-origin constraint make hosting arrangements part of the design.

The other thing to weigh is the SQLite WebAssembly build itself. The dependency is `@sqlite.org/sqlite-wasm` at about version 3.51.2, which means the library tracks the upstream SQLite version closely. That is a good thing for correctness, since a browser engine that lags the server engine by years would produce different behaviour for the same query, and it also means the WASM binary is downloaded by the user, which is a size cost that shows up in a first-load budget.

Editorial conclusion

Adopt SQLocal if you are building a local-first browser application that needs SQL queries and durable storage without a backend, and you control the web server so you can set the isolation headers. Do not adopt it if your page is served from static hosting you cannot configure, since without cross-origin isolation the browser blocks the origin private file system, and check first that the SQLite WebAssembly build and the browser you target both support the OPFS synchronous access handle path.

Frequently asked questions

What does SQLocal do?

It runs the WebAssembly build of SQLite3 in a web worker and persists the database file to the browser's origin private file system. You name a database, get a sql tagged template function, and run any query SQLite3 supports with results returned as arrays of objects.

How do I install SQLocal?

Install the package with `npm install sqlocal`, or `yarn add sqlocal`, or `pnpm install sqlocal`. The package is published on npm as sqlocal at version 0.18.0 and is MIT licensed.

Why does SQLocal need cross-origin isolation headers?

Because persisting to the origin private file system relies on APIs that require cross-origin isolation. The page must be served with `Cross-Origin-Embedder-Policy: require-corp` and `Cross-Origin-Opener-Policy: same-origin`, otherwise the browser blocks access to the origin private file system.

Can I use SQLocal with Kysely or Drizzle?

Yes. The package exports separate entry points, sqlocal/kysely and sqlocal/drizzle. The Kysely integration provides a dialect for `new Kysely<DB>({ dialect })`, and the Drizzle integration provides a driver built on Drizzle's sqlite-proxy for `drizzle(driver)`.

Does SQLocal sync data across browsers?

No. The feature list says you can subscribe to query results and receive changes, even across tabs, which covers views of the same origin in the same browser profile. There is no channel to another browser or another machine, since the data lives in the origin private file system.

Is SQLocal still being maintained?

The repository is not archived and the last push was on 2026-09-12. The listed releases are v0.18.0 from 2026-04-25, v0.17.0 from 2026-02-02 and v0.16.0 from 2025-11-09, and the project homepage is sqlocal.dev.

Official sources

  1. DallasHoff/sqlocal on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dallashoff-sqlocal.svg)](https://hysenlabs.com/projects/dallashoff-sqlocal)

Was this useful?