Drizzle ORM Conventions
SkillFiles & storageApply when writing or modifying DB schema files in packages/db-schema or tRPC routers. Esposter's Drizzle ORM conventions, how a table, a relation and a query are written and how a migration is produced; the v2 relations API (defineRelationsPart, object-based where and orderBy) never v1, every table in its product area's Postgres schema and exported as <table>In<Schema>, registries generated rather than kept, requireMutation on .returning(), empty sentinels over null, and db:gen as the only migration generator.
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Drizzle ORM Conventions skill
What this skill tells your AI
The instructions your AI receives, as published by esposter/esposter in .agents/skills/drizzle/SKILL.md and read by ahel’s review.
Settled — do not re-propose
- A hand-kept registry, or a test that a registry is complete —
pnpm registry:genwrites both from the folders, so a declaration is registered by existing and there is nothing left to check (references/schema-registration.md). - A table or an enum in
public— every one lives in the schema of the product area that owns it (references/schemas-and-names.md). - A bare table export (
rooms,users) — it collides with a local of the same name, a row type or a library's name (livekit'sRoom, theUserStatusenum); theIn<Schema>suffix is derived, never chosen, andschema.test.tsholds it.
Deep dives
references/relations-v2.md— when adding or editing a file inpackages/db-schema/src/relations/, or writing a relational query'swhere/orderBy/with.references/migrations.md— when runningdb:gen, editing a generatedmigration.sql, regenerating the db-mock snapshot, or recovering a forked migration chain.references/table-constraints.md— when adding a CHECK constraint, unique constraint or index to a table.references/table-definition.md— when adding or editing a table, a column or a reference.references/schemas-and-names.md— when adding a table or an enum, choosing its schema, naming anything derived from a table, or moving a table to another schema.references/schema-registration.md— when adding a table, an enum, a schema or a relation part, or a migration fails on a missing type or schema.references/queries.md— when writing a query: the select shape, relational or SQL-style, a self-join, a batch insert.references/returning.md— when a write returns its rows:requireMutation, the full entity,[0]againsttakeOne, and a lost claim.references/sentinel-columns.md— when adding an optional column or inserting a possibly-absent value.references/primary-keys.md— when choosing a new table's primary key.
Column Names
A column builder is called bare, never with a name string (no-restricted-syntax): the pgTable wrapper names every column after its key (references/table-definition.md).
Table Definition
- Every table goes through the
pgTablewrapper, and every DB identifier is camelCase — the table name is the literal DDL name, held byschema.test.ts. - A column holding another table's id gets
.references(), with theonDeletethe domain means — except a column projected from user-authored content on every save, which stays a bare id. - Each table writes its own column block, even when two are twins — factor the predicate, never the columns.
- The full statement of each, and why a suite fighting a new reference is reporting its own fixtures:
references/table-definition.md.
Schemas, Names and Registries
- Every table and enum lives in its product area's Postgres schema — a folder under
src/schema/, neverpublic(references/schemas-and-names.md). - Every name drizzle derives from a table carries its schema —
usersInAuth,UserInAuth,selectUserInAuthSchema,usersInAuthRelation— held byschema.test.ts(references/schemas-and-names.md). - The registries are generated —
pnpm registry:gen, run bybuildanddb:gen; nothing is registered by hand (references/schema-registration.md).
Selects
getColumns(table) for a flat result, { alias: table } for a namespaced one, and a bare .select() only for one table (references/queries.md).
Query API: Relational vs SQL-style
The relational API for every read; SQL-style for writes, multi-column OR joins, aggregates and upserts; a read's limit is MAX_READ_LIMIT or DEFAULT_READ_LIMIT (references/queries.md).
Relations (v2 API) — at a glance
- Never the v1
relations()function (no-restricted-syntax) — the repo is on Drizzle v2'sdefineRelationsPart, and v1 is incompatible. whereandorderByare object-based, never v1 callbacks —where: { id: { eq: input } },orderBy: { createdAt: "desc" }.createSelectSchemaalways imports fromdrizzle-orm/zod, never fromdrizzle-zod(the v1 package, ano-restricted-syntaxerror).
Self-Joins (Same Table Twice)
Both sides of a self-join are alias()es named foo1, foo2 (references/queries.md).
Batch Inserts
One INSERT over an array, never a loop of them (references/queries.md).
.returning()
requireMutation on the first row, the full entity returned, and [0] rather than takeOne wherever a guard reads the absence (references/returning.md).
Empty-Sentinel Columns — the DB Schema Is the Source of Truth
An optional text column is .notNull().default("") and a numeric one .default(0) where 0 means nothing; null stays only for a timestamp or a semantically distinct absence (references/sentinel-columns.md).
Optional Insert Values
Never ?? null on an insert unless null means something the schema distinguishes (references/sentinel-columns.md).
Time Duration Columns
A duration is stored in milliseconds, never seconds, minutes or hours unless it needs sub-millisecond precision, and its column carries the Ms suffix the naming skill sets (references/numbers-and-time.md) — durationMs: integer().notNull().
Primary Keys
A UUID for a referenced entity, a text natural key, a composite for a pure join table, and a random code as id where one already identifies the row (references/primary-keys.md).
Migrations
db:gen(frompackages/db-schema/) is the only way to produce a migration, andsnapshot.jsonis machine state, never hand-cloned — a copied snapshot forks the chain the instant two migrations descend from one parent (references/migrations.md).db:genis never run as an unprompted side effect of a schema edit — note the pending migration and let the user decide; migrations apply at app startup (apps/web/server/plugins/migrate.ts), never from the CLI.
Signals
- GitHub stars
- 23
- Forks
- 3
- Last commit
- Oct 2026
- Hacker News mentions
- 12
Others that do the same job
Advanced
- Item type
- skill
- Key
drizzle-esposter- Source
- github.com/esposter/esposter
Related picks
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScriptsupabase-postgres-best-practices
Skill · supabase
The pick for Postgrespptx
Skill · anthropics
More in Files & storagedocx
Skill · anthropics
More in Files & storageresearch
Skill · mattpocock
More in Files & storage