sqlite-dev
Skill7 starsChanged 3 months agoMITView on GitHub
Opinionated SQLite conventions for local TypeScript + Bun web development with Drizzle ORM, where Postgres is the likely production target. Keeps the postgres-dba naming and modeling rules (snake_case, plural tables, `id` surrogate keys, NOT NULL FKs with explicit ON DELETE, compound junction keys, JSON document store) so the schema ports to Postgres with a dialect swap, not a rewrite. Covers the `bun:sqlite` + `drizzle-orm/bun-sqlite` driver, mandatory connection pragmas (WAL, foreign_keys, busy_timeout), STRICT tables, the SQLite analogues of enums / booleans / timestamps / JSONB / generated columns, drizzle-kit migrations, and a full section on running SQLite *in production* (single-writer discipline, Litestream/LiteFS durability, tuning) plus the Postgres cutover checklist. Use when starting or reviewing a Bun+Drizzle app on SQLite, writing the schema or migrations, choosing a column type, deciding whether SQLite can be the production database, or porting a SQLite schema to Postgres. Ships copy-ready `schema.ts`, `db.ts`, production `db.ts`, and `drizzle.config.ts`.
In this repository12 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- agent-teamsSkill
- bdd-specsSkill
- design-principlesSkill
- githubSkill
- gof-patternsSkill
- onboardSkill
- postgres-dbaSkill
- security-webSkill
- solid-principlesSkill
- typescript-best-practicesSkill
- user-storiesSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 27
NOT NULL FKs, JSON document store) exists so the schema ports to Postgres with a dialect swap, not a rewrite.
- Very specific: line 37
Use the same names, the same modeling discipline, and the same integrity rules as postgres-dba — SQLite is just a smaller engine running …
- Very specific: line 73
Pragmas are mandatory and per-connection: Every connection sets, in order: journal_mode = WAL, foreign_keys = ON, busy_timeout = 5000, …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SQLite for Local Dev — Postgres-Portable, Bun + Drizzle
🎯 Why: Design for Change
The goal of writing software is to be able to change it safely — including changing the database. Every rule here (snake_case, plural tables, NOT NULL FKs, JSON document store) exists so the schema ports to Postgres with a dialect swap, not a rewrite. Portability is change-safety: when SQLite stops fitting, you move with a Drizzle config edit instead of a month of forensics.
SQLite here is the development database for a TypeScript/Bun app whose
production system of record will almost certainly be Postgres. That single
fact drives every rule: write the SQLite schema so that switching to
Postgres is a Drizzle dialect change and a data copy, not a redesign.
Use the same names, the same modeling discipline, and the same integrity
rules as postgres-dba — SQLite is just a smaller engine running them.
This skill is about the SQLite schema, the Drizzle layer, and operating
SQLite. For schema-design philosophy (why NOT NULL FKs, why a surrogate
key) the authority is postgres-dba; this skill does not re-argue it, it
ports it. For app-layer TypeScript use the language skills.
How to use this skill
- Match the task to a rule below or in the decision guide.
- Open the matching
references/*.mdfor the rationale, the wrong way, the right way, and the explicit-override escape hatch. - Copy the closest file from
templates/and adapt it — the templates already encode every convention, so you start compliant and portable. - Apply the rule unless you can state, in a code comment, the specific reason it does not apply. "SQLite let me" is not a reason — SQLite lets you do almost anything; Postgres will not.
The hard rules (non-negotiable defaults)
- Naming is identical to
postgres-dba.snake_case, lowercase, unquoted. Tables plural (orders), columns singular (shipped_at), primary key literallyid, FKs<table_singular>_id, booleansis_/has_, timestamps past-tense_at. In Drizzle the TypeScript property may becamelCase, but the column name argument is always thesnake_casename —createdAt: integer('created_at', …). The database never sees a quoted mixed-case identifier. Seereferences/naming.md. bun:sqlite+drizzle-orm/bun-sqlite, oneDatabaseper process. That is the dev driver. Every connection runs the pragma block (Rule 3) at open. SQLite has one writer at a time; a single sharedDatabaseinstance plus serialized writes is the model — do not invent a pool of writers. Seereferences/drizzle.md.- Pragmas are mandatory and per-connection. Every connection sets, in
order:
journal_mode = WAL,foreign_keys = ON,busy_timeout = 5000,synchronous = NORMAL.foreign_keysis OFF by default in SQLite — forgetting it silently disables everyON DELETEyou wrote. This is the single most common SQLite data-integrity bug. Seereferences/drizzle.md. - Tables are
STRICT, types come from the Postgres-portable set only. DeclareSTRICT(SQLite ≥ 3.37) so a column actually rejects wrong types instead of silently coercing. Use only the type vocabulary inreferences/types.md:INTEGER,TEXT,REAL,BLOBmapped to a concrete Postgres target. NoVARCHAR(n), noDATETIME, noBOOLEAN, noNUMERIC— those are affinity theater in SQLite and lie about the Postgres column you will eventually create. - FKs are
NOT NULLby default with explicitON DELETE. Same rule aspostgres-dbaRule 4. A nullable FK carries a// nullable-fk: <reason>comment or it is a defect. Many-to-many is a compound primary key, no surrogateidon a pure junction. Seereferences/portability.md. - Enums are
text+ aCHECK (col IN (...)), mirroring the Postgres enum. SQLite has no enum type. Use Drizzletext({ enum: [...] })and an explicit check constraint so the values match theCREATE TYPEyou will write in Postgres. Booleans areinteger({ mode: 'boolean' })+CHECK (col IN (0,1)). Seereferences/portability.md. - Timestamps are one chosen representation, app-set, always UTC.
SQLite has no
timestamptz. Default:integerepoch-milliseconds via Drizzle{ mode: 'timestamp_ms' }, set by the app, neverCURRENT_TIMESTAMP(which emits a non-ISO, tz-ambiguous string). This ports to Postgrestimestamptzmechanically. Seereferences/types.md. - JSON is
text({ mode: 'json' })with a relational spine, mirroring the JSONB rule. Keys/FKs/hot fields are real columns; open-ended data is a JSON document, hot fields lifted out via aGENERATED ... STOREDcolumn and indexed. This ispostgres-dbaRule 8 withjsonb→textJSON. Seereferences/portability.md. - Set-based / multi-row business logic lives in an explicit
transaction in a named module — not scattered across callers. This
is the one place SQLite cannot match
postgres-dbaRule 7 (noplpgsql). Quarantine that logic in one repository/service function wrapped in adb.transaction(...)so the Postgres cutover has exactly one place to consider promoting to a function. Seereferences/portability.md.
Decision guide
| Situation | Rule | Reference |
| Naming any table/column/index/constraint | Rule 1 | references/naming.md |
| Drizzle TS property vs DB column name | Rule 1 | references/naming.md |
| Choosing the driver / opening the DB | Rule 2 | references/drizzle.md |
| WAL / foreign_keys / busy_timeout setup | Rule 3 | references/drizzle.md |
| Picking a column type | Rule 4 | references/types.md |
STRICT table or not | Rule 4 | references/types.md |
FK nullability and ON DELETE | Rule 5 | references/portability.md |
| Many-to-many junction | Rule 5 | references/portability.md |
| Status / role / kind column | Rule 6 | references/portability.md |
| Boolean column | Rule 6 | references/types.md |
| Storing a timestamp / date / money | Rule 7 | references/types.md |
| Open-ended / document-shaped data | Rule 8 | references/portability.md |
| Indexing a value inside a JSON document | Rule 8 | references/portability.md |
| Multi-row business rule / invariant | Rule 9 | references/portability.md |
| Generating & applying migrations | — | references/drizzle.md |
| "Can SQLite be production here?" | — | references/production.md |
| Backups / durability / replication | — | references/production.md |
| Cutting over from SQLite to Postgres | — | references/portability.md |
Templates
templates/schema.ts— a Drizzle SQLite schema demonstrating every rule: snake_case columns under camelCase keys,idsurrogate key, atext+CHECKenum, integer boolean, epoch-ms timestamps, NOT NULL FKs with explicitonDelete, one annotated nullable-FK override, a compound-key junction, a STORED generated column, and a JSON body with a lifted+indexed hot field.templates/db.ts— the dev connection:bun:sqlite+ Drizzle, the mandatory pragma block applied at open, and startup migration.templates/db.production.ts— the production connection: pragmas tuned for a server (mmap, cache), single-writer discipline,IMMEDIATEtransaction helper, and aVACUUM INTObackup hook.templates/drizzle.config.ts— drizzle-kit config for the SQLite dialect, with the one-line change that points it at Postgres later.
What this skill will not do
- Bless a SQLite-only construct that has no Postgres equivalent in the
schema (e.g. relying on rowid aliasing, type affinity coercion, or
WITHOUT ROWIDfor a normal table). If Postgres can't express it, it doesn't belong in a portable schema. - Bless
foreign_keysleft at the default. An app that doesn't set the pragma has no referential integrity, full stop. - Bless
CURRENT_TIMESTAMP/DATETIME('now')for stored timestamps — the format is not ISO-8601 and not tz-aware (Rule 7). - Bless a multi-writer connection pool against one SQLite file.
- Bless SQLite in production by default. It can be production — under
the explicit conditions in
references/production.md, and only there. - Re-derive schema-design philosophy. That is
postgres-dba's job; this skill ports its conclusions to a smaller engine.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

