D22 — CQRS command/query bus + JSON:API envelope + Scramble extensions (RESOLVED, supersedes D21)
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.
User decision — reverses D21's "skip CQRS" recommendation and resolves D21's "API response
contract" user-decision. Route every write through a CommandBus and every read through a
QueryBus, so the bus is the single place the app gates (authorize) and tracks (audit) work;
serialise all API responses in the JSON:API envelope; and port the legacy repo's Scramble
type extensions so the generated OpenAPI doc reflects both.
The bus (Modules\Foundation\Bus):
Contracts\{Command,Query}— each message declareshandledBy(): class-stringand an abstractauthorize(?Authenticatable): bool, forcing every message to consciously declare its gate (fail-safe).Contracts\{CommandBus,QueryBus}exposedispatch()/ask()returning the handler's genericTResult(no native return type — the only entries inReturnTypeTest's exception list).AbstractCommand/AbstractQuery—abstract readonly class; a message is an immutable value.LaravelCommandBus::dispatch()— on deny: auditcommand.denied(warning) + throwAccessDeniedHttpException; else auditcommand.dispatched, run the handler insideDB::transaction, auditcommand.executed(+ duration), fireCommandExecuted;command.failedon throw.LaravelQueryBus::ask()is identical minus the transaction (firesQueryExecuted).Concerns\AuditsBusMessages→ dedicatedauditlog channel (config/logging.php: daily, 90-day retention) with actor,tenant_id,correlation_id. FacadesBus\Facades\{CommandBus, QueryBus}; bound as singletons inFoundationServiceProvider.
JSON:API runtime (Modules\Foundation\Http):
Resources\JsonApiResource— base extending Laravel 13.12's nativeIlluminate\Http\Resources\JsonApi\JsonApiResource, mergingmeta(JsonApiMeta:request_id/correlation_id/timestamp).Resources\JsonApiEnvelope::make()—{data, meta, jsonapi:{version:"1.1"}},application/vnd.api+json.Http\JsonApiExceptionHandler— renders JSON-expecting errors as{errors[], meta}(validation carriessource.pointer = /data/attributes/<field>); registered inbootstrap/app.php.Http\Middleware\AssignCorrelationId(aliascorrelation) — request/correlation id intoContext- echoed response headers; applied to both API route groups.
Scramble extensions (Modules\Foundation\Scramble, registered in config/scramble.php): ported
7 of the legacy repo's 8 (paginated-collection deferred until a paginated endpoint exists),
re-namespaced to Foundation. They (a) infer dispatch()/ask() return types from the message's
handler, (b) render spatie-data DTOs + JSON:API resources/envelope/errors under
application/vnd.api+json. Auth is documented via MiddlewareAuthSecurityStrategy (Sanctum SPA
cookie scheme) so only auth:* routes are marked secured — login + discovery stay public.
scramble:export generates clean.
Identity converted as the reference implementation: ClientAuthController (login/logout/me) +
TenantDiscoveryController dispatch through the bus and return JSON:API; Commands\{LoginClient, LogoutClient}Command, Queries\{DiscoverTenants,GetCurrentClient}Query, their Handlers/,
ClientUserResource, and the {ClientUser,TenantSummary}Data DTOs.
Tests: tests/Architecture/CqrsTest (messages implement the contract, are readonly, route to a
real handler — discovered from disk so new modules are covered automatically);
tests/Feature/Foundation/CommandBusTest (denied command throws + is audited + handler never runs;
authorized command runs in a transaction, returns the result, fires CommandExecuted); the Identity
HTTP tests updated to JSON:API shapes (data.attributes.*, data[], errors.0.source.pointer).
79 tests green.
Convention going forward: business logic is invoked only via the bus — a controller/Livewire
action builds a Command/Query and dispatch()/ask()s it; handlers hold the logic; the gate +
audit are non-negotiable. This supersedes D21's "invokable Actions, not CQRS": where an Action is
still a useful unit of logic, it becomes a bus handler.