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.