Human FK Rule — contact as sole human identity

Every foreign key pointing at a human points at contacts.id; references(() => users.id) is forbidden everywhere. Spec: docs/superpowers/specs/2026-04-24-contact-as-sole-human-identity-design.md in renewa-gmbh/bookish-broccoli.

The model

  • contacts = durable human identity (Contacts). Identity attributes, membership, attribution (createdBy, approvedBy, decidedBy, …), assignment/ownership (assignedTo, reviewerId, ownerId, …), and audit references all FK here.
  • users = one authentication method among many, keyed on contact_id (Users). A person can exist (and be referenced) without a login.
  • Active-ness is a predicate, not an FK target: SELECT 1 FROM users WHERE contact_id = $1 AND is_active = true.
  • Writes use actor.contact.id (ServiceActor); auth-type gating reads actor.authFactors.

Enforcement — ratcheted, Strangler Fig

  • CI grep gate with an allowlist of pre-existing users.id FKs (the number only goes down) + /audit checks on schema-touching PRs.
  • Same migration mechanism later reused by Persistence Topology (I#2013).
  • Notable migration: workflow_transition_log.actorIdcontacts.id (PR#1931, I#1759).
  • Grandfathered (tracked, not new-code-eligible): auth-subsystem userId columns (sessions, *_tokens), audit_logs.userId, document_obtaining_request_batches.reviewerId/assigneeId/ownerId.

See Also