Architecture Overview
bopen is an open banking A2A acquiring platform deployed on AWS. It follows a microservices architecture with a statically-exported Next.js frontend and a Rust payment service, with Keycloak providing FAPI-compliant IAM.
High-level architecture
┌──────────────────────────────────────────────────────────────────────────┐
│ AWS Amplify (5 apps) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Checkout │ │ Acquirer │ │ Product │ │ Payer Hub │ │
│ │ Widget │ │ Portal │ │ Owner │ │ my.dev. │ │
│ │ checkout. │ │ acquirer. │ │ Portal │ │ bopen.tech │ │
│ │ dev.bopen.. │ │ dev.bopen.. │ │ admin.dev. │ │ /payer/* │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼────────────────┼──────────────── ┼──────────────── ┼─────────┘
│ HTTPS │ HTTPS + KC JWT │ bopen-platform │ HMAC JWT
▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Payment Service (Rust / ECS Fargate) │
│ /api/v1/payments/* /api/v1/portal/* /api/v1/admin/* │
│ /api/v1/payer/auth/* /api/v1/payer/* │
└────────────────────┬─────────────────────────┬───────────────────────────┘
│ │ Admin API
▼ ▼
┌────────────────────┐ ┌─────────────────────────────┐
│ RDS PostgreSQL │ │ Keycloak (ECS Fargate) │
│ bopen.* │ │ auth.dev.bopen.tech │
│ keycloak_core.* │ │ merchant realms │
└────────────────────┘ │ bopen-platform realm │
└─────────────────────────────┘
│ SMTP
▼
┌──────────────┐
│ AWS SES │
│ DKIM verified│
│ dev.bopen. │
│ tech domain │
└──────────────┘
dev.bopen.tech deployment — verified liveAll components above are deployed and live as of July 2026. SES is deployed and DKIM-verified on dev.bopen.tech. All five Amplify apps are live with custom domains. Keycloak runs as a single task (not 2-task HA) due to a CloudFormation ECS resource-provider timeout constraint — see IAM deployment notes.
Components
Checkout Widget (apps/web/public/widget/)
A zero-dependency vanilla JS application served inside a sandboxed iframe. Merchants embed it with a single <script> tag. Features:
- Loader (
/v1/checkout.js): readsdata-checkout-token, injects iframe, relayspostMessageevents - Widget (
/widget/index.html): 5 screen states, 16 UK banks, QR code (desktop) / deep link (mobile), 2500 ms polling,BOPEN_PAYMENT_SUCCESS/BOPEN_PAYMENT_FAILEDevents - Bundle: ~12 KB gzipped (well within 20 KB target)
- Branding: merchant colour scheme, logo, and font loaded at runtime from
GET /api/v1/portal/branding
See Checkout Widget.
Consumer Checkout (apps/web/src/app/checkout/)
Next.js multi-step flow: order summary → bank selection → SCA handoff → processing → receipt with fee breakdown and end_to_end_id.
Product Owner Portal (apps/web/src/app/admin/)
Platform super-admin control room at admin.app.bopen.tech:
- Login —
bopen-platformKeycloak realm, PKCE S256 OIDC; roles: OWNER / OPS / SUPPORT - Overview — global merchant KYB status matrix; live counts (approved/pending/suspended/held)
- Merchants — KYB approval / suspension, payout hold / release
- Disputes — cross-tenant dispute queue; resolve in favour of merchant or payer; escalate to OBL DMS
- ASPSP Health — conversion rate and latency heatmap per UK bank
Setup: run ./infra/cloudformation/scripts/deploy-platform-realm.sh then invite-platform-user.sh.
Acquirer Portal (apps/web/src/app/portal/)
B2B merchant dashboard at acquirer.dev.bopen.tech:
- Self-registration (
/portal/register) — provisions Keycloak realm + merchant DB record (KYB: PENDING) - Login — per-merchant Keycloak realm, PKCE S256 OIDC flow
- Transactions — searchable ledger with slide-out audit trail, refund button, raise-dispute button
- Settlements — balance cards, payout history, CSV and PDF export
- Disputes — view customer complaints, submit evidence, track resolution status
- Widget Branding — colour picker, logo URL, font selector with live widget preview; stored as
branding_configJSONB on the merchant record - Team — invite staff with ADMIN / DEVELOPER / FINANCE roles; remove members
- Developer Tools — webhook registration, signing secret, event delivery history
Payer Hub (apps/web/src/app/payer/)
Consumer consent dashboard at my.dev.bopen.tech — no password required:
- Magic-link auth — enter email or phone; receive a one-time link (10-min expiry) via SES/SMS; exchange for a 15-min HMAC session JWT stored in
sessionStorage - Dashboard — active VRP mandates + last 50 payments across all merchants
- Payment receipt — full details + raise-dispute form (OBL CEG requirement)
- Mandate detail — spending caps, expiry, prominent Cancel Permission kill-switch (fires
DELETE /api/v1/payer/consents/{id})
Payment Service (services/payment-service)
Rust / Axum microservice on ECS Fargate:
- Payment lifecycle (create consent → intent → SCA → settle → refund/partial-refund)
- Dispute lifecycle: open → under-review → resolved (webhooks on state changes)
- Fee calculation (percentage + flat, snapshotted at execution time)
end_to_end_idgeneration (ISO 20022, 35-char, BACS/FPS compliant)- JWT validation: Keycloak JWKS cached by issuer URL,
azpclaim → merchant lookup;validate_aud = false(Keycloak tokens carryaud: "account", identity done viaazp) - Platform JWT validation:
bopen-platformrealm tokens, role extracted fromrealm_access.roles - Payer JWT validation: HMAC-signed (HS256), issued by the service itself after magic-link verification
- Keycloak Admin API: realm provisioning, user management,
bopen-platformrealm setup - Append-only audit ledger with actor tracking (
MERCHANT_API,BANK_CALLBACK)
Keycloak IAM
FAPI-compliant IAM engine on ECS Fargate at auth.dev.bopen.tech:
- Merchant realms — one per acquirer, PKCE
bopen-portalclient, PS256, 15-min tokens bopen-platformrealm — internal staff; roles: OWNER, OPS, SUPPORT;bopen-adminPKCE client- Single task in
bopen-dev(HA requires resolving CFN ECS resource-provider timeout — see IAM) - SES SMTP fully configured and DKIM-verified on
dev.bopen.tech
See Identity & Access Management.
Database (database/)
Versioned PostgreSQL migrations on RDS (001–013):
- B2B payments (
bopen.*): merchants (+branding_config,payout_held), payment_consents, payment_intents (+refunded_amount), payment_events, webhook_deliveries - Dispute engine (
bopen.*): disputes (reason/status enums, evidence fields, resolution tracking) - Payer auth (
bopen.*): payer_tokens (magic-link SHA-256 hash, 10-min expiry) - Platform staff (
bopen.*): platform_users (keycloak_user_id, role enum) - IAM (
keycloak_core.*): Keycloak session/realm state (separate schema, separate DB user)
See Database Schema.
Infrastructure (infra/cloudformation/)
13 nested CloudFormation templates — all deployed in dev.bopen.tech account:
| Template | Deployed |
|---|---|
vpc.yaml, ecs.yaml, ecr.yaml, rds.yaml, logging.yaml | ✓ |
api-gateway.yaml, keycloak.yaml, ses.yaml | ✓ |
amplify-portal.yaml, amplify-web.yaml, amplify-docs.yaml | ✓ |
amplify-admin.yaml (Product Owner Portal) | ✓ |
amplify-payer.yaml (Payer Hub) | ✓ |
See AWS Infrastructure.
Monorepo layout
bopen/
├── apps/
│ └── web/ # Next.js (static export — 5 Amplify apps)
│ └── public/
│ ├── v1/checkout.js # Widget loader
│ └── widget/index.html # Embeddable widget
│ └── src/
│ ├── app/
│ │ ├── admin/ # Product Owner Portal (/admin/*)
│ │ │ ├── login/
│ │ │ ├── overview/
│ │ │ ├── merchants/
│ │ │ ├── disputes/
│ │ │ └── aspsp-health/
│ │ ├── portal/ # Acquirer Portal (/portal/*)
│ │ │ ├── transactions/
│ │ │ ├── settlements/
│ │ │ ├── disputes/
│ │ │ ├── branding/
│ │ │ ├── team/
│ │ │ └── developers/
│ │ ├── payer/ # Payer Hub (/payer/*)
│ │ │ ├── verify/
│ │ │ ├── dashboard/
│ │ │ ├── payments/detail/
│ │ │ └── consents/detail/
│ │ └── checkout/ # Consumer checkout
│ └── lib/
│ ├── keycloak.ts # Merchant portal auth helpers
│ ├── adminAuth.ts # Platform admin auth helpers
│ └── payerAuth.ts # Payer magic-link auth helpers
├── services/
│ └── payment-service/ # Rust Axum service
│ └── src/
│ ├── auth.rs # JWKS cache + merchant/platform/payer JWT validation
│ ├── keycloak_admin.rs # Admin API client + platform realm setup
│ ├── handlers.rs # All HTTP handlers (payments, portal, admin, payer)
│ └── state.rs
├── database/
│ └── scripts/
│ ├── migrations/ # 001–013 versioned schema
│ └── seeds/ # Dev/test data
├── infra/cloudformation/
│ ├── templates/ # 13 nested stack templates
│ └── scripts/
│ ├── deploy.sh # Full stack deploy
│ ├── deploy-admin-amplify.sh # Product Owner Portal
│ ├── deploy-portal-amplify.sh # Acquirer Portal
│ ├── deploy-payer-amplify.sh # Payer Hub
│ ├── deploy-platform-realm.sh # bopen-platform Keycloak realm
│ ├── invite-platform-user.sh # Invite internal staff
│ ├── generate-payer-session-secret.sh
│ └── generate-ses-smtp-credentials.sh
└── docs/ # This documentation site
End-to-end payment flow
1. Acquirer registers via /portal/register
→ Keycloak realm provisioned, merchant DB record created (KYB: PENDING)
→ SES email invite sent to admin (live on dev.bopen.tech)
2. Platform operator approves KYB via admin.dev.bopen.tech/admin/merchants
→ kyb_status = APPROVED; merchant can now process payments
3. Merchant calls POST /api/v1/payments with Keycloak Bearer token
→ JWT validated (JWKS), merchant resolved via azp claim
→ KYB + payout-hold checks enforced
→ payment_consent + payment_intent created; fee breakdown snapshotted
→ Returns redirect_url, end_to_end_id, gross/fee/net amounts
4. Consumer completes SCA (via Checkout Widget or direct checkout)
→ Payment transitions: AwaitingAuthorisation → Authorised → Settled
→ payment_events audit trail updated (MERCHANT_API / BANK_CALLBACK)
5. BOPEN_PAYMENT_SUCCESS postMessage fires to merchant page
→ Merchant webhook receives settlement notification (payment.settled)
6. Payer raises dispute (optionally)
→ POST /api/v1/payer/payments/{id}/dispute from Payer Hub
→ dispute.opened webhook sent to merchant
→ Merchant submits evidence via /portal/disputes
→ Platform operator resolves via admin.dev.bopen.tech/admin/disputes
→ dispute.resolved webhook confirms outcome