Accepted

ADR 0010 — Raw D1 prepared statements, not an ORM

Cusp talks to D1 through raw prepared statements behind a repository wrapper that owns the chunked-write and versioned-write contracts

Decision

Cusp accesses D1 through raw prepared statements — db.prepare(...).bind(...).run() — wrapped by a repository layer. No ORM sits between the application and SQLite. Each repository exposes per-row interfaces that give type safety at the boundary, and reads the way the SQL reads.

Two contracts live in the repository wrapper, independent of the access style:

  • Chunked writes — multi-row writes go through the chunk() helper with a batch-size cap of 50, keeping every statement clear of SQLite's 999-parameter bind limit.
  • Versioned writes — mutations of versioned entities go through the versioned-write helper, which records the entity_versions row in the same D1 batch as the live-row mutation, so the snapshot and the mutation succeed or fail together.

Why not an ORM

An ORM buys query-builder ergonomics and migration tooling. Cusp needs neither: migrations are hand-written raw .sql files applied by an embedded runner, and the per-row repository interfaces already provide boundary type safety. Staying on raw statements keeps the Worker bundle smaller and removes the friction between an ORM's query types and D1's native prepared-statement types.

Consequences

The repository layer is the canonical reference for the raw-statement pattern, and the documentation matches the code. The cost is that genuinely complex queries must be written by hand — a real but bounded trade-off.

Versioned writes (ADR 0007)