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