Skip to main content

D26 — Soft deletes + blamable, codified like the id/uuid convention

Architecture decision record, relocated verbatim from the retired single-file docs/tenancy/ decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.

Two more app-wide column conventions, enforced the same way as D25 (trait + Blueprint macro + arch

  • schema guards, mandatory-unless-excluded).

Soft deletes — universal, unless excluded. Every entity table gets deleted_at ($table-> softDeletes()) + Laravel's SoftDeletes trait. Excluded: framework/vendor tables, pure pivots, and tenants (the stancl-managed / VirtualColumn tenancy boundary — lifecycle lives in the tenancy layer). Immutable financial tables (ledger/journal/audit) will be added to the exemption when they land — they are append-only/reversed, never deleted.

Blamable — polymorphic actor. created_by / updated_by / deleted_by, each a *_type + *_id morph ($table->blamable()nullableMorphs), filled by Modules\Foundation\Concerns\ Blamable from the authenticated actor of the active guard — a central operator (User) OR a tenant client user (ClientUser), the same source as the CQRS bus audit. Polymorphic (no cross-boundary FK) so either actor type is recordable; nullable so console/seeder/system writes leave it blank. Exposes creator()/updater()/deleter() morphTo relations. deleted_by is stamped via a targeted update in the deleting hook because soft delete's own update bypasses arbitrary columns.

Partial unique indexes. Soft-deletable tables with a unique constraint use a partial index WHERE deleted_at IS NULL (raw, in the migration) so a deleted row's unique value can be re-used: users (email), client_users (tenant_id, email), tenant_roles (tenant_id, name).

Mechanism + enforcement. $table->softDeletes() (native) + $table->blamable() (Foundation macro); traits SoftDeletes + Blamable on User/ClientUser/TenantRole. Guards: tests/Architecture/ModelConventionsTest (audited models use both traits, unless excluded) + tests/Feature/SchemaConventionTest (every non-exempt table has deleted_at + the six blamable columns — its audit-exemption list adds tenants on top of the public-id list). The blamable morph columns carry no FK, so they add no RLS path — tenants:verify-isolation stays green. Behaviour proven by tests/Feature/Foundation/BlamableTest (creator/updater/deleter stamped; partial-unique re-use). All green: 79 tests, PHPStan L6, Pint.


← Decision log index