Maker-checker enforcement engine¶
| ID | MOD-168 |
| System | SD07 |
| Repo | bank-platform |
| Build status | Deployed |
| Deployed | Yes |
| Last commit | f87190eaec0e0aa6710df8ebfca670e4a90d1ef9 |
What it does¶
MOD-168 is the platform-wide maker-checker enforcement engine. It provides a single shared service for all back-office modules that need four-eyes authorisation on consequential state-changing commands. Any module that needs a second person to approve an action before it executes delegates to MOD-168 rather than building its own proposal table, approval workflow, and audit trail.
The module exposes four APIs: submit a proposal, approve a proposal, reject a proposal, and query open proposals. It stores every proposal, decision, and expiry in an immutable log and writes each decision to MOD-047 (agent action logger) and MOD-048 (system decision log). No proposal may be self-approved — this constraint is enforced at the database layer and cannot be bypassed by any role, environment variable, or configuration flag.
Why it exists¶
Before ADR-059, maker-checker was implemented per-module. MOD-127 (product configuration panel) has its own app.product_config_proposals table. MOD-140 (chart of accounts) has its own core.gl_account_proposals table. Both were built correctly for their scope. The problem is that each new back-office module with write operations would independently implement the same pattern, creating fragmented audit surfaces — separate proposal tables in separate schemas with no central query, no cross-module approval queue, and no single view for compliance reporting.
ADR-059 resolved this by establishing MOD-168 as the single enforcement point. New modules built after ADR-059 must call MOD-168 rather than creating their own proposal tables. Existing implementations (MOD-127, MOD-140) remain in place as v1 and are flagged for migration to MOD-168 in their respective v2s.
Command classification¶
Every command that registers with MOD-168 is classified at one of two risk tiers. The owning module declares the tier when it registers its command type. The classification register is reviewed and approved by the compliance team.
TIER-2 — Medium risk, reversible. Product configuration changes, fee schedule updates, interest rate adjustments, payment limit overrides. MOD-168 requires a second approver before execution but applies no review window — the approver may act immediately after the proposal is submitted.
TIER-3 — High risk, irreversible or regulatory impact. Sanctions holds, account closures, GL account creation and modification, credit limit overrides below policy floor, customer risk rating downgrades, hardship arrangement applications. MOD-168 requires a second approver AND enforces a minimum 15-minute review window between proposal submission and approval. This window gives the reviewing officer time to investigate context before committing. The review window is configurable per command type via AppConfig.
API surface¶
POST /checker/proposals Submit a pending command for review
GET /checker/proposals List open proposals (paginated; filterable by domain, command_type, tier, status)
GET /checker/proposals/{id} Retrieve a single proposal with full context
POST /checker/proposals/{id}/approve Second-party approval (reviewer ≠ proposer enforced)
POST /checker/proposals/{id}/reject Second-party rejection with mandatory reason
The proposal payload carries the full command context — the command type, the target entity, the proposed new state, a human-readable summary, and the proposer's staff ID. The approver supplies only a confirmation and an optional note. The command context is stored verbatim in the proposal record and cannot be altered after submission.
Data model¶
CREATE TABLE platform.checker_proposals (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
command_type text NOT NULL, -- e.g. 'PRODUCT_RATE_CHANGE', 'GL_ACCOUNT_CREATE', 'SANCTIONS_HOLD'
command_domain text NOT NULL, -- owning system domain, e.g. 'SD01', 'SD02', 'SD07'
tier text NOT NULL CHECK (tier IN ('TIER-2', 'TIER-3')),
status text NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED', 'WITHDRAWN')),
target_entity_type text NOT NULL, -- e.g. 'product', 'gl_account', 'party'
target_entity_id text NOT NULL, -- entity primary key
command_payload jsonb NOT NULL, -- full command context; immutable after insert
summary text NOT NULL, -- human-readable one-line description for the approval queue
proposed_by text NOT NULL, -- staff_id of the proposing officer
proposed_at timestamptz NOT NULL DEFAULT now(),
review_window_until timestamptz , -- earliest approval time for TIER-3; NULL for TIER-2
reviewed_by text , -- staff_id of the reviewer; set on APPROVED or REJECTED
reviewed_at timestamptz ,
review_note text , -- optional reviewer note
expires_at timestamptz NOT NULL, -- proposal expires if not actioned (configurable per command_type, default 48h)
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ', 'AU')),
trace_id uuid NOT NULL,
idempotency_key text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
DB-level invariant: CHECK (proposed_by != reviewed_by) — enforced at the database layer. No role or configuration can override this constraint. This is the unconditional self-approval block.
Immutability: command_payload, proposed_by, proposed_at, and tier are set at INSERT and never updated. The status field transitions PENDING → APPROVED / REJECTED / EXPIRED / WITHDRAWN via a Cat 2 trigger that allows only these forward transitions and prevents any reversal.
Indexes:
- idx_checker_proposals_status on (status, expires_at) WHERE status = 'PENDING'
- idx_checker_proposals_proposed_by on (proposed_by, proposed_at DESC)
- idx_checker_proposals_command_domain on (command_domain, status, proposed_at DESC)
- idx_checker_proposals_target on (target_entity_type, target_entity_id) WHERE status = 'PENDING'
Integration with MOD-062 (TIER-3 review window)¶
For TIER-3 commands, MOD-168 uses a Step Functions task-token pause/resume pattern (MOD-062 FR-295): the proposal Lambda submits the command to a Step Functions state machine, then suspends. The state machine holds a task token. When the reviewer approves via the MOD-168 approve API, MOD-168 resumes the Step Functions execution with the task token. If the review window has not elapsed, the approve API returns 422 REVIEW_WINDOW_NOT_ELAPSED. If the proposal expires before approval, the state machine times out and records an EXPIRED status. TIER-2 commands do not use Step Functions — the approve API directly executes the command and writes the APPROVED record synchronously.
Approval queue for back-office operators¶
The MOD-168 query API powers the unified approval queue in the back-office app (SD08). Operators with checker role see all PENDING proposals across all command domains they are authorised to review. The queue is filterable by domain, tier, command type, and age. Each proposal shows the full command context, the proposed-by officer, the time since submission, and (for TIER-3) the review window countdown.
Self-approval block test requirement¶
Every module that integrates with MOD-168 must include a policy test (tests/policy/self-approval-blocked.ts) that verifies the 422 SELF_APPROVAL_FORBIDDEN response when the same staff ID attempts to both submit and approve a proposal. This test must pass in CI before any integration with MOD-168 is considered complete.
Migration path for MOD-127 and MOD-140¶
MOD-127 and MOD-140 each maintain their own proposal tables and approval workflows as v1 implementations. In their respective v2 builds, the proposal table is deprecated and the approval flow is replaced with MOD-168 API calls. The MOD-168 command type for product configuration changes is PRODUCT_RATE_CHANGE / PRODUCT_TERM_CHANGE / PRODUCT_FEE_CHANGE; for GL account changes it is GL_ACCOUNT_CREATE / GL_ACCOUNT_MODIFY. Historical proposal records from the v1 tables are archived to cold storage before the v1 tables are dropped.
Module dependencies¶
Depends on¶
| Module | Title | Required? | Contract | Reason |
|---|---|---|---|---|
| MOD-047 | Agent action logger | Required | — | Every proposal submission, approval, rejection, and expiry is logged to the agent action logger as an immutable audit record. |
| MOD-048 | System decision log | Required | — | Maker-checker approval and rejection decisions are system decisions — each must be recorded in the immutable system decision log with the full command context and reviewer identity. |
| MOD-062 | Workflow orchestration engine | Optional | — | Complex TIER-3 commands that require a review window (minimum 15-minute lock before approval) use MOD-062 Step Functions task-token pause/resume; TIER-2 commands are synchronous and do not require MOD-062. |
| MOD-103 | Neon database platform bootstrap | Required | — | Neon database and schema provisioned by MOD-103 must exist before this module can read or write Postgres (checker_proposals table). |
| MOD-104 | AWS shared infrastructure bootstrap | Required | — | AWS shared infrastructure provisioned by MOD-104 is required before this module can be deployed. |
Required by¶
| Module | Title | As | Contract |
|---|---|---|---|
| MOD-175 | Model change control & re-approval workflow | Hard dependency | — |
| MOD-177 | SD06 risk dashboard renderer | Hard dependency | — |
Policies satisfied¶
| Policy | Title | Mode | How |
|---|---|---|---|
| GOV-003 | Three Lines of Defence Policy | CHECKER |
The enforcement engine is the platform-wide implementation of the three-lines-of-defence second-line control — every consequential back-office command above TIER-1 requires a second authorised reviewer before execution, with self-approval blocked at the database layer. |
| DT-012 | Ledger Data Contracts & Event Publication Policy | LOG |
Every proposal, approval, rejection, and expiry is written to the system decision log (MOD-048) and the agent action logger (MOD-047) as an immutable audit record, satisfying the ledger data contracts and event publication audit obligation. |
| GOV-005 | Financial Accountability Regime (FAR) Policy | CHECKER |
FAR accountability obligations require that material operational decisions are attributable to named individuals — the maker-checker record provides the named proposer and named approver for every TIER-2 and TIER-3 command. |
Capabilities satisfied¶
(No capabilities mapped)
Part of SD07 — Data Platform & Governance Infrastructure
Compiled 2026-05-22 from source/entities/modules/MOD-168.yaml