Why we wrote an ORM only for Cloudflare D1

CloudflareD1TypeScriptOpen source

While working on a product that runs on Cloudflare Workers, we looked at what was actually in the bundle and noticed something. Most of the ORM was there for databases we do not use.

Tree-shaking cannot remove generality

A common belief is that the bundler will drop whatever you do not use. That is half true. Tree-shaking removes code that cannot be reached, not code that is merely general.

Here is what stays reachable from a general-purpose ORM’s SQLite entry point.

  • The dialect indirection, which decides what SQL to assemble for which database
  • The transaction and savepoint subsystem
  • The prepared-statement abstraction that covers both sync and async drivers

Every one of those is reachable from a single query of yours, which is why the bundler keeps them. To get rid of them, they have to not exist in the source in the first place.

Narrowing to one target

So we wrote an ORM that targets Cloudflare D1 and Workers and nothing else. It is called d1zzle. The API is inherited from Drizzle, so there is next to no migration cost.

import { drizzle, eq, integer, sqliteTable, text } from 'd1zzle';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').notNull().unique(),
});

const db = drizzle(env.DB);
await db.select().from(users).where(eq(users.id, 1)).get();

Bundling a Worker with esbuild that contains the driver, the schema DSL and one query gives this.

minifiedgzipped
drizzle-orm/d1 + drizzle-orm/sqlite-core77.8 kB22.2 kB
d1zzle44.1 kB15.3 kB
−43%−31%

To be clear, this is not a story about Drizzle being bloated. Drizzle declares sideEffects: false correctly and tree-shakes very well. The only difference is whether what disappears is code you do not use, or code that exists to be general.

Size was not the main prize

The real gain was elsewhere. Once you narrow the target, the features that have no counterpart in other databases can be treated as first class. D1’s positional read path, its bind-parameter limit, the Sessions API, the billing counters. In a general-purpose ORM those sit on the far side of the abstraction, and reaching them means hunting for an escape hatch.

Running your own open-source dependency in production

d1zzle was not published and abandoned. It runs the entire data layer of arukutomaru in production, and Better Auth sits on the same ORM. Rough edges we hit while using it get written up as notes in the consuming repository and then fixed in the library. Instead of filing an issue with a vendor and waiting, we can fix it ourselves, and that turned out to be the real payoff of writing one of our dependencies.


Nami Smart LLC

High-quality apps, built faster and cheaper than anyone else. If you would like to talk about a project, please get in touch.

Contact us