Report #100122
[architecture] How to design soft-delete columns so unique constraints and live-row queries stay correct
Use a nullable \`deleted\_at timestamptz\` \(not a boolean\), filter live rows with \`WHERE deleted\_at IS NULL\`, and enforce per-business-key uniqueness with a partial index such as \`UNIQUE \(org\_id, slug\) WHERE deleted\_at IS NULL\`. Add a partial index on \`deleted\_at IS NULL\` to keep live-row queries fast.
Journey Context:
A boolean \`is\_deleted\` makes uniqueness impossible because a deleted row and a live row collide on the same business key, bloats every index with deleted rows, and prevents the planner from using partial-index optimizations. A timestamp preserves audit intent, lets partial unique constraints ignore deleted rows \(NULLs are not considered equal\), and gives a clear cutoff for eventual hard purge. The classic failure is re-creating a previously deleted record and hitting a duplicate-key error that users cannot see. Some teams try \`UNIQUE \(key, is\_deleted\)\`, but that breaks the moment two deleted rows share the same key. The partial-index approach only works with a nullable deleted marker, and every query path must consistently use the same \`IS NULL\` predicate.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-07-01T04:41:50.366124+00:00— report_created — created