Drizzle SQLite Scaffold
Parameterized templates for bootstrapping Drizzle + SQLite in a fresh project, or adding a new table/repository to an existing one. Every output bakes in the conventions documented in references/conventions.md — explicit primary keys, indexed foreign keys, relations() declarations, $inferSelect/$inferInsert exports, timestamp_ms dates, boolean-mode bools, WAL + foreign_keys=ON + busy_timeout pragmas, singleton client with HMR guard, and CRUD helpers using .returning() + inArray() + .onConflictDoUpdate().
When to Apply
Reach for these templates when:
- Starting a new project that will use Drizzle with SQLite (any driver)
- Adding a new table to an existing Drizzle project — the table file should match the existing patterns
- Adding a CRUD repository module for an existing table
- Refactoring a hand-rolled Drizzle setup that's missing pragmas, has no
relations(), or has hand-written User types that drift from the schema
- Migrating from another ORM (Prisma, Kysely) to Drizzle and wanting consistent shapes from the start
Setup
Required parameters (asked on first use, saved to config.json)
| Parameter |
Required |
Default |
Values |
driver |
yes |
— |
better-sqlite3 | libsql | bun-sqlite (D1 has a different lifecycle — see "Cloudflare D1" below) |
db_url_env |
no |
DATABASE_URL |
env var name |
schema_dir |
no |
./src/db/schema |
per-table schema files |
repository_dir |
no |
./src/db/repository |
per-table CRUD modules |
validators_dir |
no |
./src/db/validators |
drizzle-zod schemas (when with_zod=true) |
client_path |
no |
./src/db/client.ts |
singleton client module |
migrations_dir |
no |
./drizzle |
drizzle-kit output |
If config.json already exists with values, this skill uses them; otherwise it asks via AskUserQuestion.
Per-table parameters (asked each time a new table is scaffolded)
| Parameter |
Required |
Default |
Description |
name |
yes |
— |
Kebab-case singular: user, order-item. Used for filenames and TS identifiers (name_camel, name_pascal derived). |
table_name |
no |
snake_case plural of name |
SQL table name: users, order_items |
pk |
no |
serial-int |
serial-int | uuid | cuid2 | text |
timestamps |
no |
true |
adds createdAt/updatedAt columns |
soft_delete |
no |
false |
adds nullable deletedAt + partial index |
relations |
no |
[] |
list of related table names — expands relations() body |
with_zod |
no |
true |
emits a drizzle-zod validators file |
Available Templates
Project-init templates (emit once)
| Template |
Output File |
When |
drizzle.config.local.ts.template |
drizzle.config.ts |
driver is better-sqlite3, bun-sqlite, or libsql with a file: URL |
drizzle.config.turso.ts.template |
drizzle.config.ts |
driver is libsql against Turso (remote libsql: URL) |
client.better-sqlite3.ts.template |
{{client_path}} |
driver is better-sqlite3 |
client.libsql.ts.template |
{{client_path}} |
driver is libsql |
client.bun-sqlite.ts.template |
{{client_path}} |
driver is bun-sqlite |
schema-index.ts.template |
{{schema_dir}}/index.ts |
Always (initially empty; append exports as tables are added) |
gitignore.template |
.gitignore (append) |
Always |
Per-table templates (emit once per table)
| Template |
Output File |
When |
table.ts.template |
{{schema_dir}}/{{name}}.ts |
Per table |
repository.ts.template |
{{repository_dir}}/{{name}}.ts |
Per table |
validators.ts.template |
{{validators_dir}}/{{name}}.ts |
Per table when with_zod=true |
How to Use
Flow A — Initialize a new project (run once)
Resolve project parameters. Read config.json. For any required field that's empty, ask the user via AskUserQuestion (driver is the only strictly required one; the rest have sensible defaults).
Install runtime + tooling first so the rendered files type-check immediately:
# Pick the driver-specific runtime package:
npm install drizzle-orm @libsql/client # for libsql
npm install drizzle-orm better-sqlite3 # for better-sqlite3
npm install drizzle-orm # bun:sqlite is built into Bun
# Dev tools (all drivers):
npm install -D drizzle-kit
npm install -D @types/better-sqlite3 # better-sqlite3 only
npm install -D drizzle-zod zod # if with_zod=true
Pick the config and client variants for the resolved driver:
better-sqlite3 → drizzle.config.local.ts.template + client.better-sqlite3.ts.template
libsql with file: URL → drizzle.config.local.ts.template + client.libsql.ts.template
libsql with remote Turso URL → drizzle.config.turso.ts.template + client.libsql.ts.template
bun-sqlite → drizzle.config.local.ts.template + client.bun-sqlite.ts.template
Render and write the project-init files:
drizzle.config.ts
{{client_path}} (typically src/db/client.ts)
{{schema_dir}}/index.ts (empty barrel)
- Append the
gitignore.template block to the project's .gitignore
For libsql: the client template uses top-level await migrate(...). Verify tsconfig.json has "module": "ESNext" (or "NodeNext") and "target": "ES2022"+ for top-level await support. If the runtime is CommonJS, replace the top-level await with an exported async function init() the app calls during startup.
Save resolved values to config.json so subsequent table runs don't re-prompt.
Flow B — Add a new table (run per table)
Resolve per-table parameters. Ask the user for name, then offer defaults for table_name (snake_case plural), pk, timestamps, soft_delete, relations, with_zod. Use AskUserQuestion for any non-default the user wants.
Compute derived identifiers:
name_camel — camelCase of name (user, orderItem)
name_pascal — PascalCase of name (User, OrderItem)
pk_field — the PK column name (id for all 4 pk modes)
pk_ts_type — TS type for the PK (number for serial-int, string for uuid/cuid2/text)
pk_definition — the actual line, e.g., id: integer().primaryKey({ autoIncrement: true }), (see PK Variants table below)
Render the table template: Read table.ts.template, substitute {{name}}, {{name_camel}}, {{name_pascal}}, {{table_name}}, {{pk_definition}}, {{pk_extra_imports}}, etc. Expand {{timestamps_block}} and {{soft_delete_block}} per the parameters (see "Block Expansions" below). Write to {{schema_dir}}/{{name}}.ts.
Render the repository template with the same parameters. Write to {{repository_dir}}/{{name}}.ts.
If with_zod=true, render the validators template. Write to {{validators_dir}}/{{name}}.ts.
Append to the schema barrel: Add export * from './{{name}}'; to {{schema_dir}}/index.ts.
Generate the migration: Tell the user to run npx drizzle-kit generate to produce the SQL file. Remind them to answer rename prompts explicitly if this scaffold replaces an existing differently-named table.
Apply the migration: Run npx drizzle-kit migrate against the dev database. The client templates also call migrate(...) on boot, but applying once in the dev loop confirms the SQL works before the next process restart.
Flow C — Add a CRUD module for an existing table (no schema change)
Same as Flow B steps 1-2, but skip the table.ts.template render and just emit the repository (and optionally validators) modules.
PK Variants
pk value |
pk_definition |
pk_ts_type |
Extra imports |
serial-int (default) |
id: integer().primaryKey({ autoIncrement: true }), |
number |
— |
uuid |
id: text().primaryKey().$defaultFn(() => crypto.randomUUID()), |
string |
— (uses Web Crypto) |
cuid2 |
id: text().primaryKey().$defaultFn(() => createId()), |
string |
import { createId } from '@paralleldrive/cuid2'; |
text |
id: text().primaryKey(), |
string |
— (caller supplies the ID) |
Placeholder Reference
Every {{placeholder}} the templates use, with its derivation rule. Items marked simple sub are find-and-replace; items marked block require the agent to expand per the rules in the next section.
| Placeholder |
Type |
Source / derivation |
{{driver}} |
simple sub |
config.json:driver |
{{db_url_env}} |
simple sub |
config.json:db_url_env |
{{schema_dir}} |
simple sub |
config.json:schema_dir |
{{repository_dir}} |
simple sub |
config.json:repository_dir |
{{validators_dir}} |
simple sub |
config.json:validators_dir |
{{client_path}} |
simple sub |
config.json:client_path |
{{migrations_dir}} |
simple sub |
config.json:migrations_dir |
{{schema_index_import}} |
simple sub |
derived: client_path → relative path to {{schema_dir}}/index.ts (typically './schema') |
{{client_import}} |
simple sub |
derived: from a repository file, relative path back to client_path (typically '../client') |
{{schema_import}} |
simple sub |
derived: from a repository or validators file, relative path to the matching table file (typically '../schema/{{name}}') |
{{name}} |
simple sub |
per-table param — kebab-case singular (user) |
{{name_camel}} |
simple sub |
derived from name (user, orderItem) |
{{name_pascal}} |
simple sub |
derived from name (User, OrderItem) |
{{table_name}} |
simple sub |
per-table param, default = snake_case plural of name (users, order_items) |
{{pk}} |
metadata |
per-table param — serial-int | uuid | cuid2 | text (drives the rows below) |
{{pk_definition}} |
block |
the actual PK column line — see "PK Variants" table |
{{pk_field}} |
simple sub |
always id for the four PK variants this skill ships |
{{pk_ts_type}} |
simple sub |
number for serial-int, string for uuid / cuid2 / text |
{{pk_extra_imports}} |
block |
empty for serial-int / uuid / text; import { createId } from '@paralleldrive/cuid2'; for cuid2 |
{{relation_imports}} |
block |
for each table in relations[], emit import { {{relatedCamel}} } from './{{related-kebab}}'; |
{{domain_columns}} |
block |
the agent (or user) replaces this with the actual non-PK, non-timestamp columns for the entity. Leave as a TODO comment if the user hasn't provided them yet. |
{{timestamps_block}} |
block |
see expansion below — emit when timestamps=true, remove the line entirely when false |
{{soft_delete_block}} |
block |
see expansion below — emit in the columns block when soft_delete=true |
{{indexes}} |
block |
one index(...) line per foreign key column (and any composite (authorId, publishedAt)-style indexes the user wants) |
{{soft_delete_index}} |
block |
the partial index on deletedAt (see expansion below); emit only when soft_delete=true |
{{relations_body}} |
block |
one one(...) / many(...) line per related table; see expansion below |
{{insert_refinements}} |
block |
drizzle-zod refinement callbacks for the INSERT shape; leave the example comment if none provided |
{{update_refinements}} |
block |
same for the partial UPDATE shape |
{{exports}} |
block |
inside schema-index.ts.template: one export * from './{{name}}'; line per table; append on each new-table run |
If a template emits a {{placeholder}} not in this table, that's a bug — file it under gotchas.md.
Block Expansions
The table.ts.template uses placeholder blocks for variable-shaped sections — the agent must expand them per parameters, not just text-substitute.
{{timestamps_block}} (when timestamps=true)
createdAt: integer({ mode: 'timestamp_ms' })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer({ mode: 'timestamp_ms' })
.notNull()
.$defaultFn(() => new Date())
.$onUpdateFn(() => new Date()),
When timestamps=false, remove the line entirely (don't leave the comment marker).
{{soft_delete_block}} (when soft_delete=true)
In the columns block:
deletedAt: integer({ mode: 'timestamp_ms' }),
In the indexes block:
index('{{table_name}}_active_idx').on(table.deletedAt).where(sql`deleted_at IS NULL`),
(Adjust the sql import accordingly.)
{{relations_body}} (when relations is non-empty)
For each related table in relations[], the agent decides whether it's a one or many based on whether the FK lives on the current table (then it's one) or on the related table (then it's many):
// FK on current table — `one`:
parent: one(parents, { fields: [users.parentId], references: [parents.id] }),
// FK on related table — `many`:
posts: many(posts),
If the user can't easily tell, default to one example of each and leave a comment.
{{indexes}} block
For each FK column, emit:
index('{{table_name}}_{{column}}_idx').on(table.{{columnCamel}}),
Cloudflare D1 note
D1 has a different lifecycle: the client is constructed per request from env.DB (the binding), not as a module-level singleton. This skill doesn't ship a D1 client template — follow the official D1 + Drizzle guide for the wiring. The table.ts.template, repository.ts.template, and validators.ts.template are still usable with D1 — only the client module differs.
Reference Files
| File |
Description |
| references/conventions.md |
The 11 conventions enforced, with WHY and rule cross-references |
| gotchas.md |
Edge cases discovered over time |
| metadata.json |
Version + driver references |
| config.json |
Project-level parameter store |
Related Skills
drizzle-sqlite — The library-reference rules these templates encode. The conventions doc cites specific rule filenames from it. Read it when you need to make an informed exception, debug a generated file, or scaffold something outside the templates' scope (custom migrations, complex queries, performance work).
better-auth-scaffold — Scaffolds Better Auth on top of a Drizzle DB; can be run after this skill provides the client.
1---
2name: drizzle-sqlite-scaffold
3description: Scaffolds Drizzle ORM + SQLite boilerplate — a new `drizzle.config.ts`, a singleton client with the right pragmas, per-table schema files with explicit primary keys/indexed foreign keys/relations()/inferred types, per-table repository modules with `.returning()` + `inArray()` + `.onConflictDoUpdate()`, or drizzle-zod validators. Produces convention-enforced templates for three drivers (better-sqlite3, libsql/Turso, bun:sqlite). Trigger even when the user doesn't say "scaffold" — phrases like "add a table for X", "set up Drizzle in this project", "wire up SQLite", "create a CRUD module for X", or "bootstrap the DB layer" should pull this in. Pairs with the `drizzle-sqlite` skill, which covers the 45 rules these templates encode — read it when an exception is required.
4---
5
6# Drizzle SQLite Scaffold
7
8Parameterized templates for bootstrapping Drizzle + SQLite in a fresh project, or adding a new table/repository to an existing one. Every output bakes in the conventions documented in [`references/conventions.md`](references/conventions.md) — explicit primary keys, indexed foreign keys, `relations()` declarations, `$inferSelect`/`$inferInsert` exports, timestamp_ms dates, boolean-mode bools, WAL + `foreign_keys=ON` + `busy_timeout` pragmas, singleton client with HMR guard, and CRUD helpers using `.returning()` + `inArray()` + `.onConflictDoUpdate()`.
9
10## When to Apply
11
12Reach for these templates when:
13- Starting a new project that will use Drizzle with SQLite (any driver)
14- Adding a new table to an existing Drizzle project — the table file should match the existing patterns
15- Adding a CRUD repository module for an existing table
16- Refactoring a hand-rolled Drizzle setup that's missing pragmas, has no `relations()`, or has hand-written `User` types that drift from the schema
17- Migrating from another ORM (Prisma, Kysely) to Drizzle and wanting consistent shapes from the start
18
19## Setup
20
21### Required parameters (asked on first use, saved to `config.json`)
22
23| Parameter | Required | Default | Values |
24|-----------|----------|---------|--------|
25| `driver` | yes | — | `better-sqlite3` \| `libsql` \| `bun-sqlite` (D1 has a different lifecycle — see "Cloudflare D1" below) |
26| `db_url_env` | no | `DATABASE_URL` | env var name |
27| `schema_dir` | no | `./src/db/schema` | per-table schema files |
28| `repository_dir` | no | `./src/db/repository` | per-table CRUD modules |
29| `validators_dir` | no | `./src/db/validators` | drizzle-zod schemas (when `with_zod=true`) |
30| `client_path` | no | `./src/db/client.ts` | singleton client module |
31| `migrations_dir` | no | `./drizzle` | drizzle-kit output |
32
33If `config.json` already exists with values, this skill uses them; otherwise it asks via `AskUserQuestion`.
34
35### Per-table parameters (asked each time a new table is scaffolded)
36
37| Parameter | Required | Default | Description |
38|-----------|----------|---------|-------------|
39| `name` | yes | — | Kebab-case singular: `user`, `order-item`. Used for filenames and TS identifiers (`name_camel`, `name_pascal` derived). |
40| `table_name` | no | snake_case plural of `name` | SQL table name: `users`, `order_items` |
41| `pk` | no | `serial-int` | `serial-int` \| `uuid` \| `cuid2` \| `text` |
42| `timestamps` | no | `true` | adds `createdAt`/`updatedAt` columns |
43| `soft_delete` | no | `false` | adds nullable `deletedAt` + partial index |
44| `relations` | no | `[]` | list of related table names — expands `relations()` body |
45| `with_zod` | no | `true` | emits a drizzle-zod validators file |
46
47## Available Templates
48
49### Project-init templates (emit once)
50
51| Template | Output File | When |
52|----------|-------------|------|
53| [`drizzle.config.local.ts.template`](assets/templates/drizzle.config.local.ts.template) | `drizzle.config.ts` | `driver` is `better-sqlite3`, `bun-sqlite`, or `libsql` with a `file:` URL |
54| [`drizzle.config.turso.ts.template`](assets/templates/drizzle.config.turso.ts.template) | `drizzle.config.ts` | `driver` is `libsql` against Turso (remote `libsql:` URL) |
55| [`client.better-sqlite3.ts.template`](assets/templates/client.better-sqlite3.ts.template) | `{{client_path}}` | `driver` is `better-sqlite3` |
56| [`client.libsql.ts.template`](assets/templates/client.libsql.ts.template) | `{{client_path}}` | `driver` is `libsql` |
57| [`client.bun-sqlite.ts.template`](assets/templates/client.bun-sqlite.ts.template) | `{{client_path}}` | `driver` is `bun-sqlite` |
58| [`schema-index.ts.template`](assets/templates/schema-index.ts.template) | `{{schema_dir}}/index.ts` | Always (initially empty; append exports as tables are added) |
59| [`gitignore.template`](assets/templates/gitignore.template) | `.gitignore` (append) | Always |
60
61### Per-table templates (emit once per table)
62
63| Template | Output File | When |
64|----------|-------------|------|
65| [`table.ts.template`](assets/templates/table.ts.template) | `{{schema_dir}}/{{name}}.ts` | Per table |
66| [`repository.ts.template`](assets/templates/repository.ts.template) | `{{repository_dir}}/{{name}}.ts` | Per table |
67| [`validators.ts.template`](assets/templates/validators.ts.template) | `{{validators_dir}}/{{name}}.ts` | Per table when `with_zod=true` |
68
69## How to Use
70
71### Flow A — Initialize a new project (run once)
72
731. **Resolve project parameters.** Read `config.json`. For any required field that's empty, ask the user via `AskUserQuestion` (driver is the only strictly required one; the rest have sensible defaults).
74
752. **Install runtime + tooling first** so the rendered files type-check immediately:
76 ```bash
77 # Pick the driver-specific runtime package:
78 npm install drizzle-orm @libsql/client # for libsql
79 npm install drizzle-orm better-sqlite3 # for better-sqlite3
80 npm install drizzle-orm # bun:sqlite is built into Bun
81
82 # Dev tools (all drivers):
83 npm install -D drizzle-kit
84 npm install -D @types/better-sqlite3 # better-sqlite3 only
85 npm install -D drizzle-zod zod # if with_zod=true
86 ```
87
883. **Pick the config and client variants** for the resolved `driver`:
89 - `better-sqlite3` → `drizzle.config.local.ts.template` + `client.better-sqlite3.ts.template`
90 - `libsql` with `file:` URL → `drizzle.config.local.ts.template` + `client.libsql.ts.template`
91 - `libsql` with remote Turso URL → `drizzle.config.turso.ts.template` + `client.libsql.ts.template`
92 - `bun-sqlite` → `drizzle.config.local.ts.template` + `client.bun-sqlite.ts.template`
93
944. **Render and write the project-init files:**
95 - `drizzle.config.ts`
96 - `{{client_path}}` (typically `src/db/client.ts`)
97 - `{{schema_dir}}/index.ts` (empty barrel)
98 - Append the `gitignore.template` block to the project's `.gitignore`
99
1005. **For libsql:** the client template uses top-level `await migrate(...)`. Verify `tsconfig.json` has `"module": "ESNext"` (or `"NodeNext"`) and `"target": "ES2022"+` for top-level await support. If the runtime is CommonJS, replace the top-level await with an exported `async function init()` the app calls during startup.
101
1026. **Save resolved values to `config.json`** so subsequent table runs don't re-prompt.
103
104### Flow B — Add a new table (run per table)
105
1061. **Resolve per-table parameters.** Ask the user for `name`, then offer defaults for `table_name` (snake_case plural), `pk`, `timestamps`, `soft_delete`, `relations`, `with_zod`. Use `AskUserQuestion` for any non-default the user wants.
107
1082. **Compute derived identifiers:**
109 - `name_camel` — camelCase of `name` (`user`, `orderItem`)
110 - `name_pascal` — PascalCase of `name` (`User`, `OrderItem`)
111 - `pk_field` — the PK column name (`id` for all 4 `pk` modes)
112 - `pk_ts_type` — TS type for the PK (`number` for `serial-int`, `string` for `uuid`/`cuid2`/`text`)
113 - `pk_definition` — the actual line, e.g., `id: integer().primaryKey({ autoIncrement: true }),` (see PK Variants table below)
114
1153. **Render the table template:** Read `table.ts.template`, substitute `{{name}}`, `{{name_camel}}`, `{{name_pascal}}`, `{{table_name}}`, `{{pk_definition}}`, `{{pk_extra_imports}}`, etc. Expand `{{timestamps_block}}` and `{{soft_delete_block}}` per the parameters (see "Block Expansions" below). Write to `{{schema_dir}}/{{name}}.ts`.
116
1174. **Render the repository template** with the same parameters. Write to `{{repository_dir}}/{{name}}.ts`.
118
1195. **If `with_zod=true`**, render the validators template. Write to `{{validators_dir}}/{{name}}.ts`.
120
1216. **Append to the schema barrel:** Add `export * from './{{name}}';` to `{{schema_dir}}/index.ts`.
122
1237. **Generate the migration:** Tell the user to run `npx drizzle-kit generate` to produce the SQL file. Remind them to answer rename prompts explicitly if this scaffold replaces an existing differently-named table.
124
1258. **Apply the migration:** Run `npx drizzle-kit migrate` against the dev database. The client templates also call `migrate(...)` on boot, but applying once in the dev loop confirms the SQL works before the next process restart.
126
127### Flow C — Add a CRUD module for an existing table (no schema change)
128
129Same as Flow B steps 1-2, but skip the table.ts.template render and just emit the repository (and optionally validators) modules.
130
131## PK Variants
132
133| `pk` value | `pk_definition` | `pk_ts_type` | Extra imports |
134|---|---|---|---|
135| `serial-int` (default) | `id: integer().primaryKey({ autoIncrement: true }),` | `number` | — |
136| `uuid` | `id: text().primaryKey().$defaultFn(() => crypto.randomUUID()),` | `string` | — (uses Web Crypto) |
137| `cuid2` | `id: text().primaryKey().$defaultFn(() => createId()),` | `string` | `import { createId } from '@paralleldrive/cuid2';` |
138| `text` | `id: text().primaryKey(),` | `string` | — (caller supplies the ID) |
139
140## Placeholder Reference
141
142Every `{{placeholder}}` the templates use, with its derivation rule. Items marked **simple sub** are find-and-replace; items marked **block** require the agent to expand per the rules in the next section.
143
144| Placeholder | Type | Source / derivation |
145|---|---|---|
146| `{{driver}}` | simple sub | `config.json:driver` |
147| `{{db_url_env}}` | simple sub | `config.json:db_url_env` |
148| `{{schema_dir}}` | simple sub | `config.json:schema_dir` |
149| `{{repository_dir}}` | simple sub | `config.json:repository_dir` |
150| `{{validators_dir}}` | simple sub | `config.json:validators_dir` |
151| `{{client_path}}` | simple sub | `config.json:client_path` |
152| `{{migrations_dir}}` | simple sub | `config.json:migrations_dir` |
153| `{{schema_index_import}}` | simple sub | derived: `client_path` → relative path to `{{schema_dir}}/index.ts` (typically `'./schema'`) |
154| `{{client_import}}` | simple sub | derived: from a repository file, relative path back to `client_path` (typically `'../client'`) |
155| `{{schema_import}}` | simple sub | derived: from a repository or validators file, relative path to the matching table file (typically `'../schema/{{name}}'`) |
156| `{{name}}` | simple sub | per-table param — kebab-case singular (`user`) |
157| `{{name_camel}}` | simple sub | derived from `name` (`user`, `orderItem`) |
158| `{{name_pascal}}` | simple sub | derived from `name` (`User`, `OrderItem`) |
159| `{{table_name}}` | simple sub | per-table param, default = snake_case plural of `name` (`users`, `order_items`) |
160| `{{pk}}` | metadata | per-table param — `serial-int` \| `uuid` \| `cuid2` \| `text` (drives the rows below) |
161| `{{pk_definition}}` | block | the actual PK column line — see "PK Variants" table |
162| `{{pk_field}}` | simple sub | always `id` for the four PK variants this skill ships |
163| `{{pk_ts_type}}` | simple sub | `number` for `serial-int`, `string` for `uuid` / `cuid2` / `text` |
164| `{{pk_extra_imports}}` | block | empty for `serial-int` / `uuid` / `text`; `import { createId } from '@paralleldrive/cuid2';` for `cuid2` |
165| `{{relation_imports}}` | block | for each table in `relations[]`, emit `import { {{relatedCamel}} } from './{{related-kebab}}';` |
166| `{{domain_columns}}` | block | the agent (or user) replaces this with the actual non-PK, non-timestamp columns for the entity. Leave as a TODO comment if the user hasn't provided them yet. |
167| `{{timestamps_block}}` | block | see expansion below — emit when `timestamps=true`, remove the line entirely when `false` |
168| `{{soft_delete_block}}` | block | see expansion below — emit in the columns block when `soft_delete=true` |
169| `{{indexes}}` | block | one `index(...)` line per foreign key column (and any composite `(authorId, publishedAt)`-style indexes the user wants) |
170| `{{soft_delete_index}}` | block | the partial index on `deletedAt` (see expansion below); emit only when `soft_delete=true` |
171| `{{relations_body}}` | block | one `one(...)` / `many(...)` line per related table; see expansion below |
172| `{{insert_refinements}}` | block | drizzle-zod refinement callbacks for the INSERT shape; leave the example comment if none provided |
173| `{{update_refinements}}` | block | same for the partial UPDATE shape |
174| `{{exports}}` | block | inside `schema-index.ts.template`: one `export * from './{{name}}';` line per table; append on each new-table run |
175
176If a template emits a `{{placeholder}}` not in this table, that's a bug — file it under `gotchas.md`.
177
178## Block Expansions
179
180The `table.ts.template` uses placeholder *blocks* for variable-shaped sections — the agent must expand them per parameters, not just text-substitute.
181
182### `{{timestamps_block}}` (when `timestamps=true`)
183
184```typescript
185createdAt: integer({ mode: 'timestamp_ms' })
186 .notNull()
187 .$defaultFn(() => new Date()),
188updatedAt: integer({ mode: 'timestamp_ms' })
189 .notNull()
190 .$defaultFn(() => new Date())
191 .$onUpdateFn(() => new Date()),
192```
193
194When `timestamps=false`, remove the line entirely (don't leave the comment marker).
195
196### `{{soft_delete_block}}` (when `soft_delete=true`)
197
198In the columns block:
199
200```typescript
201deletedAt: integer({ mode: 'timestamp_ms' }),
202```
203
204In the indexes block:
205
206```typescript
207index('{{table_name}}_active_idx').on(table.deletedAt).where(sql`deleted_at IS NULL`),
208```
209
210(Adjust the `sql` import accordingly.)
211
212### `{{relations_body}}` (when `relations` is non-empty)
213
214For each related table in `relations[]`, the agent decides whether it's a `one` or `many` based on whether the FK lives on the current table (then it's `one`) or on the related table (then it's `many`):
215
216```typescript
217// FK on current table — `one`:
218parent: one(parents, { fields: [users.parentId], references: [parents.id] }),
219
220// FK on related table — `many`:
221posts: many(posts),
222```
223
224If the user can't easily tell, default to one example of each and leave a comment.
225
226### `{{indexes}}` block
227
228For each FK column, emit:
229
230```typescript
231index('{{table_name}}_{{column}}_idx').on(table.{{columnCamel}}),
232```
233
234## Cloudflare D1 note
235
236D1 has a different lifecycle: the client is constructed per request from `env.DB` (the binding), not as a module-level singleton. This skill doesn't ship a D1 client template — follow the [official D1 + Drizzle guide](https://orm.drizzle.team/docs/get-started/d1-new) for the wiring. The `table.ts.template`, `repository.ts.template`, and `validators.ts.template` are still usable with D1 — only the client module differs.
237
238## Reference Files
239
240| File | Description |
241|------|-------------|
242| [references/conventions.md](references/conventions.md) | The 11 conventions enforced, with WHY and rule cross-references |
243| [gotchas.md](gotchas.md) | Edge cases discovered over time |
244| [metadata.json](metadata.json) | Version + driver references |
245| [config.json](config.json) | Project-level parameter store |
246
247## Related Skills
248
249- **[`drizzle-sqlite`](../drizzle-sqlite/SKILL.md)** — The library-reference rules these templates encode. The conventions doc cites specific rule filenames from it. Read it when you need to make an informed exception, debug a generated file, or scaffold something outside the templates' scope (custom migrations, complex queries, performance work).
250- **[`better-auth-scaffold`](../better-auth-scaffold/SKILL.md)** — Scaffolds Better Auth on top of a Drizzle DB; can be run after this skill provides the client.