| name | db-schema |
| description | Apply when designing database schemas, writing migrations, or reviewing table structure. Covers naming, keys, indexes, constraints, nullability, and migration safety. |
| license | MIT |
| version | 1.0.0 |
| tokens_target | 1900 |
| triggers | ["database schema","migration","table design"] |
| loads_after | [] |
| supersedes | [] |
Sub-Skill: Database Schema Design
Purpose: Prevent data-integrity bugs, race conditions, and irreversible migrations by enforcing database-level constraints and safe migration patterns before code reaches production.
Rules
Naming Conventions
- Snake case everywhere. Always use
snake_case for table names, column names, and index names; never use camelCase or PascalCase in SQL identifiers.
- Plural table names. Always name tables in the plural (
users, orders, line_items); singular names cause inconsistency across ORMs and query builders.
- Consistent FK column naming. Always name foreign-key columns
<referenced_table_singular>_id (e.g., user_id, order_id) so the reference is self-documenting.
Primary Keys
- Surrogate primary key. Always define an explicit surrogate primary key (
id) on every table; never rely on natural keys as the sole PK unless the domain guarantees immutability.
- UUID vs serial. Prefer
uuid PKs for distributed systems or public-facing IDs; prefer bigserial / bigint GENERATED ALWAYS AS IDENTITY for high-insert internal tables where sort order matters.
Foreign Keys & Referential Integrity
- Declare FK constraints. Always declare
FOREIGN KEY constraints in the schema; never rely on application-layer join logic to enforce referential integrity.
- Explicit ON DELETE behaviour. Always specify
ON DELETE behaviour (CASCADE, SET NULL, or RESTRICT) on every FK; omitting it defaults to RESTRICT, which can cause surprising errors at runtime.
- Missing FK is a data-integrity bug. Never ship a column that references another table without a corresponding FK constraint — orphaned rows accumulate silently. Reference: ERR-2026-022
Indexes
- Index every FK column. Always add an index on every foreign-key column; unindexed FKs cause full-table scans on joins and cascade operations.
- Covering indexes for hot queries. Use covering indexes (include all columns in the
SELECT) for read-heavy queries rather than adding redundant columns to the base table.
- Unique constraints at DB level. Always enforce uniqueness at the database level rather than relying on application-layer deduplication; race conditions between concurrent writes bypass app checks. Reference: ERR-2026-022
Nullability & Defaults
- Explicit NOT NULL. Always mark columns
NOT NULL unless NULL carries distinct semantic meaning (unknown vs. absent); nullable columns complicate query logic and ORM mapping.
- Meaningful defaults. Always supply a
DEFAULT for columns that have a sensible zero-value (0, '', false, now()); avoid forcing callers to supply values the DB can compute.
Timestamps & Soft Delete
- Standard audit columns. Always include
created_at TIMESTAMPTZ NOT NULL DEFAULT now() and updated_at TIMESTAMPTZ NOT NULL DEFAULT now() on every table; omitting them makes debugging and auditing impossible.
- Soft delete with deleted_at. Prefer
deleted_at TIMESTAMPTZ over hard deletes for user-facing entities; ensure all queries filter WHERE deleted_at IS NULL or use a view/partial index.
Enum Columns
- DB-native enum or check constraint. Always enforce enum-like columns with a
CHECK constraint or a native ENUM type; never store unconstrained strings and validate only in the application.
Migrations
- Up and down migrations. Always write both an
up and a down migration; a rollback path is mandatory before any migration reaches production.
- Idempotent migrations. Always guard DDL statements with
IF NOT EXISTS / IF EXISTS so re-running a migration does not error; this is required for blue-green and multi-replica deployments.
- Non-blocking index creation. Use
CREATE INDEX CONCURRENTLY (Postgres) or equivalent for indexes on large tables; blocking index creation causes downtime.
Constraints Over App Logic
- Check constraints for invariants. Always encode business invariants (e.g.,
price > 0, quantity >= 0) as CHECK constraints; application-layer validation is bypassed by direct DB writes, scripts, and migrations.
See also
skills/code-quality/SKILL.md — general code-quality rules that apply to migration scripts
skills/review-deployment/SKILL.md — deployment checklist that includes migration safety gates