Tenant row and isolated by platform='banno' and tenant-scoped queries.
How It Differs from Candescent
| Aspect | Candescent | Banno |
|---|---|---|
| Delivery | CDX React widget via Module Federation | Server-rendered HTML loaded in iframe (Banno plugin) |
| Framework | React 18 + MUI 7 + TypeScript | Django 6.0 + Python 3.12 |
| Member Auth | Candescent JWT forwarded by OLB shell | Banno OIDC + PKCE → internal HS256 JWT cookie |
| Account/Transaction Data | Candescent DevEx REST APIs | Banno Consumer API (OAuth-protected) |
| FI Admin UI | Candescent admin portal | RAF Admin Console (bundled Django app InviteFriendConsole) |
| Token Handling | Forwarded Authorization header | Banno tokens cached server-side, never sent to client |
Solution Capabilities
The Banno integration delivers the same RAF capabilities documented in the product introduction, plus a few Banno-specific ones:- Iframe plugin tile with five dynamic states (
anonymous,inviter_ready,acceptor_pending,acceptor_eligible,acceptor_paid) rendered server-side at/plugin/ - Banno OIDC + PKCE sign-in flow, state-token CSRF protection, and server-side token cache
- Internal short-lived JWT (600s HS256, httpOnly cookie) with silent re-issue while the Banno session is still valid
- Live eligibility checks that call the Banno Consumer API on demand, with a cached fallback when the Banno access token is absent
- FI staff console with campaign CRUD, referral list, per-referral eligibility detail, and pay/mark-as-paid actions
- Provider abstraction (
services/banking/providers/) that makes it possible to swap Banno for another provider (Q2 already lives alongside Banno) without touching the RAF business logic
Technology Stack
Application- Django 6.0.2 on Python 3.12
- Django templates server-rendered (no SPA)
requestsfor outbound HTTP to Banno- PyJWT for internal JWT issuance and validation
- SQLite in development
- PostgreSQL in production (via
DB_*environment variables)
- Banno OIDC + PKCE (RFC 7636) for member sign-in
- Django session + staff login for the admin console
- Internal HS256 JWT signed with Django
SECRET_KEY, stored in theraf_access_tokenhttpOnly cookie
- Any WSGI/ASGI server (gunicorn, uvicorn)
- Must be reachable over HTTPS from Banno (ngrok in dev; TLS-terminating load balancer in prod)
Content-Security-Policy: frame-ancestors 'self' <BANNO_FI_URL>is set by middleware so the plugin tile can be embedded by the FI’s Banno domain
Integration Points
Banno platform- OIDC authorization endpoint:
GET {FI_URL}/a/consumer/api/v0/oidc/auth - Token endpoint:
POST {FI_URL}/a/consumer/api/v0/oidc/token - Consumer API (accounts, transactions):
GET {FI_URL}/a/consumer/api/v0/users/{userId}/...
- Plugin tile and dashboard views — rendered by
InviteFriend/views.py - Stateless JSON API (Bearer JWT) —
InviteFriend/api_views.py - Campaign / referral / eligibility / email / payment services —
InviteFriend/*_service.py - FI admin console —
InviteFriendConsole/
Quick Links
- Authentication Flow — Banno OIDC + PKCE + internal JWT
- Member Experience — Plugin tile → submit → share → eligibility
- Admin Console — Overview, referrals, new campaign form
