pulse-banking
Audience
PLANA staff. The PSD2 / open-banking layer between Bulgarian banks and each tenant's accounting workflow.
pulse-banking is the unified PSD2 (Berlin Group) connector for every bank we support. It handles consent flows, fetches accounts and transactions, and exposes a stable PLANA-internal API regardless of which bank is being read.
Stack
| Property | Value |
|---|---|
| Repo | git.planapulse.com/plana-pulse/pulse-banking |
| Language | JavaScript ESM |
| Framework | Fastify v5 |
| PSD2 framework | Berlin Group NextGenPSD2 v1.3.6 |
| Image base | node:20-alpine |
| Namespace | pulse-banking |
| Port | 3200 |
| Database | pulse_banking on pg01 |
| Cache | Redis at redis.redis:6379 |
Supported banks
Ten adapters, each implementing the same internal interface:
| Bank | Adapter ID | PSD2 endpoint |
|---|---|---|
| Revolut | revolut | https://b2b.revolut.com/api/1.0 |
| UniCredit Bulbank | unicredit | https://psd2-prod.unicreditbulbank.bg/v1 |
| United Bulgarian Bank (UBB) | ubb | https://psd2.ubb.bg/api/v1 |
| DSK Bank | dsk | https://psd2.dskbank.bg/v1 |
| Postbank | postbank | https://psd2.postbank.bg/v1 |
| Raiffeisenbank | raiffeisen | https://psd2.raiffeisen.bg/v1 |
| First Investment Bank (FIBank) | fibank | https://api.fibank.bg/psd2/v1 |
| Central Cooperative Bank (CCB) | ccb | https://psd2.ccbank.bg/v1 |
| Bulgarian-American Credit Bank (BACB) | bacb | https://psd2.bacb.bg/v1 |
| Mock | mock | Internal — for testing |
All adapters share a common shape:
adapters/
base.js # Berlin Group base implementation
revolut.js # bank-specific overrides
unicredit.js
ubb.js
dsk.js
postbank.js
raiffeisen.js
fibank.js
ccb.js
bacb.js
mock.js # for tests and demosA new bank is added by writing a new adapter file plus any per-bank quirks the upstream PSD2 implementation has — different consent renewal cadences are common.
API surface
GET /banking/adapters list available adapters
POST /banking/:bankId/consent start a consent flow → returns URL + consentId
GET /banking/:bankId/consent/:consentId check consent status (valid / pending / expired)
GET /banking/:bankId/accounts list accounts under the active consent
GET /banking/:bankId/accounts/:accountId/transactions?from=…&to=…
GET /banking/:bankId/accounts/:accountId/balance
GET /healthz liveness
GET /readyz readiness (Redis + DB)Authentication: X-API-Key header per workspace. Calls are scoped to the workspace via the key lookup; a workspace cannot list accounts under another workspace's consent.
Consent flow
The Berlin Group consent flow is the entry point to every bank:
1. Customer in BOS → Banking → "Connect Revolut" → BOS calls:
POST /api/banking/revolut/consent (proxied through pulse-account-api)
2. pulse-banking returns: { consentId, authUrl, expiresIn }
3. BOS redirects the customer's browser to authUrl
(the bank's PSD2 SCA consent page)
4. Customer authenticates with their bank, approves the scope
(typically: read accounts + read transactions, 90-day TTL)
5. Bank redirects back to: https://my.planapulse.ai/banking/callback?
consentId=…&bankId=revolut&status=valid
6. pulse-account-api hits pulse-banking GET /banking/revolut/consent/:id
to confirm; updates the workspace's banking state to "connected"
7. BOS shows the accounts list, fetched via:
GET /api/banking/revolut/accountsConsents are bank-specific in detail (some require SCA every 90 days, some every 180; some allow consent renewal, some require a full re-auth). The adapter encapsulates these differences.
Storage
Table (pulse_banking DB) | Purpose |
|---|---|
consent | One row per workspace × bank — consent_id, status, expires_at, scope JSON |
account | Cached account metadata under a consent |
transaction | Cached transactions (deduplicated, indexed by transaction_id) |
The transaction cache is a write-through cache: pulses pulled from the bank are immediately persisted, then served from the local table on subsequent reads. The cache TTL is 15 minutes for balances and 24 hours for historical transactions. Force-refresh is exposed via the ?refresh=true query parameter.
Configuration
| Env var | Purpose |
|---|---|
DB_URL | Postgres connection |
REDIS_URL | Redis connection |
BANK_CREDENTIALS_REVOLUT_CLIENT_ID (etc.) | One per bank — credentials live in SOPS |
BANK_CREDENTIALS_REVOLUT_CLIENT_SECRET | (etc.) |
PUBLIC_CALLBACK_URL | https://my.planapulse.ai/banking/callback |
Per-bank credentials are issued by each bank's PSD2 developer portal. PLANA holds one TPP (Third-Party Provider) certificate per bank for the mTLS handshake.
TPP certificates
PSD2 requires TPP authentication via QWAC (Qualified Website Authentication Certificate) and QSEALC (Qualified Electronic Seal Certificate). PLANA holds:
- One QWAC issued by a Bulgarian QTSP (qualified trust service provider)
- One QSEALC for request signing
Both certs are mounted into the pulse-banking pod from a SOPS-managed Secret. Renewal is annual; the procedure is in infra/docs/runbooks/tpp-cert-renewal.md.
Common operational tasks
Verify a consent is valid
kubectl -n pulse-banking exec deploy/pulse-banking -- \
psql "$DB_URL" -c "SELECT bank_id, status, expires_at FROM consent WHERE workspace_id='<workspace>' ORDER BY expires_at DESC"Force-refresh a workspace's account list
curl -sX GET "https://my.planapulse.ai/api/banking/revolut/accounts?refresh=true" \
-H "Authorization: Bearer <workspace-pa_live-key>"Diagnose a bank-side outage
If a bank's PSD2 endpoint is down, expect 502 / 504 responses from the adapter. The bank-status dashboard in Grafana tracks each adapter's last successful call.
A multi-hour bank outage is communicated to customers via their workspace Matrix room — banks rarely give SLA notifications, so we detect from our own probes.
Rotate a bank's PSD2 credentials
- Bank's PSD2 portal → generate new client_id / secret
- Update SOPS
pulse_banking.bank_<bankid>_client_secret - Reconcile the secret into the namespace
- Restart the deployment
Where to read more
- BOS → Banking — customer-facing flow
- PLANA Business Cloud → Bank reconciliation — how transactions flow into the accounting app
- pulse-account-api — the proxy layer
- Source:
infra/k8s/pulse-banking/