Decision Log (ADRs) — Index
What this covers / who it's for. The D-numbered architecture decision log for
stables-core-app— one file per decision going forward — D1 through D93 today, with three historical clusters (D27–D32, D33–D35, the D38–D69 register) grouped as single files. It records what was decided, why, and what is still open, for engineers (and AI agents) who need the rationale behind a pattern before changing it. Data security and tenant data hygiene are the overriding goals — when a choice trades convenience for stronger isolation, we take the isolation.
Context (the log's charter)
stables-core-appis the Laravel 13 / PHP 8.5 control plane for a fintech neo-bank. It is the main interface to a banking-rails provider accessed over a REST API.- We run single-database multi-tenancy with PostgreSQL Row-Level Security (RLS) via
stancl/tenancyv4. No multi-database, no separate-schema, Postgres only. - Two faces of the app:
- Central app — backoffice/admin. Runs under a
BYPASSRLSPostgres role. Sees all tenants' data plus central/reference tables. - Tenant app — client-facing. Runs under a
NOBYPASSRLSRLS role; every query is auto-scoped to the current tenant by a Postgres session variable + RLS policies.
- Central app — backoffice/admin. Runs under a
- The app is a modular monolith using
InterNACHI/modular. - A tenant == a client/business whose data must be isolated.
The log began as the multi-tenancy setup decision log (branch feat/tenancy-rls-foundation)
and grew into the app-wide ADR register. Its companion appendices — open items, the tenancy
package inventory, the historical file manifest, team conventions, and the changelog — live in
appendices.md.
Recording a new ADR
New ADRs are recorded here (in wiki/engineering/decisions/), not in docs/:
- One new file per decision, following the naming scheme: lowercase, zero-padded 3-digit
D-number, short kebab slug from the title (≤6 words) — e.g.
d094-some-new-decision.md. Use the standard page header (title/audience/related: README.md), make the ADR title the H1, and end with the[← Decision log index](README.md)backlink. - Add a row to the index table below (D-number, title, status, file link).
- Parallel branches: take the next free D-number, add an HTML comment
<!-- RENUMBER-ON-MERGE: parallel branch -->on the H1 line of the file and mark the index row. At merge time, the later branch renumbers: rename the file, update the D-number in its H1/title, and sweep every reference to the old number (docs, READMEs, skills, tracking docs). - Sub-amendments to an existing decision go in the parent's file as
D<n>.<m>(H2 heading — theD74.1/D76.1precedent), not as a new file.
The decisions
Rows marked ⚠ carry a RENUMBER-ON-MERGE: parallel branch comment (see above).
| D | Title | Status | File |
|---|---|---|---|
| D1 | Single-DB + Postgres RLS | Resolved | d001 |
| D2 | Tenant identification: request data (X-Tenant) | Resolved | d002 |
| D3 | Postgres everywhere, including tests | Resolved | d003 |
| D4 | RLS roles (central BYPASSRLS vs stables_rls) | Resolved | d004 |
| D5 | RLS manager + session variable | Resolved | d005 |
| D6 | Bootstrappers enabled (single-DB RLS profile) | Resolved | d006 |
| D7 | Table scoping: scopeByDefault = true | Resolved | d007 |
| D8 | Tests on Postgres — approach pending | Superseded by D13 | d008 |
| D8a | Package version: track dev-master | Resolved (stable-tag pin still open) | d008a |
| D9 | Tenant model uses HasDatabase | Resolved | d009 |
| D10 | RLS role provisioning & grants | Resolved | d010 |
| D11 | Session variable locked to stables.current_tenant | Resolved | d011 |
| D12 | Central Livewire backoffice, tenant headless REST API | Resolved | d012 |
| D13 | Dedicated Postgres test database (stables_testing) | Resolved | d013 |
| D14 | RLS isolation tests without RefreshDatabase | Resolved | d014 |
| D15 | Client auth: Sanctum SPA + tenant-scoped ClientUser | Resolved | d015 |
| D16 | Email-first tenant discovery (central) | Resolved (hardening tracked) | d016 |
| D17 | API versioning: URL /api/v1/… | Resolved | d017 |
| D18 | Cloudflare Turnstile on unauthenticated endpoints | Resolved | d018 |
| D19 | Modular monolith: code in modules, App\ empty | Resolved | d019 |
| D20 | Module-owned Blade views + Livewire | Resolved | d020 |
| D21 | Inspiration mined from the legacy stablesxyz/core repo | Partly superseded by D22 | d021 |
| D22 | CQRS bus + JSON:API envelope + Scramble extensions | Resolved (supersedes D21's CQRS-skip) | d022 |
| D23 | Fundamentals-readiness hardening | Resolved | d023 |
| D24 | RBAC in a dedicated Authorization module | Resolved (schema superseded by D25/D26) | d024 |
| D25 | Internal bigint PK + external UUIDv7 | Resolved | d025 |
| D26 | Soft deletes + blamable, codified | Resolved | d026 |
| D27 | Money is integer minor units + explicit currency | Resolved (enforced) | d027–d032 |
| D28 | Write idempotency for externally-triggered commands | Resolved (realized in Phase 0 / D56) | d027–d032 |
| D29 | Pessimistic row locks for balance mutations | Recorded convention | d027–d032 |
| D30 | PII encryption-at-rest + data classification | Recorded convention | d027–d032 |
| D31 | Retention + GDPR erasure vs soft-delete/blamable | Resolved (floor realized in D70) | d027–d032 |
| D32 | Ledger correctness invariants (double-entry, append-only) | Recorded convention | d027–d032 |
| D33 | Custom Features/Entitlements engine; Pennant dormant | Resolved | d033–d035 |
| D34 | Jurisdiction gates, tier entitles | Resolved | d033–d035 |
| D35 | Stepped/banded entitlements + usage metering | Resolved | d033–d035 |
| D36 | Per-module permission registry, both guards | Resolved | d036 |
| D37 | Permission dependencies at role-build time only | Resolved | d037 |
| D38–D69 | Conduit integration register — authoritative home: docs/conduit/README.md §3; the log file records per-phase realizations + refinements | Pointer register | d038–d069 |
| D70 | Age-based retention prunes + the 7-year compliance floor | Resolved | d070 |
| D71 | DefaultRoleRegistry: default roles per module | Resolved | d071 |
| D72 | Real per-module entitlement features | Partial (provisioning gap tracked) | d072 |
| D73 | Per-tenant entitlement overrides | Resolved | d073 |
| D74 | Observability app-code slices (+ D74.1 hardening) | Resolved | d074 |
| D75 | DomainException carries JSON:API source + meta | Resolved | d075 |
| D76 | Expected client 4xx reclassified (+ D76.1 durable carve-out) | Resolved (narrow compliance question open) | d076 |
| D77 | Class-aware notification-log retention | Resolved (mechanism-only) | d077 |
| D78 | Document-vault retention + RTBF crypto-shred | Resolved (mechanism-only) | d078 |
| D79 | DLP V2 egress detection | Resolved (mechanism-only) | d079 |
| D80 | Jurisdictions: full ISO hierarchy + gate inheritance | Resolved (evolved by D81) | d080 |
| D81 | Jurisdiction enrichment: M2M blocs + currency/region data | Resolved | d081 |
| D82 | Local S3-compatible object storage: MinIO | Resolved ⚠ | d082 |
| D83 | Async-request platform (pollable request/status) | Resolved ⚠ | d083 |
| D84 | Typed finance-provider identity (FinanceProvider enum) | Resolved ⚠ | d084 |
| D85 | Audit/DLP growth: WORM segment export | Resolved ⚠ | d085 |
| D86 | Documents direct-to-object-storage transfer | Resolved ⚠ | d086 |
| D87 | Direct-path encryption at rest: bucket-default SSE-KMS | Resolved ⚠ | d087 |
| D88 | Documents malware scanning: quarantine lifecycle | Resolved ⚠ | d088 |
| D89 | Notification recipient resolution + CTAs as plain data | Resolved ⚠ | d089 |
| D90 | Typed supported-asset vocabulary (SupportedAsset enum) | Resolved ⚠ | d090 |
| D91 | Per-tenant base currency | Resolved ⚠ | d091 |
| D92 | Defined pair-rate providers registry | Resolved ⚠ | d092 |
| D93 | Durable async-task execution (task-runner) | Resolved ⚠ | d093 |
Thematic clusters
- Tenancy / RLS: D1–D11, D13, D14, D53
- CQRS bus + API contract: D12, D17, D21, D22, D23, D75, D76
- Identifiers / schema conventions: D25, D26, D84, D90
- Money safety: D27–D32, D61, D91, D92
- Features / entitlements / jurisdictions: D33–D35, D72, D73, D80, D81
- Auth + RBAC: D15, D16, D18, D24, D36, D37, D51, D62, D71
- Conduit integration: D38–D69
- Audit / DLP / documents: D41, D44, D46, D54, D57, D69, D76, D78, D79, D85, D86, D87, D88
- Retention / lifecycle: D31, D70, D77, D78, D83
- Async platforms: D83, D93
- Observability: D74, D85, D89
(D38–D69 numbers cited above resolve inside the Conduit register
/ docs/conduit/README.md §3.)
Appendices
Open items, the tenancy package inventory, the historical file manifest, team conventions, and
the changelog: appendices.md.