Database design
Use when designing a new schema, writing a migration, adding or auditing indexes, reviewing query performance, choosing between hard delete and soft delete, or establishing database conventions for a project.From its SKILL.md
npx -y skills add aneja5/forge-skills --skill database-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.5 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it
Database Design
Overview
Define the project's schema conventions and migration safety policy before migrations land in production. Outputs .forge/database-design.md (naming, FK rules, audit columns, soft-delete policy, index strategy, partition strategy, RLS/tenant-scoping rules) and .forge/migrations-policy.md (reversibility, idempotency, locking-aware patterns, the query-review checklist). Consumed by architecture-and-contracts and incremental-implementation.
When to Use
- A new project starts and the schema is being designed
- A migration is being written that touches a table >1M rows
- An ORM is generating N+1 queries in a hot path
- An audit reveals tables with no FK constraints or missing indexes
- The team is debating soft delete vs hard delete and there's no policy
- A production query is timing out and there's no EXPLAIN process
When NOT to Use
- Adding a column to a tiny dev-only table where conventions already apply
- Schema-less stores (Mongo, DynamoDB) — see
architecture-and-contractsdata-model section instead - A migration on an existing project that has documented conventions and the change is local
Common Rationalizations
| Thought | Reality |
|---|---|
| "We can add indexes later" | Missing indexes in production cause outages, not slowdowns. A full table scan on a 50M-row table locks the DB. |
| "Soft delete is always better" | Soft delete without a cleanup job becomes data hoarding. GDPR forces real deletion eventually. |
| "ORMs handle it" | ORMs generate N+1 queries by default. Lazy loading + a loop = production incident. |
| "One big migration is fine" | Irreversible migrations are deployment landmines. Forward-only with a long expand-contract window is the safe path. |
| "We don't need FK constraints, the app enforces it" | The app has bugs. The DB doesn't. FK constraints are the last line of defense against orphan rows. |
| "Let's just denormalize for speed" | Denormalization without a sync job creates two sources of truth that diverge. |
Red Flags
ALTER TABLE large_table ADD COLUMN ... NOT NULLwith no locking review- Foreign key columns without indexes
- A migration that drops or renames a column without a deprecation window
- Soft delete columns (
deleted_at) with no cleanup or RLS policy - ORM
findAll()followed by.map(item => item.related)(N+1) - A query that scans rows without an EXPLAIN run
- No
created_at/updated_ataudit columns - Multi-tenant tables without RLS or
tenant_idindexes
Core Process
Step 1: Define naming conventions
Write these to .forge/database-design.md:
- Tables:
snake_case, plural (users,task_assignments) - Columns:
snake_case, no abbreviations (created_atnotcr_at) - Primary keys:
id(UUIDv7 preferred — sortable, no hot shards) - Foreign keys:
<table_singular>_id(user_id,task_id) - Indexes:
idx_<table>_<columns> - Constraints:
chk_<table>_<rule>,unq_<table>_<columns> - Audit columns required on every business table:
created_at,updated_at(timestamptz, server-set)
Step 2: Audit existing schema (if applicable)
Run a check: every FK has an index? every business table has audit columns? every soft-delete table has cleanup? Record violations in .forge/database-design.md with an owner and a deadline.
Step 3: Set migration guardrails
In .forge/migrations-policy.md:
- Reversibility: every migration has an explicit
down. Forward-only migrations require an ADR. - Idempotency:
CREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS,CREATE INDEX IF NOT EXISTS. - Locking-aware:
CREATE INDEX CONCURRENTLYon Postgres. Online schema change (gh-ost, pt-osc) for MySQL on large tables. NeverALTER TABLEwithACCESS EXCLUSIVE LOCKduring business hours. - Expand-contract pattern: add new column → backfill → switch reads → drop old column over multiple deploys.
- Backup before destructive: any
DROP COLUMN,DROP TABLE,TRUNCATE, or destructiveUPDATErequires a snapshot reference in the PR.
Step 4: Write the query-review checklist
Append to .forge/database-design.md:
- Every hot-path query has been EXPLAINed (Postgres) or
EXPLAIN ANALYZEd - No sequential scan on a table >100k rows
- No N+1 — verify with query logs in a test that simulates the loop
- Index used for every JOIN and WHERE condition
- LIMIT used on any user-facing list query
- Pagination uses cursor (not OFFSET) for large tables
Step 5: Identify partition candidates
Tables likely to exceed 100M rows (events, logs, audit, time-series): plan partitioning before they grow. Document the partition key (usually created_at range or tenant_id hash) and the retention/cleanup job.
Step 6: Document RLS / tenant-scoping rules
For multi-tenant systems:
- Every business table has
tenant_idand a Row-Level Security policy - Application-side defense (every query filters by
tenant_id) + DB-side defense (RLS) — both, not either-or - The contract from
architecture-and-contractsMUST specify which tables are tenant-scoped
Step 7: Headers
Prepend a forge:meta header on both .forge/database-design.md and .forge/migrations-policy.md (co-output — both share the same depends_on + generated_from): generated_by: database-design, generated_at: <ISO 8601 UTC with Z>, depends_on: [.forge/architecture.md] — paths only, never hashes, generated_from: {.forge/architecture.md: <upstream content_hash AT generation time>}, content_hash: <sha256 first 8 of THIS file's body> (each file computes its own). See forge-dependency-graph.
Verification
-
.forge/database-design.mdwritten with naming conventions, soft-delete policy, partition strategy -
.forge/migrations-policy.mdwritten with reversibility, idempotency, locking-aware rules - Every migration in the codebase is idempotent (
IF NOT EXISTSor equivalent) - Every FK has an index (verified via schema query)
- Every business table has
created_atandupdated_at - Every hot-path query has an EXPLAIN attached to the PR (or linked in
.forge/database-design.md) - No migration drops user-visible data without an ADR
- Multi-tenant tables have RLS policies and
tenant_idindexes - N+1 queries are gone from the codebase (verified by query-count test in CI)
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 1 of the 12 instructions most databases sql skills give in ~1.5k tokens
Counted across 609 of the 712 authors here whose files we hold, read 2026-09-06
- Index all foreign key columnsin 26 of 609
- Use cursor pagination instead of offsetin 25 of 609, across 20 files
- Use timestamptz for timestampsin 21 of 609
- Specify columns instead of using select starin 20 of 609, across 10 files
- Use parameterized queries for all database interactionsin 20 of 609, across 19 files
- Use Enum for categorical datain 17 of 609, across 7 files
- Order by frequently filtered columnsin 17 of 609, across 7 files
- Batch data insertsin 17 of 609, across 7 files
- Use expand-contract pattern for schema changeshere, and in 17 of 609
- Use materialized views for real-time aggregationsin 16 of 609, across 6 files
- Partition tables by timein 16 of 609, across 6 files
- Use smallest appropriate data typesin 16 of 609, across 6 files
Said here and by no other author read
- define schema conventions before production migrations
- require explicit down migrations for all changes
- require snapshot references for destructive operations
- explain all hot-path queries
- require audit columns on all business tables
- prepend forge meta headers to design documents
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.