Scaffolds Drizzle ORM + SQLite boilerplate: config, singleton client, per-table schema with relations, CRUD repositories, and drizzle-zod validators for three SQLite drivers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pproenca-dot-skills-1:drizzle-sqlite-scaffoldThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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`](references/conventions.md) — explicit primary keys, indexed foreign keys, `relations()` declarations, `$inferSelect`/`$inferInsert` exports, timestamp_ms dates, boolean-mode bools, WA...
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().
Reach for these templates when:
relations(), or has hand-written User types that drift from the schemaconfig.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.
| 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 |
| 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 |
| 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 |
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.templatelibsql with file: URL → drizzle.config.local.ts.template + client.libsql.ts.templatelibsql with remote Turso URL → drizzle.config.turso.ts.template + client.libsql.ts.templatebun-sqlite → drizzle.config.local.ts.template + client.bun-sqlite.ts.templateRender and write the project-init files:
drizzle.config.ts{{client_path}} (typically src/db/client.ts){{schema_dir}}/index.ts (empty barrel)gitignore.template block to the project's .gitignoreFor 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.
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.
Same as Flow B steps 1-2, but skip the table.ts.template render and just emit the repository (and optionally validators) modules.
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) |
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.
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}} blockFor each FK column, emit:
index('{{table_name}}_{{column}}_idx').on(table.{{columnCamel}}),
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.
| 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 |
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.npx claudepluginhub joshuarweaver/cascade-code-general-misc-1 --plugin pproenca-dot-skills-1Covers Drizzle ORM schema, migrations, queries, relations, transactions, and type inference for SQLite backends (better-sqlite3, Turso, D1, expo-sqlite).
Provides type-safe SQL with Drizzle ORM for defining schemas, writing queries, setting relations, and running migrations across PostgreSQL, MySQL, SQLite, Cloudflare D1, and Durable Objects.
Designs type-safe database schemas and queries using Drizzle ORM, including migrations, relational queries, and serverless integrations.