
Security News
Socket Releases Free Certified Patches for Critical vm2 Sandbox Escape
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.
@firtoz/drizzle-sqlite-wasm
Advanced tools
SQLite WASM in a worker, Drizzle ORM, TanStack DB collections — reactive, non-blocking browser databases with migrations and React hooks when you need them.
⚠️ Early WIP Notice: This package is in very early development and is not production-ready. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See CONTRIBUTING.md for details.
npm install @firtoz/drizzle-sqlite-wasm @firtoz/drizzle-utils drizzle-orm @tanstack/db
// schema.ts
import { syncableTable } from "@firtoz/drizzle-utils";
import { text, integer } from "drizzle-orm/sqlite-core";
export const todoTable = syncableTable("todos", {
title: text("title").notNull(),
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
description: text("description"),
});
export const schema = { todoTable };
# Generate Drizzle migrations
drizzle-kit generate
// App.tsx
import { DrizzleSqliteProvider } from "@firtoz/drizzle-sqlite-wasm";
import SqliteWorker from "@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker?worker";
import * as schema from "./schema";
import migrations from "./migrations";
function App() {
return (
<DrizzleSqliteProvider
worker={SqliteWorker}
dbName="my-app"
schema={schema}
migrations={migrations}
loadingFallback={<div>Loading database…</div>}
>
<TodoApp />
</DrizzleSqliteProvider>
);
}
dbName or workerOpenOptions change: the provider’s children and collections are for the new file (previous in-provider UI state from the old file does not carry over). State above the provider (layout, URL-driven shell, etc.) is unchanged—handle that in your app if it should follow the new database.loadingFallback: The worker and migrations run asynchronously. Until the session is ready, the provider renders loadingFallback (not the main children). Put routes that use useDrizzleSqlite / useSqliteCollection inside the provider, but they only mount in the “ready” phase—use this slot for “Loading…”, skeletons, or a minimal shell. Keep app chrome (nav, layout) above the gated region if you want it to stay stable.errorFallback (or a default error area with data-testid="sqlite-db-error" for tests).// useDrizzleSqliteDb without the provider: gate on sessionStatus before using drizzle / createCollection
import { useDrizzleSqliteDb } from "@firtoz/drizzle-sqlite-wasm";
// …
const { drizzle, readyPromise, sessionStatus } = useDrizzleSqliteDb(...);
if (sessionStatus !== "ready") {
return <p>Loading…</p>;
}
// TodoList.tsx
import { useDrizzleSqlite, useSqliteCollection } from "@firtoz/drizzle-sqlite-wasm";
import { todoTable } from "./schema";
function TodoList() {
const { drizzle } = useDrizzleSqlite();
// Option 1: Use Drizzle ORM directly
const loadTodos = async () => {
const todos = await drizzle.select().from(todoTable).all();
return todos;
};
// Option 2: Use TanStack DB collection
const collection = useSqliteCollection("todos");
const [todos] = collection.useStore();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Vite has built-in support for Web Workers with the ?worker suffix:
import SqliteWorker from "@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker?worker";
const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
To enable OPFS (Origin Private File System) persistence with SQLite WASM, you need to configure Vite properly. Add the following to your vite.config.ts:
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
// Required: Set COOP/COEP headers for SharedArrayBuffer support
{
name: "configure-response-headers",
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
},
},
// Required: Fix for sqlite-wasm OPFS proxy worker module format
{
name: "sqlite-wasm-opfs-fix",
enforce: "pre",
transform(code, id) {
// Transform worker creation calls to use module type
if (
id.includes("@sqlite.org/sqlite-wasm") &&
code.includes("new Worker")
) {
// This fixes the "Unexpected token 'export'" error in OPFS proxy worker
let transformed = code;
const workerRegex =
/new\s+Worker\s*\(\s*(new\s+URL\s*\([^)]+,[^)]+\))\s*\)/g;
transformed = transformed.replace(
workerRegex,
"new Worker($1, { type: 'module' })",
);
if (transformed !== code) {
return {
code: transformed,
map: null,
};
}
}
},
},
],
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
optimizeDeps: {
exclude: ["@sqlite.org/sqlite-wasm"],
},
worker: {
format: "es",
},
});
Why is this needed?
SharedArrayBuffer support, which OPFS needs for synchronous file operations{ type: 'module' }, causing syntax errors when the OPFS proxy worker (which uses ES module syntax) is loadedFor a complete example, see tests/test-playground-collections/vite.config.ts in the repository.
Use new URL() with import.meta.url:
const SqliteWorker = class extends Worker {
constructor() {
super(
new URL("@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker", import.meta.url),
{ type: "module" }
);
}
};
const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
Similar to Webpack:
const SqliteWorker = class extends Worker {
constructor() {
super(
new URL("@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker", import.meta.url)
);
}
};
const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
The primary feature of this package: Create reactive, type-safe collections backed by SQLite WASM.
Create TanStack DB collections backed by SQLite:
import { createCollection } from "@tanstack/db";
import type { DrizzleSqliteTableCollection } from "@firtoz/drizzle-utils";
import { sqliteCollectionOptions } from "@firtoz/drizzle-sqlite-wasm";
import * as schema from "./schema";
const collection = createCollection(
sqliteCollectionOptions({
drizzle,
tableName: "todos",
readyPromise,
syncMode: "on-demand", // or "realtime"
})
);
// CRUD operations
await collection.insert({ title: "Buy milk", completed: false });
await collection.update(todoId, { completed: true });
await collection.delete(todoId); // Soft delete (sets deletedAt)
// Query with filters
const completed = await collection.find({
where: { completed: true },
orderBy: { createdAt: "desc" },
});
type TodosCollection = DrizzleSqliteTableCollection<typeof schema.todoTable>;
// Subscribe to changes
collection.subscribe((todos) => {
console.log("Todos updated:", todos);
});
Use DrizzleSqliteTableCollection<TTable> from @firtoz/drizzle-utils when you want a reusable collection type alias (shared with @firtoz/drizzle-durable-sqlite).
Config:
drizzle: DrizzleDB - Drizzle instancetableName: string - Table namereadyPromise: Promise<void> - Database ready promisesyncMode?: "on-demand" | "realtime" - Sync modeDrizzleSqliteProviderContext provider for SQLite WASM:
<DrizzleSqliteProvider
worker={SqliteWorker} // Worker constructor
dbName="my-app" // Database name
schema={schema} // Drizzle schema
migrations={migrations} // Migration config
>
{children}
</DrizzleSqliteProvider>
Props:
worker: new () => Worker - Worker constructor (bundler-specific)dbName: string - Name of the SQLite databaseschema: TSchema - Drizzle schema objectmigrations: DurableSqliteMigrationConfig - Migration configurationworkerOpenOptions?: SqliteWasmWorkerOpenOptions - Optional PRAGMA synchronous / journal_mode on first DB open (see hook section below)useDrizzleSqliteDb(worker, dbName, schema, migrations, debug?, interceptor?, workerOpenOptions?)Hook to create a Drizzle instance with Web Worker:
Optional workerOpenOptions sets SQLite pragmas when the worker first opens that dbName (same global worker + same dbName ⇒ options from the first open win until the worker is reset):
import type { SqliteWasmWorkerOpenOptions } from "@firtoz/drizzle-sqlite-wasm";
const open: SqliteWasmWorkerOpenOptions = {
synchronous: "NORMAL", // default worker behavior if omitted: "FULL"
journalMode: "WAL", // default if omitted: "WAL"
};
useDrizzleSqliteDb(SqliteWorker, "my-app", schema, migrations, undefined, undefined, open);
DrizzleSqliteProvider accepts the same shape as workerOpenOptions.
function MyComponent() {
const { drizzle, readyPromise } = useDrizzleSqliteDb(
SqliteWorker,
"my-app",
schema,
migrations
);
useEffect(() => {
readyPromise.then(() => {
console.log("Database ready!");
});
}, [readyPromise]);
// Use drizzle...
}
Returns:
drizzle: DrizzleDB - Drizzle ORM instancereadyPromise: Promise<void> - Resolves when database is readyuseDrizzleSqlite()Access the Drizzle context:
function MyComponent() {
const { drizzle, getCollection } = useDrizzleSqlite();
// Use drizzle or get collections...
}
Returns:
drizzle: DrizzleDB - Drizzle ORM instancegetCollection: (tableName) => Collection - Get TanStack DB collectionuseSqliteCollection(tableName)Hook for a specific collection with automatic ref counting:
function TodoList() {
const collection = useSqliteCollection("todos");
const [todos] = collection.useStore();
// Collection is automatically cleaned up on unmount
}
SqliteWorkerManagerManages multiple SQLite databases in a single worker:
import { getSqliteWorkerManager, initializeSqliteWorker } from "@firtoz/drizzle-sqlite-wasm";
// Initialize global worker
await initializeSqliteWorker(SqliteWorker, true);
// Get manager
const manager = getSqliteWorkerManager();
// Get database instance
const db = manager.getDatabase("mydb");
Global Functions:
initializeSqliteWorker(Worker, debug?) - Initialize the global workergetSqliteWorkerManager() - Get the global manager instanceisSqliteWorkerInitialized() - Check if worker is initializedresetSqliteWorkerManager() - Reset the global managerdrizzleSqliteWasm(sqliteDb, config, debug?)Use SQLite WASM directly in the main thread (for testing or synchronous contexts):
import { drizzleSqliteWasm } from "@firtoz/drizzle-sqlite-wasm";
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
const sqlite3 = await sqlite3InitModule();
const sqliteDb = new sqlite3.oo1.DB("/mydb.db", "ct");
const drizzle = drizzleSqliteWasm(sqliteDb, { schema }, true);
// Use drizzle normally
const todos = await drizzle.select().from(todoTable).all();
customSqliteMigrate(config)Run custom SQLite migrations:
import { customSqliteMigrate } from "@firtoz/drizzle-sqlite-wasm/sqlite-wasm-migrator";
await customSqliteMigrate({
database: sqliteDb,
journal: journalData,
migrations: {
"0000_init": "CREATE TABLE todos (id TEXT PRIMARY KEY, title TEXT NOT NULL);",
"0001_add_completed": "ALTER TABLE todos ADD COLUMN completed INTEGER DEFAULT 0;",
},
debug: true,
});
Config:
database: Database - SQLite WASM database instancejournal: Journal - Drizzle journalmigrations: Record<string, string> - SQL migration stringsdebug?: boolean - Enable debug loggingfunction App() {
return (
<>
<DrizzleSqliteProvider
worker={SqliteWorker}
dbName="app-data"
schema={appSchema}
migrations={appMigrations}
>
<AppContent />
</DrizzleSqliteProvider>
<DrizzleSqliteProvider
worker={SqliteWorker}
dbName="cache-data"
schema={cacheSchema}
migrations={cacheMigrations}
>
<CacheManager />
</DrizzleSqliteProvider>
</>
);
}
import { initializeSqliteWorker, getSqliteWorkerManager } from "@firtoz/drizzle-sqlite-wasm";
// Initialize with custom worker
await initializeSqliteWorker(MyCustomWorker, true);
// Get manager for manual control
const manager = getSqliteWorkerManager();
// Access databases
const db1 = manager.getDatabase("app-data");
const db2 = manager.getDatabase("cache-data");
import { eq, and, or, gt, like, desc } from "drizzle-orm";
import { todoTable } from "./schema";
function TodoComponent() {
const { drizzle } = useDrizzleSqlite();
const searchTodos = async (searchTerm: string) => {
return await drizzle
.select()
.from(todoTable)
.where(
and(
like(todoTable.title, `%${searchTerm}%`),
eq(todoTable.completed, false),
gt(todoTable.createdAt, yesterday)
)
)
.orderBy(desc(todoTable.createdAt))
.limit(10)
.all();
};
}
function TodoComponent() {
const { drizzle } = useDrizzleSqlite();
const createTodoWithCategory = async (title: string, category: string) => {
return await drizzle.transaction(async (tx) => {
// Insert category if it doesn't exist
const [existingCategory] = await tx
.select()
.from(categoryTable)
.where(eq(categoryTable.name, category))
.limit(1)
.all();
let categoryId = existingCategory?.id;
if (!categoryId) {
const [newCategory] = await tx
.insert(categoryTable)
.values({ name: category })
.returning();
categoryId = newCategory.id;
}
// Insert todo
const [todo] = await tx
.insert(todoTable)
.values({ title, categoryId })
.returning();
return todo;
});
};
}
const todoTable = syncableTable("todos", {
title: text("title").notNull(),
completed: integer("completed", { mode: "boolean" }).notNull(),
userId: text("userId").notNull(),
}, (table) => [
index("completed_idx").on(table.completed),
index("user_id_idx").on(table.userId),
index("user_completed_idx").on(table.userId, table.completed),
]);
Always use the Worker mode for production to keep the UI responsive:
// âś… Good - Non-blocking
const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
// ❌ Bad - Blocks UI thread
const drizzle = drizzleSqliteWasm(sqliteDb, { schema });
// âś… Good - Single transaction
await drizzle.insert(todoTable).values([
{ title: "Todo 1" },
{ title: "Todo 2" },
{ title: "Todo 3" },
]);
// ❌ Bad - Multiple transactions
for (const title of titles) {
await drizzle.insert(todoTable).values({ title });
}
// âś… Good - Reactive updates
const collection = useSqliteCollection("todos");
const [todos] = collection.useStore(); // Automatically updates
// ❌ Bad - Manual polling
useEffect(() => {
const interval = setInterval(loadTodos, 1000);
return () => clearInterval(interval);
}, []);
Vite: Make sure you're using the ?worker suffix:
import SqliteWorker from "path/to/sqlite.worker?worker";
Webpack: Check that worker-loader is configured or use new URL() pattern
Always wait for the ready promise:
const { drizzle, readyPromise } = useDrizzleSqliteDb(/* ... */);
useEffect(() => {
readyPromise.then(() => {
// Now safe to query
loadData();
});
}, [readyPromise]);
useDrizzleSqliteDb(Worker, dbName, schema, migrations, true)Check the console for specific errors. Common issues:
Enable debug mode for detailed logs:
<DrizzleSqliteProvider
worker={SqliteWorker}
dbName="mydb"
schema={schema}
migrations={migrations}
debug={true} // Enable debug logging
>
Re-exported from @firtoz/drizzle-utils:
import {
syncableTable,
makeId,
type IdOf,
type TableId,
type Branded,
type SelectSchema,
type InsertSchema,
} from "@firtoz/drizzle-sqlite-wasm";
Check out the test playground for complete examples:
tests/test-playground-collections/e2e/ — E2E tests (collections / SQLite demos)@firtoz/drizzle-utils@firtoz/maybe-error@firtoz/worker-helper@sqlite.org/sqlite-wasmdrizzle-orm@tanstack/dbreactzodMIT
Firtina Ozbalikchi firtoz@github.com
FAQs
Drizzle SQLite WASM bindings
We found that @firtoz/drizzle-sqlite-wasm demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A critical vm2 sandbox escape can allow untrusted JavaScript to break isolation and execute commands on the host Node.js process.

Research
Five malicious NuGet packages impersonate Chinese .NET libraries to deploy a stealer targeting browser credentials, crypto wallets, SSH keys, and local files.

Security News
pnpm 11 turns on a 1-day Minimum Release Age and blocks exotic subdeps by default, adding safeguards against fast-moving supply chain attacks.