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-scaffold3description: 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# Drizzle SQLite Scaffold
6
7Parameterized 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()`.
8
9## When to Apply
10
11Reach for these templates when:
12- Starting a new project that will use Drizzle with SQLite (any driver)
13- Adding a new table to an existing Drizzle project — the table file should match the existing patterns
14- Adding a CRUD repository module for an existing table
15- Refactoring a hand-rolled Drizzle setup that's missing pragmas, has no `relations()`, or has hand-written `User` types that drift from the schema
16- Migrating from another ORM (Prisma, Kysely) to Drizzle and wanting consistent shapes from the start
17
18## Setup
19
20### Required parameters (asked on first use, saved to `config.json`)
21
22| Parameter | Required | Default | Values |
23|-----------|----------|---------|--------|
24| `driver` | yes | — | `better-sqlite3` \| `libsql` \| `bun-sqlite` (D1 has a different lifecycle — see "Cloudflare D1" below) |
25| `db_url_env` | no | `DATABASE_URL` | env var name |
26| `schema_dir` | no | `./src/db/schema` | per-table schema files |
27| `repository_dir` | no | `./src/db/repository` | per-table CRUD modules |
28| `validators_dir` | no | `./src/db/validators` | drizzle-zod schemas (when `with_zod=true`) |
29| `client_path` | no | `./src/db/client.ts` | singleton client module |
30| `migrations_dir` | no | `./drizzle` | drizzle-kit output |
31
32If `config.json` already exists with values, this skill uses them; otherwise it asks via `AskUserQuestion`.
33
34### Per-table parameters (asked each time a new table is scaffolded)
35
36| Parameter | Required | Default | Description |
37|-----------|----------|---------|-------------|
38| `name` | yes | — | Kebab-case singular: `user`, `order-item`. Used for filenames and TS identifiers (`name_camel`, `name_pascal` derived). |
39| `table_name` | no | snake_case plural of `name` | SQL table name: `users`, `order_items` |
40| `pk` | no | `serial-int` | `serial-int` \| `uuid` \| `cuid2` \| `text` |
41| `timestamps` | no | `true` | adds `createdAt`/`updatedAt` columns |
42| `soft_delete` | no | `false` | adds nullable `deletedAt` + partial index |
43| `relations` | no | `[]` | list of related table names — expands `relations()` body |
44| `with_zod` | no | `true` | emits a drizzle-zod validators file |
45
46## Available Templates
47
48### Project-init templates (emit once)
49
50| Template | Output File | When |
51|----------|-------------|------|
52| [`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 |
53| [`drizzle.config.turso.ts.template`](assets/templates/drizzle.config.turso.ts.template) | `drizzle.config.ts` | `driver` is `libsql` against Turso (remote `libsql:` URL) |
54| [`client.better-sqlite3.ts.template`](assets/templates/client.better-sqlite3.ts.template) | `{{client_path}}` | `driver` is `better-sqlite3` |
55| [`client.libsql.ts.template`](assets/templates/client.libsql.ts.template) | `{{client_path}}` | `driver` is `libsql` |
56| [`client.bun-sqlite.ts.template`](assets/templates/client.bun-sqlite.ts.template) | `{{client_path}}` | `driver` is `bun-sqlite` |
57| [`schema-index.ts.template`](assets/templates/schema-index.ts.template) | `{{schema_dir}}/index.ts` | Always (initially empty; append exports as tables are added) |
58| [`gitignore.template`](assets/templates/gitignore.template) | `.gitignore` (append) | Always |
59
60### Per-table templates (emit once per table)
61
62| Template | Output File | When |
63|----------|-------------|------|
64| [`table.ts.template`](assets/templates/table.ts.template) | `{{schema_dir}}/{{name}}.ts` | Per table |
65| [`repository.ts.template`](assets/templates/repository.ts.template) | `{{repository_dir}}/{{name}}.ts` | Per table |
66| [`validators.ts.template`](assets/templates/validators.ts.template) | `{{validators_dir}}/{{name}}.ts` | Per table when `with_zod=true` |
67
68## How to Use
69
70### Flow A — Initialize a new project (run once)
71
721. **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).
73
742. **Install runtime + tooling first** so the rendered files type-check immediately:
75 ```bash
76 # Pick the driver-specific runtime package:
77 npm install drizzle-orm @libsql/client # for libsql
78 npm install drizzle-orm better-sqlite3 # for better-sqlite3
79 npm install drizzle-orm # bun:sqlite is built into Bun
80
81 # Dev tools (all drivers):
82 npm install -D drizzle-kit
83 npm install -D @types/better-sqlite3 # better-sqlite3 only
84 npm install -D drizzle-zod zod # if with_zod=true
85 ```
86
873. **Pick the config and client variants** for the resolved `driver`:
88 - `better-sqlite3` → `drizzle.config.local.ts.template` + `client.better-sqlite3.ts.template`
89 - `libsql` with `file:` URL → `drizzle.config.local.ts.template` + `client.libsql.ts.template`
90 - `libsql` with remote Turso URL → `drizzle.config.turso.ts.template` + `client.libsql.ts.template`
91 - `bun-sqlite` → `drizzle.config.local.ts.template` + `client.bun-sqlite.ts.template`
92
934. **Render and write the project-init files:**
94 - `drizzle.config.ts`
95 - `{{client_path}}` (typically `src/db/client.ts`)
96 - `{{schema_dir}}/index.ts` (empty barrel)
97 - Append the `gitignore.template` block to the project's `.gitignore`
98
995. **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.
100
1016. **Save resolved values to `config.json`** so subsequent table runs don't re-prompt.
102
103### Flow B — Add a new table (run per table)
104
1051. **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.
106
1072. **Compute derived identifiers:**
108 - `name_camel` — camelCase of `name` (`user`, `orderItem`)
109 - `name_pascal` — PascalCase of `name` (`User`, `OrderItem`)
110 - `pk_field` — the PK column name (`id` for all 4 `pk` modes)
111 - `pk_ts_type` — TS type for the PK (`number` for `serial-int`, `string` for `uuid`/`cuid2`/`text`)
112 - `pk_definition` — the actual line, e.g., `id: integer().primaryKey({ autoIncrement: true }),` (see PK Variants table below)
113
1143. **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`.
115
1164. **Render the repository template** with the same parameters. Write to `{{repository_dir}}/{{name}}.ts`.
117
1185. **If `with_zod=true`**, render the validators template. Write to `{{validators_dir}}/{{name}}.ts`.
119
1206. **Append to the schema barrel:** Add `export * from './{{name}}';` to `{{schema_dir}}/index.ts`.
121
1227. **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.
123
1248. **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.
125
126### Flow C — Add a CRUD module for an existing table (no schema change)
127
128Same as Flow B steps 1-2, but skip the table.ts.template render and just emit the repository (and optionally validators) modules.
129
130## PK Variants
131
132| `pk` value | `pk_definition` | `pk_ts_type` | Extra imports |
133|---|---|---|---|
134| `serial-int` (default) | `id: integer().primaryKey({ autoIncrement: true }),` | `number` | — |
135| `uuid` | `id: text().primaryKey().$defaultFn(() => crypto.randomUUID()),` | `string` | — (uses Web Crypto) |
136| `cuid2` | `id: text().primaryKey().$defaultFn(() => createId()),` | `string` | `import { createId } from '@paralleldrive/cuid2';` |
137| `text` | `id: text().primaryKey(),` | `string` | — (caller supplies the ID) |
138
139## Placeholder Reference
140
141Every `{{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.
142
143| Placeholder | Type | Source / derivation |
144|---|---|---|
145| `{{driver}}` | simple sub | `config.json:driver` |
146| `{{db_url_env}}` | simple sub | `config.json:db_url_env` |
147| `{{schema_dir}}` | simple sub | `config.json:schema_dir` |
148| `{{repository_dir}}` | simple sub | `config.json:repository_dir` |
149| `{{validators_dir}}` | simple sub | `config.json:validators_dir` |
150| `{{client_path}}` | simple sub | `config.json:client_path` |
151| `{{migrations_dir}}` | simple sub | `config.json:migrations_dir` |
152| `{{schema_index_import}}` | simple sub | derived: `client_path` → relative path to `{{schema_dir}}/index.ts` (typically `'./schema'`) |
153| `{{client_import}}` | simple sub | derived: from a repository file, relative path back to `client_path` (typically `'../client'`) |
154| `{{schema_import}}` | simple sub | derived: from a repository or validators file, relative path to the matching table file (typically `'../schema/{{name}}'`) |
155| `{{name}}` | simple sub | per-table param — kebab-case singular (`user`) |
156| `{{name_camel}}` | simple sub | derived from `name` (`user`, `orderItem`) |
157| `{{name_pascal}}` | simple sub | derived from `name` (`User`, `OrderItem`) |
158| `{{table_name}}` | simple sub | per-table param, default = snake_case plural of `name` (`users`, `order_items`) |
159| `{{pk}}` | metadata | per-table param — `serial-int` \| `uuid` \| `cuid2` \| `text` (drives the rows below) |
160| `{{pk_definition}}` | block | the actual PK column line — see "PK Variants" table |
161| `{{pk_field}}` | simple sub | always `id` for the four PK variants this skill ships |
162| `{{pk_ts_type}}` | simple sub | `number` for `serial-int`, `string` for `uuid` / `cuid2` / `text` |
163| `{{pk_extra_imports}}` | block | empty for `serial-int` / `uuid` / `text`; `import { createId } from '@paralleldrive/cuid2';` for `cuid2` |
164| `{{relation_imports}}` | block | for each table in `relations[]`, emit `import { {{relatedCamel}} } from './{{related-kebab}}';` |
165| `{{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. |
166| `{{timestamps_block}}` | block | see expansion below — emit when `timestamps=true`, remove the line entirely when `false` |
167| `{{soft_delete_block}}` | block | see expansion below — emit in the columns block when `soft_delete=true` |
168| `{{indexes}}` | block | one `index(...)` line per foreign key column (and any composite `(authorId, publishedAt)`-style indexes the user wants) |
169| `{{soft_delete_index}}` | block | the partial index on `deletedAt` (see expansion below); emit only when `soft_delete=true` |
170| `{{relations_body}}` | block | one `one(...)` / `many(...)` line per related table; see expansion below |
171| `{{insert_refinements}}` | block | drizzle-zod refinement callbacks for the INSERT shape; leave the example comment if none provided |
172| `{{update_refinements}}` | block | same for the partial UPDATE shape |
173| `{{exports}}` | block | inside `schema-index.ts.template`: one `export * from './{{name}}';` line per table; append on each new-table run |
174
175If a template emits a `{{placeholder}}` not in this table, that's a bug — file it under `gotchas.md`.
176
177## Block Expansions
178
179The `table.ts.template` uses placeholder *blocks* for variable-shaped sections — the agent must expand them per parameters, not just text-substitute.
180
181### `{{timestamps_block}}` (when `timestamps=true`)
182
183```typescript
184createdAt: integer({ mode: 'timestamp_ms' })
185 .notNull()
186 .$defaultFn(() => new Date()),
187updatedAt: integer({ mode: 'timestamp_ms' })
188 .notNull()
189 .$defaultFn(() => new Date())
190 .$onUpdateFn(() => new Date()),
191```
192
193When `timestamps=false`, remove the line entirely (don't leave the comment marker).
194
195### `{{soft_delete_block}}` (when `soft_delete=true`)
196
197In the columns block:
198
199```typescript
200deletedAt: integer({ mode: 'timestamp_ms' }),
201```
202
203In the indexes block:
204
205```typescript
206index('{{table_name}}_active_idx').on(table.deletedAt).where(sql`deleted_at IS NULL`),
207```
208
209(Adjust the `sql` import accordingly.)
210
211### `{{relations_body}}` (when `relations` is non-empty)
212
213For 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`):
214
215```typescript
216// FK on current table — `one`:
217parent: one(parents, { fields: [users.parentId], references: [parents.id] }),
218
219// FK on related table — `many`:
220posts: many(posts),
221```
222
223If the user can't easily tell, default to one example of each and leave a comment.
224
225### `{{indexes}}` block
226
227For each FK column, emit:
228
229```typescript
230index('{{table_name}}_{{column}}_idx').on(table.{{columnCamel}}),
231```
232
233## Cloudflare D1 note
234
235D1 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.
236
237## Reference Files
238
239| File | Description |
240|------|-------------|
241| [references/conventions.md](references/conventions.md) | The 11 conventions enforced, with WHY and rule cross-references |
242| [gotchas.md](gotchas.md) | Edge cases discovered over time |
243| [metadata.json](metadata.json) | Version + driver references |
244| [config.json](config.json) | Project-level parameter store |
245
246## Related Skills
247
248- **[`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).
249- **[`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.