Connecting a Bank Account
Connecting a Bank Account
The platform uses TrueLayer Open Banking to securely connect your bank accounts and import transactions. The OAuth flow is handled entirely server-side via two dedicated API routes.
How the connection flow works
1. Initiate — GET /api/bank/authorise
When you click Connect Bank Account, the app redirects you to /api/bank/authorise. This route:
- Verifies you are authenticated and resolves your active organisation.
- Generates a random CSRF nonce and encodes it with your
userIdandorgIdinto a base64urlstatestring. - Stores the state in a secure
HttpOnly,SameSite=Laxcookie with a 10-minute expiry. - Redirects your browser to TrueLayer's consent screen.
Rate limit: You can initiate a connection a maximum of 10 times per minute. Exceeding this redirects to
/dashboard/bank?error=rate_limited.
Error redirects from /api/bank/authorise
| Condition | Redirect target |
|---|---|
| Not authenticated | /sign-in |
| Rate limit exceeded | /dashboard/bank?error=rate_limited |
| No organisation membership found | /dashboard/bank?error=missing_org |
| TrueLayer environment variables not configured | /dashboard/bank?error=config_error |
2. Callback — GET /api/bank/callback
After you grant (or deny) consent on TrueLayer's screen, TrueLayer redirects your browser back to /api/bank/callback. This route:
- CSRF validation — Compares the
statequery parameter against the value in thetl_oauth_statecookie. Mismatches are rejected immediately. - State decoding — Extracts
userIdandorgIdfrom the state payload. No active session is required at this point. - Token exchange — Exchanges the one-time auth code for TrueLayer access and refresh tokens.
- Account fetch — Lists the connected bank accounts to retrieve provider metadata (
providerName,providerLogoUrl). - Record upsert — Creates or updates the
bank_connectionsrecord with encrypted tokens andstatus: "connected". - Account upsert — Creates or updates
bank_accountsrecords. Sensitive fields (sort code, account number, IBAN) are encrypted at rest. - Audit log — Records a
bank.connectedaudit event. - Background sync — Fires the
bank/sync.requestedInngest event so transactions are fetched immediately in the background. - Redirect — Clears the CSRF cookie and sends you to
/dashboard/bank?connected=true.
Error redirects from /api/bank/callback
| Condition | Redirect target |
|---|---|
| User denied consent | /dashboard/bank?error=access_denied |
code parameter missing | /dashboard/bank?error=missing_code |
| CSRF cookie/state mismatch | /dashboard/bank?error=invalid_state |
| State payload malformed | /dashboard/bank?error=malformed_state |
| Token exchange or other error | /dashboard/bank?error=connection_failed |
Partial account failures during callback are non-fatal. If listing accounts fails, the connection record is still saved and accounts will be fetched during the first background sync.
Security
| Mechanism | Detail |
|---|---|
| CSRF protection | state cookie is HttpOnly, SameSite=Lax; validated on every callback |
| Encrypted credentials | TrueLayer tokens stored encrypted in bank_connections; sort code, account number, and IBAN encrypted in bank_accounts |
| Rate limiting | OAuth initiation capped at 10 requests/minute per user |
| Cookie expiry | CSRF state cookie expires after 10 minutes |
Sync error reporting
After a bank account is connected, background syncs are run by the bank/sync.requested Inngest function. As of v1.0.479, sync errors are accurately reflected on the connection status:
- All accounts failed to sync → connection status is set to
"error" - Some accounts failed → connection status remains
"connected"; the partial failure detail is stored inlastSyncError - Individual account errors are reported to the error tracking service via
captureError()
Previously, the connection was always marked "connected" after a sync run regardless of whether any transactions were successfully fetched.
Required environment variables
| Variable | Description |
|---|---|
TRUELAYER_CLIENT_ID | TrueLayer application client ID |
TRUELAYER_CLIENT_SECRET | TrueLayer application client secret |
TRUELAYER_REDIRECT_URI | Must be set to <your-app-url>/api/bank/callback |
If any of these are missing, the authorise route redirects to /dashboard/bank?error=config_error rather than throwing a 500.