Nami Smart LLC

Why we wrote an ORM only for Cloudflare D1

Updated: September 6, 2026 CloudflareD1TypeScriptOSS

Cloudflare D1 has limits and features with no counterpart in other databases, and an ORM that abstracts for portability hides them. This is why we wrote orm-d1 (formerly d1zzle), an ORM targeting only D1 and Workers, and how it is designed.

The API matches Drizzle, so schemas and queries are written the same way.

import { drizzle, eq, sqliteTable, text } from 'orm-d1';

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

Per-plan limits, warned at runtime

D1’s limits differ between the free and paid plans, and the library can be told which one it runs on.

const db = drizzle(env.DB, { plan: 'free' }); // 'paid' is also accepted
freepaid
Queries per Worker invocation501,000
Database size500 MB10 GB

Neither is known until statements actually run, so these are development-only warnings. Queries inside batch() are counted individually, because D1 counts them that way, which means batch() is not a way around the limit. For size, the meta.size_after D1 returns on every statement is checked, and one warning is emitted once it passes 90% of the limit.

Fixed limits, checked at build time

Limits that do not depend on the plan are checked while the query is built. A Worker isolate compiles once, and the query is already being walked at that point, so the check costs almost nothing.

LimitValue
Bind parameters per query100
SQL statement length100,000 bytes
Arguments to a SQL function32
Columns per table100

SQLite’s too many SQL variables does not say which inArray caused it; an error at build time can name the call site. The limit is also avoided: inserts are split automatically, and inArray expands to json_each, folding a long list into a single parameter.

Decisions that follow from D1

  • Reads are positional (.raw()). .all() builds a keyed object per row, so duplicate column names in a join lose one of the values.
  • batch() is the unit of atomicity. It is the only atomicity guarantee D1 offers.
  • No transaction(). D1 has no interactive transactions, and a BEGIN may reach a different connection.
  • Sessions and bookmarks are supported. They are D1’s read replication mechanism and have no counterpart in Postgres or MySQL drivers.
  • rows_read / rows_written are returned. They are D1’s billing unit and are present in every response.

Bundle size

A Worker containing the driver, the schema DSL and one query, bundled with esbuild.

minifiedgzipped
drizzle-orm/d1 + drizzle-orm/sqlite-core77.8 kB22.2 kB
orm-d144.1 kB15.3 kB

The difference is not tree-shaking. Only unreachable code can be dropped, and both the dialect indirection and the transaction implementation are reachable from the SQLite entry point.

orm-d1 runs in production in arukutomaru, with Better Auth on top of it.


Nami Smart LLC

Fast, cost-efficient, high-quality app development. If you would like to talk about a project, please get in touch.

Contact us