Platform
Architecture decisions
34 records of a decision, its context and its consequences. What follows is each record's decision, extracted from the record itself.
Written from docs/adr/ — decision paragraphs are published; full records are released under agreement
- ADR-0001Accepted
Modular monolith with mechanically enforced boundaries
BRD §10 asks for "a modular, API-first core (microservices or modular monolith)". BRD §6 requires one build artifact deployable to Baobyte's cloud or a client's data centre. BRD §4 targets buyers ranging from a single microfinance bank to a tier-1 bank.
Decision
One Go module, one binary, run modes selected by --mode (server, worker, migrate, seed, verify-audit, pack).
- ADR-0002AcceptedRelated: Q4 (the schema-per-tenant half is unbuilt — see the Q4 correction), Q12
Tenancy isolation: shared schema with RLS, schema-per-tenant optional
BRD §14.3 names multi-tenant data isolation failure as a key risk. BRD §9 requires "strict logical (and optionally physical) data separation between SaaS tenants; no cross-tenant data leakage". BRD §6.2 requires multi-tenancy designed in from the start even where on-premise is effectively single-tenant.
Decision
Five independent layers. Any one of them alone would be sufficient; we implement all five because "sufficient" is not the standard here.
- ADR-0004AcceptedRelated: Q15
Append-only audit with a per-tenant hash chain
BRD §8.9 requires a "full, immutable audit trail of all user actions, configuration changes, and data access (tamper-evident logging)". BRD §9 requires immutable retention for a minimum of five years. Architectural mandate 6 specifies a per-tenant hash chain with no UPDATE or DELETE grants for the application role, a verification job, and an admin integrity view.
Decision
app.audit_event carries (tenant_id, seq, occurred_at, actor_id, actor_type, action, resource_type, resource_id, before, after, request_id, ip, prev_digest, digest) where
- ADR-0005Accepted
Configuration as versioned, schema-validated, layered documents
BRD §7 is the commercial thesis of the product: ship a working default, let each institution tailor it without custom code. BRD §7.2 enumerates what must be no-code configurable. BRD §9 requires changes achievable "by non-technical compliance staff through an admin console, without vendor intervention".
Decision
Configuration is a first-class subsystem, not a settings table.
- ADR-0006AcceptedRelated: Q1, Q6
Signed, versioned regulatory content packs
BRD §10 requires "a versioned 'regulatory content pack' (rule templates, report schemas, thresholds) that can be updated independently of core application releases as CBN/NFIU requirements evolve". BRD §3.3 makes embedded local regulatory knowledge the differentiator against international vendors — which is only true if that knowledge can be updated faster than the software release cycle.
Decision
A pack is a tar.gz of manifest.yaml plus items/, with a detached signature.
- ADR-0007Accepted
CEL as the tenant-authored expression layer
BRD §7.2 requires transaction monitoring rule thresholds, alert scoring logic and risk-scoring factors to be configurable with no code. Architectural mandate 4 requires a non-technical compliance administrator to compose a monitoring rule at runtime. ADR-0006 requires report field mappings expressed as target_field ← expression.
Decision
CEL-go, with limits set deliberately rather than left at defaults.
- ADR-0008Accepted
Maker-checker as a workflow primitive
BRD §8.5 requires "maker-checker (four-eyes) approval workflow before report filing or case closure". Architectural mandate 8 widens this: four-eyes applies to report filing, case closure, rule changes, risk-model changes and watchlist edits, and must be a first-class primitive rather than a per-screen bolt-on.
Decision
A single app.approval_request table and one generic flow:
- ADR-0009AcceptedRelated: Q10, Q21, Q22
Optional dependencies degrade rather than fail
BRD §6 requires the same artifact to run in Baobyte's cloud and in a client's data centre. The Phase 2 exit criterion is a clean-room install on a bare VM with no internet, completed from documentation alone. A microfinance bank's IT team is not going to operate a Redis cluster, an OpenTelemetry collector and an object store to run a compliance tool.
Decision
Exactly two dependencies are required: Postgres and an OIDC issuer. Everything else has a working in-process fallback selected by configuration at startup.
- ADR-0010Accepted
Money is int64 minor units plus a currency code
BRD §8.6 requires automatic CTR generation "for transactions above regulatory thresholds". BRD §9 requires Naira formatting. Architectural mandate: money is never a float.
Decision
types.Money{ Amount int64; Currency Currency } where Amount is in minor units (kobo for NGN, cents for USD). Stored as amount_minor bigint NOT NULL plus currency char(3) NOT NULL, never as one composite column, so the amount stays indexable and comparable in SQL.
- ADR-0011AcceptedRelated: Q5, Q10, Q25
Envelope encryption with per-subject data keys
BRD §9 requires encryption at rest and in transit. NDPA 2023 (BRD §5) governs processing of customer personal data. The build brief requires field-level encryption for identity data, PII access logging, and data-subject export and erasure paths that respect AML retention overrides.
Decision
Three-tier envelope encryption:
- ADR-0012Accepted
UUIDv7 identifiers and API conventions
BRD §9 requires an open REST API for core banking, payment switches and data providers. BRD §9 requires scale to tier-1 volumes. The build brief requires the OpenAPI spec to be the source of truth with a generated TypeScript client, and server-side pagination on every table under the assumption of millions of rows.
Decision
Identifiers: UUIDv7, generated application-side.
- ADR-0013AcceptedRelated: Q27, ADR-0024
Party as a supertype
BRD §8.1 requires identity capture for individuals and corporates plus beneficial ownership for corporate accounts. BRD §8.4 requires screening of "customers and transaction counterparties". BRD §8.7 requires internal lists of "previously exited customers, internally flagged entities".
Decision
party is any natural person or legal entity the platform knows about. customer is a *relationship* between the institution and a party, not a kind of thing.
- ADR-0014Accepted, chunked recovery and alert-audit relay implementedRelated: ADR-0029
Transaction ingest and partitioning
BRD §8.3 requires "support for both API-based real-time monitoring (for PSPs/payment rails) and file/batch ingestion (for core banking exports)". BRD §9 requires scaling to tier-1 volumes without re-architecture. The build brief requires batch ingest to be resumable, idempotent, with per-file reconciliation and quarantine for malformed records.
Decision
One table, two writers. app.transaction is range-partitioned monthly on booking_ts. Both paths write the same rows; nothing downstream knows which arrived how.
- ADR-0015AcceptedRelated: Q6
Fuzzy name matching
BRD §8.4 requires "fuzzy name-matching with configurable match sensitivity to balance false positives against risk". BRD §13 makes the false-positive rate a KPI with a quarter-on-quarter reduction target.
Decision
Match on a party, score on evidence, decide on corroboration.
- ADR-0016Accepted
Rule evaluation
BRD §8.3 requires a "configurable rule library organized by typology, with the ability to enable/disable and tune thresholds per customer segment", covering structuring, rapid movement of funds, dormant account activity and high-risk corridor transfers.
Decision
Split the rule into a window and a predicate.
- ADR-0017Accepted
Alert correlation and de-duplication
BRD §8.3 requires "alert generation with severity scoring and de-duplication of related alerts". BRD §13 makes false-positive reduction a tracked KPI.
Decision
Every alert carries a correlation_key computed at creation. It is derived from configuration, not hardcoded:
- ADR-0018AcceptedRelated: Q1
Report generation and the goAML placeholder
BRD §8.6 requires CTR generation above thresholds and STR generation "in the NFIU goAML XML schema, with manual review before submission".
Decision
Reporting is a mapping layer with a swappable schema, and it refuses to lie about which schema it used.
- ADR-0019AcceptedRelated: Q5, ADR-0011
PII encryption in practice
Phase 0 built the key hierarchy — root key, tenant KEK, per-subject DEK — before any PII existed, because per-subject keys cannot be retrofitted. Phase 1 is where PII actually arrives, so this records which fields are encrypted, which are not, and why.
Decision
Encrypted, under the subject's own DEK:
- ADR-0020Accepted
Vendored UI primitives instead of the shadcn/ui CLI
The mandated stack names "Tailwind + shadcn/ui". What Phase 0 actually built was web/components/ui/primitives.tsx: one hand-written file of composable, Tailwind-styled components in the shadcn idiom, with no components.json, no Radix packages and no class-variance-authority.
Decision
Keep the vendored primitives. Do not adopt the shadcn CLI or Radix.
- ADR-0021Accepted
Background jobs on river, and where its tables live
river has been in the mandated stack and the architecture document since Phase 0 ("Queue | river on Postgres | same — no broker, ever"). It was never added. go.mod has no entry for it, internal/platform/jobs/ and internal/platform/outbox/ are empty directories, and a code comment claiming "the river queue is wired in Phase 1" was wrong when it was written. The worker is a fifteen-minute time.Ticker doing one thing.
Decision
Adopt river, and put its tables in a dedicated river schema.
- ADR-0022AcceptedRelated: Q10, Q21, Q22, Q23, Q24, ADR-0009, ADR-0011, ADR-0021
Kubernetes packaging and worker observation
BRD §6.2 requires environment-agnostic containerised packaging and §10 requires the same release to deploy to Baobyte's cloud or to a client's environment. Kubernetes is half of that requirement and deploy/helm/ has been empty since Phase 0, when the chart was deferred at the gate. The traceability matrix records both rows as PARTIAL with deploy/helm/ (empty) as the evidence, which is an unusually honest citation of nothing.
Decision
A single chart, deploy/helm/baobyte, serving both deployment modes from one artifact, with these four decisions carrying the weight.
- ADR-0023AcceptedRelated: ADR-0006, ADR-0011, ADR-0022, Q10, Q21, Q25, Q30, BRD §6.2, §10
The air-gapped bundle, and the one install step a script is not allowed to guess
The Phase 2 exit criterion is a clean-room install on a bare VM with no internet, completed from documentation alone. ADR-0009 records the *principle* that makes that possible — exactly two required dependencies, everything else degrading to an in-process fallback — and stops there. It says nothing about what is in the artifact, how it is verified on arrival, or what brings the first tenant into existence.
Decision
### 1. The bundle is a single file, and everything in it is checksummed together
- ADR-0024AcceptedRelated: ADR-0005, ADR-0008, ADR-0013,
CDD form definitions and the answer→storage mapping
BRD §7.2-05 requires the CDD/EDD fields and required documents per customer type to be configurable without a deploy. onboarding.cdd_forms already satisfied the configuration half: a registered JSON Schema, a shipped default with per-tier, per-customer-type fields and show_if predicates, and a go_type: onboarding.FormSet declaration in the registration.
Decision
### 1. The FormSet type its own schema already named
- ADR-0025AcceptedRelated: W1c C1, C3, C11, C12
Money on the filing path, and how a mixed-currency set is presented
ADR-0010 decided that money is types.Money{Amount int64; Currency Currency}, that cross-currency arithmetic does not compile, and that currency codes are validated against ISO 4217 with per-currency minor-unit exponents.
Decision
### 1. Money enters a regulatory filing through exactly one door
- ADR-0026AcceptedRelated: Q6, ADR-0001, ADR-0004, ADR-0006, ADR-0014, ADR-0015, ADR-0021
Watchlist maintenance, and where list content is allowed to come from
BRD §8.7 requires internal blacklist and greylist maintenance, bulk import and export of watchlists, and an audit log of all list changes. BRD §8.4.4 requires watchlist update scheduling and §8.4.11 requires version tracking.
Decision
### 1. The internal list is a separate, editable master; the screening index is a snapshot
- ADR-0027AcceptedRelated: Q18, Q29, Q30, Q31, ADR-0001
Analytics owns a published read model, and suppression lives in the query
BRD §8.8 asks for compliance and executive dashboards. BRD §13 names the KPIs: alert false-positive rate, case ageing, SLA breaches, filing volumes and timeliness. BRD §4.1 asks the executive persona for filing volumes, regulatory exposure and deadline tracking.
Decision
Analytics owns two tables and reads nothing else.
- ADR-0028AcceptedRelated: ADR-0001,
The condition tree, the field vocabulary, and what the builder refuses to open
docs/adr/README.md has held this number open since Phase 2 began, with the reason written down: "reserved for the condition tree, its CEL compilation and the field vocabulary… that work is not built: there is no tree type, no condition_tree column, no compiler, no simulator and no rule write API. ADR-0007's amendment depends on it."
Decision
### 1. The field vocabulary is data, and the activation is generated from it
- ADR-0029AcceptedRelated: ADR-0012, ADR-0014, Q7, BRD §8.3, §9
The transaction ingest API: who calls it, what makes a retry safe, and what it does not decide
BRD §8.3 requires "support for both API-based real-time monitoring (for PSPs/payment rails) and file/batch ingestion (for core banking exports)". ADR-0014 designed both paths and the monitoring module implemented both.
Decision
### 1. Ingest permissions are held by machines, and BRD §4.1 has no machine
- ADR-0030AcceptedRelated: ADR-0025, W1c C12, Q1
What a case-derived filing states about its transaction and its account
The CTR template's transaction block is singular: /ctr/transaction/amount_local, /ctr/transaction/date, /ctr/transaction/channel, /ctr/customer/account. A Currency Transaction Report describes *a transaction*.
Decision
The filing states the aggregate, and says that it is an aggregate.
- ADR-0031AcceptedRelated: ADR-0002, ADR-0010, ADR-0014, ADR-0016
A fallback must be distinguishable from a decision, and an identifier must name a row
Four of the open W1c findings turned out to be one idea seen from four sides: the system was storing values that look like decisions and are not.
Decision
### 1. A fallback is acceptable. A fallback indistinguishable from a configured value is not.
- ADR-0032AcceptedRelated: ADR-0001, ADR-0005, ADR-0012,
Where a module's surface becomes reachable, and who is allowed to decide
Phase 2 ran as nine parallel lanes. Three of them — onboarding's CDD write path, analytics, and watchlists — produced complete, tested, green modules that nothing called, because internal/api and internal/app belonged to a single lane and the others were asked to *specify* an HTTP surface rather than to wire one.
Decision
### 1. A module surface is not done until a route reaches it, and a role can hold the permission
- ADR-0033AcceptedRelated: ADR-0018, ADR-0023, Q1, Q17
Real regulatory-schema validation without weakening the air-gapped build
ADR-0018 correctly refused to submit reports generated from invented placeholder schemas, but the implementation had two gaps once a real regulator artifact became available:
Decision
### XSD 1.0 engine and closed capability claim
- ADR-0034AcceptedRelated: ADR-0018, ADR-0033, Q1
Truthful regulatory-submission boundary
ADR-0033 proves that a rendered report matches the exact authoritative XSD and immutable payload. That is necessary evidence about a document, but it is not evidence that the document left Baobyte or reached a regulator. The original Submit service changed the report to submitted immediately after revalidating those local bytes. It invoked no transport and stored no delivery evidence, so the status claimed an event the system had not observed.
Decision
Direct submission fails closed. The legacy Submit operation remains so existing callers receive an explicit report-transport-unavailable conflict, but it cannot write submitted state.