Skip to main content
Baobyte AML Suite
Documentation contents

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

  1. 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).

  2. 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.

  3. ADR-0003AcceptedRelated: Q2, Q3, Q12

    Zitadel for authentication; Baobyte owns authorization

    BRD §8.9 requires MFA and SSO/SAML for enterprise clients, RBAC with granular per-module permissions, and a tenant management console. BRD §7.2 requires that "user roles, permissions, and organizational hierarchy" be configurable with no code by a compliance administrator.

    Decision

    Split them at the natural seam.

  4. 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

  5. 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.

  6. 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.

  7. 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.

  8. 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:

  9. 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.

  10. 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.

  11. 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:

  12. 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.

  13. 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.

  14. 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.

  15. 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.

  16. 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.

  17. 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:

  18. 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.

  19. 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:

  20. 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.

  21. 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.

  22. 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.

  23. 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

  24. 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

  25. 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

  26. 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

  27. 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.

  28. 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

  29. 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

  30. 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.

  31. 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.

  32. 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

  33. 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

  34. 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.