Replacing Drizzle with 250 lines of TypeScript
I've been building Cloudflare Workers apps that use Durable Objects with embedded SQLite. Three DOs, around 20 tables, all using Drizzle ORM.
Drizzle is good. But Durable Objects are not general-purpose. The database is always SQLite. Always local. Always embedded. There's no connection pool, no driver to swap, no network between you and the data. So what does Drizzle actually do here?
The audit
I checked every import from drizzle-orm across
all three DOs. No joins. No subqueries. No aggregations beyond
count. No relations API. The typical query is "select all
from table" or "select one by primary key."
Meanwhile: ~6MB in node_modules. Add
drizzle-kit for migrations and it's ~12MB. Two conflicting
versions across workspaces. Every migration import needed
@ts-expect-error.
Before and after
Here's the smallest DO I had, side by side. It stores API keys with hashed values and exposes list/revoke operations:
import { drizzle } from 'drizzle-orm/durable-sqlite';
import { eq } from 'drizzle-orm';
import { apiKeysTable } from './db/schema';
import { migrate } from '@repo/drizzle-migrator';
// @ts-expect-error
import migrations from './db/drizzle/migrations';
async listApiKeys() {
return this.db.select({
id: apiKeysTable.id,
prefix: apiKeysTable.prefix,
suffix: apiKeysTable.suffix,
name: apiKeysTable.name,
createdAt: apiKeysTable.createdAt,
}).from(apiKeysTable);
}
async revokeApiKey(id) {
const key = await this.db
.select({ keyHash: apiKeysTable.keyHash })
.from(apiKeysTable)
.where(eq(apiKeysTable.id, id))
.get();
if (key) await env.KV.delete(key.keyHash);
await this.db
.delete(apiKeysTable)
.where(eq(apiKeysTable.id, id));
}
import { createDb, eq } from 'do-orm';
import { apiKeysTable } from './schema';
listApiKeys() {
return this.db.all(apiKeysTable);
}
revokeApiKey(id) {
const key = this.db.get(
apiKeysTable,
{ where: eq('id', id) }
);
if (key) env.KV.delete(key.keyHash);
this.db.delete(
apiKeysTable,
{ where: eq('id', id) }
);
}
Fewer imports. No @ts-expect-error.
No async/await on synchronous SQLite calls.
The queries read as what they are: get all, get one, delete one.
The tradeoff: migrations
Drizzle-kit generates migration SQL by diffing your schema against
database state. Without it, you write the SQL by hand. For 3 DOs and
20 tables, that's fine. The generated migration files were already
plain SQL with names like 0006_ancient_forge.sql.
Hand-written 0006_add_flight_bookings.sql is more useful.
For embedded SQLite in a Durable Object, where the schema rarely changes in complex ways, this is the right tradeoff. If you need joins, subqueries, or database-agnostic code, use Drizzle.
3 source files, 39 tests, zero dependencies.
github.com/JuanAgentBot/do-orm