
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-indexeddb
Advanced tools
Drizzle-shaped schemas and migrations on top of IndexedDB β TanStack DB collections in the browser with React hooks and generated migration functions (SQLite-flavored Drizzle types today).
β οΈ 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.
Note: This package currently builds on top of Drizzle's SQLite integration (using
drizzle-orm/sqlite-coretypes) until Drizzle adds native IndexedDB support. The migration system uses function-based migrations generated from Drizzle's SQLite migrations to create IndexedDB object stores and indexes.
npm install @firtoz/drizzle-indexeddb @firtoz/drizzle-utils drizzle-orm @tanstack/db
deletedAt column// 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),
});
# Generate Drizzle migrations
drizzle-kit generate
# Generate IndexedDB migration functions
bun drizzle-indexeddb-generate
// db.ts
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
import migrations from "./drizzle/indexeddb-migrations";
export const db = await migrateIndexedDBWithFunctions(
"my-app",
migrations,
true // Enable debug logging
);
// App.tsx
import { DrizzleIndexedDBProvider, useIndexedDBCollection } from "@firtoz/drizzle-indexeddb";
import { createCollection } from "@tanstack/db";
function App() {
return (
<DrizzleIndexedDBProvider db={db} schema={schema}>
<TodoList />
</DrizzleIndexedDBProvider>
);
}
function TodoList() {
const collection = useIndexedDBCollection("todos");
const [todos] = collection.useStore();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
The primary feature of this package: Create reactive, type-safe collections backed by IndexedDB.
Create reactive collections backed by IndexedDB:
import { createCollection } from "@tanstack/db";
import {
drizzleIndexedDBCollectionOptions,
type DrizzleIndexedDBCollection,
} from "@firtoz/drizzle-indexeddb";
import * as schema from "./schema";
const todosCollection = createCollection(
drizzleIndexedDBCollectionOptions({
indexedDBRef: { current: db },
table: schema.todoTable,
storeName: "todos",
syncMode: "on-demand", // or "realtime"
})
);
type TodosCollection = DrizzleIndexedDBCollection<typeof schema.todoTable>;
// Subscribe to changes
const unsubscribe = todosCollection.subscribe((todos) => {
console.log("Todos updated:", todos);
});
// CRUD operations
await todosCollection.insert({
title: "Buy milk",
completed: false,
});
await todosCollection.update(todoId, {
completed: true,
});
await todosCollection.delete(todoId); // Soft delete (sets deletedAt)
// Query with filters
const completedTodos = await todosCollection.find({
where: { completed: true },
orderBy: { createdAt: "desc" },
limit: 10,
});
interface IndexedDBCollectionConfig {
db: IDBDatabase;
tableName: string;
syncMode?: "on-demand" | "realtime";
debug?: boolean;
}
deletedAt) or hard delete// Filtering
collection.find({
where: {
completed: false,
title: { contains: "urgent" },
priority: { in: ["high", "critical"] },
createdAt: { gt: yesterday },
}
});
// Sorting
collection.find({
orderBy: { createdAt: "desc" }
});
// Pagination
collection.find({
limit: 10,
offset: 20,
});
// Soft delete filtering (automatic)
// By default, records with deletedAt !== null are excluded
Use generated migration functions to migrate your IndexedDB schema:
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
import migrations from "./drizzle/indexeddb-migrations";
const db = await migrateIndexedDBWithFunctions(
"my-app-db",
migrations,
true // debug flag
);
console.log("Database migrated successfully!");
Features:
Migration Tracking:
Migrations are tracked in the __drizzle_migrations object store:
interface MigrationRecord {
id: number; // Migration index
tag: string; // Migration name
when: number; // Migration timestamp
appliedAt: number; // When it was applied
}
For complex migrations that require custom logic, you can write migration functions directly:
import { migrateIndexedDBWithFunctions } from "@firtoz/drizzle-indexeddb";
const migrations = [
// Migration 0: Initial schema
{
tag: "0000_initial",
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
const store = db.createObjectStore("todos", { keyPath: "id" });
store.createIndex("title", "title", { unique: false });
},
},
// Migration 1: Add completed index
{
tag: "0001_add_completed",
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
const store = transaction.objectStore("todos");
store.createIndex("completed", "completed", { unique: false });
},
},
// Migration 2: Transform data
{
tag: "0002_add_priority",
migrate: async (db: IDBDatabase, transaction: IDBTransaction) => {
const store = transaction.objectStore("todos");
const todos = await new Promise<any[]>((resolve, reject) => {
const req = store.getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
// Transform data
for (const todo of todos) {
todo.priority = todo.priority || "medium";
store.put(todo);
}
},
},
];
const db = await migrateIndexedDBWithFunctions("my-app-db", migrations, true);
Wrap your app with the provider:
import { DrizzleIndexedDBProvider } from "@firtoz/drizzle-indexeddb";
function App() {
return (
<DrizzleIndexedDBProvider db={db} schema={schema}>
<YourApp />
</DrizzleIndexedDBProvider>
);
}
Access the context:
import { useDrizzleIndexedDB } from "@firtoz/drizzle-indexeddb";
function MyComponent() {
const { getCollection } = useDrizzleIndexedDB();
const todosCollection = getCollection("todos");
const usersCollection = getCollection("users");
// Use collections...
}
Features:
Hook for a specific collection:
import { useIndexedDBCollection } from "@firtoz/drizzle-indexeddb";
function TodoList() {
const collection = useIndexedDBCollection("todos");
// Automatic ref counting and cleanup
useEffect(() => {
return () => {
// Collection automatically cleaned up when component unmounts
};
}, []);
// Use collection...
}
Completely delete an IndexedDB database:
import { deleteIndexedDB } from "@firtoz/drizzle-indexeddb";
await deleteIndexedDB("my-app-db");
console.log("Database deleted!");
Useful for:
Generate IndexedDB migration files from Drizzle snapshots programmatically:
import { generateIndexedDBMigrations } from "@firtoz/drizzle-indexeddb";
generateIndexedDBMigrations({
drizzleDir: "./drizzle", // Path to Drizzle directory (default: ./drizzle)
outputDir: "./drizzle/indexeddb-migrations", // Output directory (default: ./drizzle/indexeddb-migrations)
});
The package includes a CLI tool to generate IndexedDB migrations from Drizzle schema snapshots.
# Generate migrations (run after drizzle-kit generate)
bun drizzle-indexeddb-generate
# With custom paths
bun drizzle-indexeddb-generate --drizzle-dir ./db/drizzle
bun drizzle-indexeddb-generate --output-dir ./src/migrations
# Show help
bun drizzle-indexeddb-generate --help
| Option | Description | Default |
|---|---|---|
--drizzle-dir <path>, -d | Path to Drizzle directory | ./drizzle |
--output-dir <path>, -o | Path to output directory | <drizzle-dir>/indexeddb-migrations |
Add to your package.json:
{
"scripts": {
"db:generate": "bun --bun drizzle-kit generate && bun drizzle-indexeddb-generate"
}
}
Note: The
--bunflag forces bun's runtime instead of Node, which is needed because this package exports raw TypeScript. See Troubleshooting if you encounter type stripping errors.
import { indexedDBCollectionOptions } from "@firtoz/drizzle-indexeddb";
const collection = createCollection(
indexedDBCollectionOptions({
db,
tableName: "todos",
syncMode: "realtime", // Subscribe to changes automatically
debug: true, // Enable debug logging
})
);
Clear all data from a collection:
// Clear all todos
await todoCollection.utils.truncate();
This clears the IndexedDB store and updates the local reactive store.
For scenarios where IndexedDB needs to be accessed over a messaging layer (e.g., Chrome extensions, WebSockets), the proxy system enables multi-client sync:
βββββββββββ βββββββββββ βββββββββββ
β Client 1β β Client 2β β Client Nβ
ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ
β β β
βββββββββββββββββΌββββββββββββββββ
β
ββββββββΌβββββββ
β Server β
β (manages β
β IndexedDB) β
βββββββββββββββ
import {
createMultiClientTransport,
createProxyServer,
createProxyIDbCreator,
migrateIndexedDBWithFunctions,
DrizzleIndexedDBProvider,
} from "@firtoz/drizzle-indexeddb";
// Create transport (in-memory for testing, or custom for production)
const { createClientTransport, serverTransport } = createMultiClientTransport();
// Create server with migrations
const server = createProxyServer({
transport: serverTransport,
dbCreator: async (dbName) => {
return await migrateIndexedDBWithFunctions(dbName, migrations);
},
});
// Create client
const clientTransport = createClientTransport();
const dbCreator = createProxyIDbCreator(clientTransport);
// Use with React provider
function App() {
const handleSyncReady = useCallback((handleSync) => {
clientTransport.onSync(handleSync);
}, []);
return (
<DrizzleIndexedDBProvider
dbName="my-app.db"
schema={schema}
dbCreator={dbCreator}
onSyncReady={handleSyncReady}
>
<YourApp />
</DrizzleIndexedDBProvider>
);
}
// Server setup (once)
const { createClientTransport, serverTransport } = createMultiClientTransport();
const server = createProxyServer({ transport: serverTransport, ... });
// Each client gets its own transport
const client1Transport = createClientTransport();
const client2Transport = createClientTransport();
const client3Transport = createClientTransport();
// All clients share the same data and receive real-time sync
All standard collection operations automatically sync:
// Client 1 inserts
await todoCollection.insert({ title: "Buy milk", completed: false });
// β Client 2, 3, N receive the new todo instantly
// Client 2 updates
await todoCollection.update(todoId, (draft) => {
draft.completed = true;
});
// β Client 1, 3, N see the update instantly
// Client 3 deletes
await todoCollection.delete(todoId);
// β Client 1, 2, N see the deletion instantly
// Client N truncates
await todoCollection.utils.truncate();
// β All clients are cleared instantly
For production use (Chrome extension, WebSocket, etc.), implement the transport interface:
import type { IDBProxyClientTransport, IDBProxyServerTransport } from "@firtoz/drizzle-indexeddb";
// Client transport (e.g., in content script)
const clientTransport: IDBProxyClientTransport = {
sendRequest: async (request) => {
// Send to background script and wait for response
return await chrome.runtime.sendMessage(request);
},
onSync: (handler) => {
// Listen for sync broadcasts
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type?.startsWith("sync:")) handler(msg);
});
},
};
// Server transport (e.g., in background script)
const serverTransport: IDBProxyServerTransport = {
onRequest: (handler) => {
chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => {
const response = await handler(msg);
sendResponse(response);
});
},
broadcast: (message, excludeClientId) => {
// Broadcast to all connected tabs except sender
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
if (tab.id !== excludeClientId) {
chrome.tabs.sendMessage(tab.id, message);
}
}
});
},
};
try {
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
} catch (error) {
console.error("Migration failed:", error);
// Option 1: Delete and start fresh
await deleteIndexedDB("my-app");
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
// Option 2: Handle specific errors
if (error.message.includes("Primary key structure changed")) {
// Guide user to export data, delete DB, and reimport
}
}
// Enable debug mode to see performance metrics
const db = await migrateIndexedDBWithFunctions("my-app", migrations, true);
// Output shows:
// [PERF] IndexedDB function migrator start for my-app
// [PERF] Latest applied migration index: 5 (checked 5 migrations)
// [PERF] Found 2 pending migrations to apply: ["add_priority", "add_category"]
// [PERF] Upgrade started: v5 β v7
// [PERF] Creating object store: categories
// [PERF] Creating index: name on categories
// [PERF] Migration 5 complete
// [PERF] Migration 6 complete
// [PERF] All 2 migrations applied successfully
// [PERF] Migrator complete - database ready
Just update your schema and regenerate:
// Before
const todoTable = syncableTable("todos", {
title: text("title").notNull(),
});
// After
const todoTable = syncableTable("todos", {
title: text("title").notNull(),
priority: text("priority").notNull().default("medium"),
});
drizzle-kit generate
The migrator handles it automatically!
const todoTable = syncableTable("todos", {
title: text("title").notNull(),
completed: integer("completed", { mode: "boolean" }),
}, (table) => [
index("title_idx").on(table.title),
index("completed_idx").on(table.completed),
]);
Drizzle migrations don't track renames directly, but you can:
Remove from schema and regenerate - the migrator will delete the object store.
If you see this error when running drizzle-kit generate:
Error [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]: Stripping types is currently unsupported for files under node_modules
This happens because this package exports raw TypeScript files, and Node's built-in type stripping doesn't work inside node_modules.
Solution: Use bun --bun to force bun's runtime instead of Node:
bun --bun drizzle-kit generate
Or in your package.json:
{
"scripts": {
"db:generate": "bun --bun drizzle-kit generate && bun drizzle-indexeddb-generate"
}
}
Alternative: If you're not using bun, use tsx:
npx tsx node_modules/drizzle-kit/bin.cjs generate --config ./drizzle.config.ts
This happens when you change the primary key of a table. IndexedDB doesn't support changing keyPath after creation.
Solution:
await deleteIndexedDB("my-app")drizzle/indexeddb-migrations/bun drizzle-indexeddb-generate to regeneratesyncMode: "on-demand" for collections that don't need real-time updatesdeletedAt soft deletes instead of hard deletes for better performanceMIT
Firtina Ozbalikchi firtoz@github.com
FAQs
IndexedDB migrations powered by Drizzle ORM
We found that @firtoz/drizzle-indexeddb 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.