Skip to main content

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-app is 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/tenancy v4. No multi-database, no separate-schema, Postgres only.
  • Two faces of the app:
    • Central app — backoffice/admin. Runs under a BYPASSRLS Postgres role. Sees all tenants' data plus central/reference tables.
    • Tenant app — client-facing. Runs under a NOBYPASSRLS RLS role; every query is auto-scoped to the current tenant by a Postgres session variable + RLS policies.
  • 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/:

  1. 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.
  2. Add a row to the index table below (D-number, title, status, file link).
  3. 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).
  4. Sub-amendments to an existing decision go in the parent's file as D<n>.<m> (H2 heading — the D74.1 / D76.1 precedent), not as a new file.

The decisions

Rows marked ⚠ carry a RENUMBER-ON-MERGE: parallel branch comment (see above).

DTitleStatusFile
D1Single-DB + Postgres RLSResolvedd001
D2Tenant identification: request data (X-Tenant)Resolvedd002
D3Postgres everywhere, including testsResolvedd003
D4RLS roles (central BYPASSRLS vs stables_rls)Resolvedd004
D5RLS manager + session variableResolvedd005
D6Bootstrappers enabled (single-DB RLS profile)Resolvedd006
D7Table scoping: scopeByDefault = trueResolvedd007
D8Tests on Postgres — approach pendingSuperseded by D13d008
D8aPackage version: track dev-masterResolved (stable-tag pin still open)d008a
D9Tenant model uses HasDatabaseResolvedd009
D10RLS role provisioning & grantsResolvedd010
D11Session variable locked to stables.current_tenantResolvedd011
D12Central Livewire backoffice, tenant headless REST APIResolvedd012
D13Dedicated Postgres test database (stables_testing)Resolvedd013
D14RLS isolation tests without RefreshDatabaseResolvedd014
D15Client auth: Sanctum SPA + tenant-scoped ClientUserResolvedd015
D16Email-first tenant discovery (central)Resolved (hardening tracked)d016
D17API versioning: URL /api/v1/…Resolvedd017
D18Cloudflare Turnstile on unauthenticated endpointsResolvedd018
D19Modular monolith: code in modules, App\ emptyResolvedd019
D20Module-owned Blade views + LivewireResolvedd020
D21Inspiration mined from the legacy stablesxyz/core repoPartly superseded by D22d021
D22CQRS bus + JSON:API envelope + Scramble extensionsResolved (supersedes D21's CQRS-skip)d022
D23Fundamentals-readiness hardeningResolvedd023
D24RBAC in a dedicated Authorization moduleResolved (schema superseded by D25/D26)d024
D25Internal bigint PK + external UUIDv7Resolvedd025
D26Soft deletes + blamable, codifiedResolvedd026
D27Money is integer minor units + explicit currencyResolved (enforced)d027–d032
D28Write idempotency for externally-triggered commandsResolved (realized in Phase 0 / D56)d027–d032
D29Pessimistic row locks for balance mutationsRecorded conventiond027–d032
D30PII encryption-at-rest + data classificationRecorded conventiond027–d032
D31Retention + GDPR erasure vs soft-delete/blamableResolved (floor realized in D70)d027–d032
D32Ledger correctness invariants (double-entry, append-only)Recorded conventiond027–d032
D33Custom Features/Entitlements engine; Pennant dormantResolvedd033–d035
D34Jurisdiction gates, tier entitlesResolvedd033–d035
D35Stepped/banded entitlements + usage meteringResolvedd033–d035
D36Per-module permission registry, both guardsResolvedd036
D37Permission dependencies at role-build time onlyResolvedd037
D38–D69Conduit integration register — authoritative home: docs/conduit/README.md §3; the log file records per-phase realizations + refinementsPointer registerd038–d069
D70Age-based retention prunes + the 7-year compliance floorResolvedd070
D71DefaultRoleRegistry: default roles per moduleResolvedd071
D72Real per-module entitlement featuresPartial (provisioning gap tracked)d072
D73Per-tenant entitlement overridesResolvedd073
D74Observability app-code slices (+ D74.1 hardening)Resolvedd074
D75DomainException carries JSON:API source + metaResolvedd075
D76Expected client 4xx reclassified (+ D76.1 durable carve-out)Resolved (narrow compliance question open)d076
D77Class-aware notification-log retentionResolved (mechanism-only)d077
D78Document-vault retention + RTBF crypto-shredResolved (mechanism-only)d078
D79DLP V2 egress detectionResolved (mechanism-only)d079
D80Jurisdictions: full ISO hierarchy + gate inheritanceResolved (evolved by D81)d080
D81Jurisdiction enrichment: M2M blocs + currency/region dataResolvedd081
D82Local S3-compatible object storage: MinIOResolved ⚠d082
D83Async-request platform (pollable request/status)Resolved ⚠d083
D84Typed finance-provider identity (FinanceProvider enum)Resolved ⚠d084
D85Audit/DLP growth: WORM segment exportResolved ⚠d085
D86Documents direct-to-object-storage transferResolved ⚠d086
D87Direct-path encryption at rest: bucket-default SSE-KMSResolved ⚠d087
D88Documents malware scanning: quarantine lifecycleResolved ⚠d088
D89Notification recipient resolution + CTAs as plain dataResolved ⚠d089
D90Typed supported-asset vocabulary (SupportedAsset enum)Resolved ⚠d090
D91Per-tenant base currencyResolved ⚠d091
D92Defined pair-rate providers registryResolved ⚠d092
D93Durable 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.


← Engineering wiki index