Skip to content

MOD-049 — Open banking consent management (technical design)

Module: MOD-049 System: SD08 (bank-app) Repo: bank-app FR scope: FR-313, FR-314, FR-315, FR-316, FR-669, FR-670, FR-671, FR-672 NFR scope: NFR-010, NFR-011, NFR-024 Policies satisfied: PRI-001 (GATE), CON-007 (GATE), AML-010 (LOG) Dependencies: MOD-068 (Built — owns access.user_identities), MOD-052 (Built — workspace lib for staff-training-ack gate), MOD-044 (Built), MOD-103 (Built), MOD-104 (Built) Feature dependency (UI): MOD-069 customer app shell — not yet built; FR-672 partial-met Date: 2026-05-07


Objective

Platform-wide consent store of record. Captures, validates, refreshes, amends, expires, and revokes both jurisdiction-aware open banking consents (CDR, Payments NZ, FAPI 2.0) and the simpler customer-app consents (privacy, marketing, terms). Exposes a sub-50ms synchronous validation endpoint that future MOD-061 (gateway) and MOD-084 (data recipient) will call on every TPP request, and ships a requireConsent() workspace library that downstream BFFs (MOD-071 payment initiation, MOD-073 documents, MOD-051 automation rules) call before activating any consented feature.

Architectural decisions (scope review, ratified 2026-05-07)

AD Ruling
1 Library canonical, HTTP /ob-consents/validate shim
2 FR-672 UI deferred to MOD-069 — partial-met in v1
3 v1 profiles: au_cdr, nz_payments_nz, generic_fapi2 (UK + EU out of footprint)
4 CDR / Payments NZ are simulated in-app flows in v1; real ACCC / API Centre integration is a future module boundary
5 app.ob_third_parties stub registry; tests seed TPPs directly via SQL
6 Hourly EventBridge-scheduled expiry sweeper; the 50ms validation endpoint enforces expiry on every read so sub-hour precision is not a gap
7 Internal scope enum (8 entries — accounts:read, accounts:read:balances, accounts:read:details, transactions:read:90d, transactions:read:full-history, payees:read, payments:initiate, customer-profile:read); per-profile mappings
8 requireConsent() workspace library (mirrors MOD-052's enforce())
9 AML-010 LOG via app.staff_training_acks + a single POST endpoint gated by MOD-052 enforce(); v1 stub
10 Validation endpoint ships now even though MOD-061/MOD-084 don't yet exist — load-bearing GATE
11 MOD-063 stop-gap: emit bank.app.consent_withdrawn and log notification_dispatch_pending; MOD-063 wires the user-facing notification when built
12 CON-007 / AML-010 wiki MD content discrepancy — orchestrator is fixing the policy MDs separately
13 Reuse MOD-068 V006 app.idempotency_keys table; MOD-049 namespaces keys with mod049: prefix
14 Customer JWT (bank-customers-*) custom:party_id claim is the authenticated party identifier

Stacks

MOD-049-open-banking-consent/
├── infra/
│   ├── functions.ts          8 Lambdas (7 HTTP + 1 EventBridge cron), x86_64
│   ├── api.ts                HTTP API v2 — 7 routes
│   ├── audit-log.ts          /aws/bank-app/consent-events-{env}, 90d hot
│   ├── scheduled-rule.ts     hourly EventBridge cron → consent-expiry-sweeper
│   ├── ssm-outputs.ts        12 SSM parameters
│   └── index.ts
├── src/
│   ├── lib/
│   │   ├── require-consent.ts ★ canonical workspace gate
│   │   ├── consent-cache.ts   60s TTL per FR-316/670
│   │   ├── profile-schemas.ts Ajv per-profile validators (CON-007)
│   │   ├── scope-mapping.ts   internal scope enum + per-profile mappings (AD-7)
│   │   ├── audit.ts           Postgres-first writer (AD-7 mirrored from MOD-052)
│   │   ├── idempotency.ts     reuse app.idempotency_keys with mod049: prefix
│   │   ├── errors.ts, logger.ts, trace.ts, db.ts
│   ├── services/
│   │   ├── consent-service.ts   simple per-purpose consents (app.consents)
│   │   ├── ob-consent-service.ts open banking lifecycle (app.ob_consents + events)
│   │   └── event-publisher.ts    bank-app bus (4 events)
│   ├── handlers/
│   │   ├── grant-ob-consent.ts        POST   /ob-consents
│   │   ├── validate-ob-consent.ts     POST   /ob-consents/validate ★
│   │   ├── amend-ob-consent.ts        PATCH  /ob-consents/{id}
│   │   ├── grant-simple-consent.ts    POST   /consents
│   │   ├── list-consents.ts           GET    /consents
│   │   ├── revoke-consent.ts          DELETE /consents/{id}
│   │   ├── staff-training-ack.ts      POST   /staff-training-acks (MOD-052 gated)
│   │   ├── consent-expiry-sweeper.ts  EventBridge scheduled (hourly)
│   │   └── _shared.ts
│   └── index.ts                       workspace library exports
├── db/migrations/V001..V005.sql + rollbacks
└── tests/  unit ≥80% (97% achieved), contract, policy (static), integration FR/policy/infra

Data model summary

Table Purpose Mutability
app.consents Simple per-purpose consents (privacy, marketing, terms, OPEN_BANKING summary) Mutable; withdrawals are NEW rows (ADR-048 §consents)
app.ob_consents Full TPP arrangement (FR-669) Mutable status flips; amendments insert a new row + supersede
app.ob_consent_events Append-only consent lifecycle audit log Immutable (ADR-048 Cat 1)
app.ob_third_parties Local TPP registry stub Mutable
app.staff_training_acks AML-010 LOG channel Immutable (ADR-048 Cat 1)

SSM outputs

Path Value
/bank/{env}/mod049/consent-api/url API Gateway URL
/bank/{env}/mod049/consent-api/id API Gateway ID
/bank/{env}/mod049/grant-ob-consent/fn-arn Lambda ARN
/bank/{env}/mod049/validate-ob-consent/fn-arn Lambda ARN (consumer: future MOD-061/084)
/bank/{env}/mod049/amend-ob-consent/fn-arn Lambda ARN
/bank/{env}/mod049/grant-simple-consent/fn-arn Lambda ARN
/bank/{env}/mod049/list-consents/fn-arn Lambda ARN
/bank/{env}/mod049/revoke-consent/fn-arn Lambda ARN
/bank/{env}/mod049/staff-training-ack/fn-arn Lambda ARN
/bank/{env}/mod049/consent-expiry-sweeper/fn-arn Lambda ARN
/bank/{env}/mod049/consent-events-log/group-arn CW Logs group ARN
/bank/{env}/mod049/consent-events-log/group-name CW Logs group name

Events published (bank-app bus)

Event When Required fields Consumer
bank.app.consent_granted Grant succeeds trace_id, party_id, consent_id, consent_type, jurisdiction_profile?, third_party_id? SIEM, future MOD-063
bank.app.consent_amended Amendment supersedes prior trace_id, party_id, consent_id, added_scopes, removed_scopes SIEM
bank.app.consent_withdrawn Customer or staff revokes trace_id, party_id, consent_id, consent_type, withdrawn_via future MOD-063 (FR-672 notification)
bank.app.consent_expired Hourly sweeper transitions status trace_id, party_id, consent_id SIEM, churn analytics

Test evidence

pnpm test:unit
 96 tests passed in 13 files
 Coverage: 97.37% lines / 100% functions / 86.28% branches (gates: 80/80/75)
FR / Policy Pass
FR-313 plain language consent presentation partial-met (UI follows MOD-069)
FR-314 immutable audit log ✓ (live trigger rejects UPDATE/DELETE)
FR-315 consent expiry sweeper + event ✓ (hourly cron + read-path enforcement)
FR-316 60s revocation propagation ✓ (TTL cache + read-path re-check)
FR-669 jurisdiction-profile-aware row
FR-670 50ms validation endpoint ✓ (live latency probe; 50ms is in-region production target)
FR-671 per-profile native flows ✓ via JSON-Schema-validated payloads (au_cdr, nz_payments_nz, generic_fapi2)
FR-672 customer consent UI partial-met — BFF complete, UI follows MOD-069
PRI-001 GATE — static + live
CON-007 GATE — static + live ✓ (au_cdr profile-schema rejection asserted)
AML-010 LOG — static + live immutability
NFR-024 audit log immutability
Coverage gate ≥80% ✓ (97.37%)

Cross-module IAM stop-gap

AuditWriter's CloudWatch Logs branch fails closed via stderr if BankAppRole lacks logs:CreateLogStream / logs:PutLogEvents on /aws/bank-app/*. The Postgres app.ob_consent_events write is the durable compliance record and is unaffected. Tracked under the existing MOD-104-auth-events-log-iam-grant.handoff.md.

NZ Open Banking policy gap (flagged by orchestrator at scope review)

There is no NZ Open Banking / Payments NZ consent policy in the registry. CON-007 is AU-only (CDR). MOD-049's nz_payments_nz v1 profile has no corresponding policy entry; the Compliance team needs to author one before the Payments NZ profile goes to production. Not a v1 blocker.

Operational notes

  • Audit hierarchy (AD-7): page on Postgres app.ob_consent_events insert failures, not on audit_log_cw_write_failed stderr.
  • 60s consent-cache TTL (AD-4): downstream callers experience worst-case 60s staleness on warm Lambdas; cold-start always loads fresh.
  • The hourly expiry sweeper (AD-6) drives the persistent expired state transition + the bank.app.consent_expired event. The synchronous validation endpoint already excludes expired consents on every read, so sub-hour precision is satisfied on the read path.
  • v1 CDR / Payments NZ are simulated in-app flows (AD-4); real regulator integration is a future module boundary.
  • MOD-063 stop-gap (AD-11): the revoke handler logs notification_dispatch_pending and emits bank.app.consent_withdrawn. MOD-063 will consume the event and dispatch the user notification when built (FR-672).

Files of interest