| Migration filename | YYYYMMDDhhmmss_<slug>.up.sql (timestamp) | Two parallel feature branches won't collide on 000034_. |
| Idempotency in DDL | CREATE TABLE IF NOT EXISTS, ON CONFLICT DO NOTHING for seeds | Re-running a single migration on a partially-applied DB does not error. |
| Audit columns | created_at TIMESTAMPTZ NOT NULL DEFAULT now() only — no updated_at | Immutable-leaning: changes go through an audit table or event log if business needs them. |
| Delete strategy | Hard delete + audit table if business requires history | No deleted_at, no status-column for delete. DB stays simple; history is a separate concern. |
| PK | UUID v7, generated app-side | Cursor pagination, no insert sequence contention. |
| Naming | plural snake_case (users, goal_progress) | Postgres community standard. |
| Indexes | One per query (from sequences). Existing-table CREATE INDEX CONCURRENTLY always. | Each index has a write cost; CONCURRENTLY avoids long write locks. |
| Breaking changes | Auto-decompose to 3-step expand → backfill → contract | Zero-downtime by default. |
| New NOT NULL on existing table | Auto-decompose: add nullable → backfill → set NOT NULL | Default zero-downtime path. |
| String columns | VARCHAR(N) bounded; TEXT only for URLs / long descriptions | Schema-as-documentation; bounds drive validation. |
| JSONB | Only for semantically opaque payload (settings, metadata, polymorphic block.payload) | Structured fields → first-class columns. |
| Forbidden | CHECK, TRIGGER, DEFAULT '<business value>', sequences-as-PK | "DB as dumb storage" — business logic lives in code. |
| Multi-DB (replica, sharding) | Out of scope | Single-DB only. |
| Partitioning, materialized views | Out of scope | Performance optimization, not contract. |