| name | data-access |
| description | Apply when reading or writing data, designing schema, choosing a datastore, or wiring persistence — covers Drizzle ORM usage, SQLite for dev/test, Cloudflare D1 for prod, Firebase Storage links, and the absolute ban on the Repository pattern. Load whenever touching the database, migrations, queries, or storage. |
🗄️ Data Access
🧭 Datastores
- SQLite — dev and testing (fast, zero-setup, in-memory for tests).
- Cloudflare D1 — production. Write portable Drizzle; avoid SQLite-only quirks in shared code unless D1 supports them.
- Firebase Storage — downloadable item delivery. Generate expiring links when needed; do not persist download URLs.
🧱 Drizzle — the only ORM
All relational data access goes through Drizzle. Define schema with Drizzle's schema builder; query with its typed query API. Keep schema close to its domain (lib/orders/orders.schema.ts or inside the model file).
🚫 No Repository pattern — ever
This is the hill. Do not create OrderRepository, IRepository<T>, a repositories/ folder, or any generic data-access seam.
Instead, the domain service talks to Drizzle directly:
export class OrderService {
constructor(private readonly db: Database) {}
async findById(id: number): Promise<Order | null> {
const row = await this.db.query.orders.findFirst({ where: eq(orders.id, id) });
return row ? Order.from(row) : null;
}
}
The service is the persistence boundary. No repository in between. If you feel the urge to add one "for testability" — inject the Drizzle db instead and use SQLite in-memory in tests.
🔌 Wiring
The db instance is created once and injected (dependency inversion). Compose it at the application's Composite Root and pass it down — never a global Singleton, never new Database() inside a service method.
🧪 Testing data code
Use SQLite in-memory, run real Drizzle queries against it, assert on outcomes. No mocking the ORM — test against a real (throwaway) database. See testing-standards.