Skip to content

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