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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
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
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:
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.
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.
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.
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.
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.