npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@firtoz/drizzle-durable-sqlite

v0.2.1

Published

TanStack DB collections backed by Drizzle on Cloudflare Durable Object SQLite

Downloads

139

Readme

@firtoz/drizzle-durable-sqlite

TanStack DB collection configuration for Drizzle ORM on Cloudflare Durable Object SQLite (drizzle-orm/durable-sqlite). This mirrors @firtoz/drizzle-sqlite-wasm for the browser (SQLite WASM + workers), but targets Workers/DOs only—no React provider, no OPFS, no Web Workers.

Install

bun add @firtoz/drizzle-durable-sqlite @firtoz/drizzle-utils drizzle-orm @tanstack/db
bun add -d drizzle-kit @cloudflare/workers-types

Optional (only for the Hono + Zod example and a typed honoDoFetcher client):

bun add hono zod @hono/zod-validator @firtoz/hono-fetcher

Drizzle Kit

Use the durable-sqlite driver so generated migrations match DO storage:

import { defineConfig } from "drizzle-kit";

export default defineConfig({
	schema: "./src/schema.ts",
	out: "./drizzle",
	dialect: "sqlite",
	driver: "durable-sqlite",
});

Wrangler

  • Import SQL as text for the migrator (Drizzle DO docs).
  • Use new_sqlite_classes for SQLite-backed Durable Objects.
{
	"rules": [
		{ "type": "Text", "globs": ["**/*.sql"], "fallthrough": true }
	],
	"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }]
}

Durable Object initialization

Recommended for generic DOs: run migrations inside ctx.blockConcurrencyWhile so the schema is ready before fetch or alarms. Example:

import { DurableObject } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/durable-sqlite";
import { migrate } from "drizzle-orm/durable-sqlite/migrator";
import migrations from "../drizzle/migrations.js";
import * as schema from "./schema";

export class MyDurableObject extends DurableObject {
	private db!: ReturnType<typeof drizzle<typeof schema>>;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.ctx.blockConcurrencyWhile(async () => {
			const db = drizzle(ctx.storage, { schema });
			migrate(db, migrations);
			this.db = db;
		});
	}
}

@firtoz/chat-agent-drizzle uses a different pattern: ChatAgentBase calls a synchronous dbInitialize() hook (see DrizzleChatAgent). Use blockConcurrencyWhile when you are not on that agent base class.

TanStack DB collection and CRUD from a Durable Object

Use durableSqliteCollectionOptions with tables built via syncableTable from @firtoz/drizzle-utils (same as WASM). The DO SQLite driver is sync; the shared backend uses synchronous transactions and .all() / .run() for mutations (see @firtoz/drizzle-utils createSqliteTableSyncBackend with driverMode: "sync").

tableName must be the property name on your Drizzle schema object (e.g. export const schema = { todosTable }tableName: "todosTable"), not the SQLite table name string.

If something else must finish before sync runs (e.g. a migration promise), pass readyPromise. When omitted, the collection treats storage as ready immediately (same as Promise.resolve()).

For explicit collection type annotations, use DurableSqliteCollection<TTable>. It preserves select output typing and insert input typing from your Drizzle table schema.

Example schema.ts:

import { syncableTable } from "@firtoz/drizzle-utils";
import { text } from "drizzle-orm/sqlite-core";

export const todosTable = syncableTable("todos", {
	title: text("title").notNull(),
});

export const schema = { todosTable };

Example Durable Object: migrate in blockConcurrencyWhile, create the TanStack collection, then define a Hono app as a class field (chained .get(…), .post(…), …) and forward fetch to it—the same shape as the Durable Object snippet in @firtoz/hono-fetcher.

Route handlers only run when a request is handled; Workers finish your constructor’s blockConcurrencyWhile work before the first fetch, so this.todos is always assigned before any handler runs. Pass this.env into app.fetch so c.env matches Bindings: Env (middleware, secrets, etc.). Use zValidator so JSON bodies and :id params are validated; honoDoFetcherWithName can infer request/response types from that app without a separate exported App type.

For syncMode: "on-demand", await collection.preload() inside blockConcurrencyWhile if you want rows loaded before serving. For syncMode: "eager", you can wait for the first sync with await new Promise<void>((resolve) => collection.onFirstReady(() => resolve())) after preload().

A minimal vitest + DO setup lives in tests/drizzle-durable-sqlite-test.

import { DurableObject } from "cloudflare:workers";
import { zValidator } from "@hono/zod-validator";
import { createCollection } from "@tanstack/db";
import type { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
import { drizzle } from "drizzle-orm/durable-sqlite";
import { migrate } from "drizzle-orm/durable-sqlite/migrator";
import {
	durableSqliteCollectionOptions,
	type DurableSqliteCollection,
} from "@firtoz/drizzle-durable-sqlite";
import { Hono } from "hono";
import { z } from "zod";
import migrations from "../drizzle/migrations.js";
import * as schema from "./schema";

type TodosCollection = DurableSqliteCollection<typeof schema.todosTable>;

export class TodosDurableObject extends DurableObject<Env> {
	private db!: DrizzleSqliteDODatabase<typeof schema>;
	private todos!: TodosCollection;
	app = new Hono<{ Bindings: Env }>()
		.get("/todos", (c) => c.json({ todos: this.todos.toArray }))
		.post(
			"/todos",
			zValidator("json", z.object({ title: z.string() })),
			async (c) => {
				const { title } = c.req.valid("json");
				const tx = this.todos.insert([{ title }]);
				await tx.isPersisted.promise;
				return c.json({ ok: true as const }, 201);
			},
		)
		.patch(
			"/todos/:id",
			zValidator("param", z.object({ id: z.string() })),
			zValidator(
				"json",
				z.object({ title: z.string().optional() }),
			),
			async (c) => {
				const { id } = c.req.valid("param");
				const { title } = c.req.valid("json");
				const tx = this.todos.update(id, (draft) => {
					if (title !== undefined) draft.title = title;
				});
				await tx.isPersisted.promise;
				return c.json(this.todos.state.get(id) ?? null);
			},
		)
		.delete(
			"/todos/:id",
			zValidator("param", z.object({ id: z.string() })),
			async (c) => {
				const { id } = c.req.valid("param");
				const tx = this.todos.delete([id]);
				await tx.isPersisted.promise;
				return new Response(null, { status: 204 });
			},
		)
		.notFound((c) => c.text("Not found", 404));

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);

		this.ctx.blockConcurrencyWhile(async () => {
			const db = drizzle(ctx.storage, { schema });
			migrate(db, migrations);
			this.db = db;

			const todos = createCollection(
				durableSqliteCollectionOptions({
					drizzle: db,
					tableName: "todosTable",
					syncMode: "eager",
				}),
			);
			this.todos = todos;
			todos.preload(); // Preload to ensure the data's in the collection from storage.
			await new Promise<void>((resolve) => todos.onFirstReady(() => resolve()));
		});
	}

	fetch(request: Request) {
		return this.app.fetch(request, this.env);
	}
}

Typed client from your worker (same idea as Durable Objects in @firtoz/hono-fetcher):

import { honoDoFetcherWithName } from "@firtoz/hono-fetcher";

const api = honoDoFetcherWithName(env.TODOS, "my-list");

await api.post({
	url: "/todos",
	body: { title: "Buy milk" },
});

const list = await api.get({ url: "/todos" });
const data = await list.json();

body / response inference follows your zValidator + c.json shapes; use params: { id: "…" } on /todos/:id routes. If inference ever fails to pick up the DO’s app type, pass an explicit generic, e.g. honoDoFetcherWithName<InstanceType<typeof TodosDurableObject>["app"]>(…).

Adjust paths (../drizzle/migrations.js, ./schema) and Env to match your worker. To return the created row from POST, read from this.todos.state / toArray after isPersisted and return c.json(…) with a shape that matches what you want the fetcher to infer.

License

MIT