Container Diagram
C4 Level 2 — Container Diagram
Drop fintech platform container architecture showing all runtime containers, their responsibilities, communication patterns, and the middleware chain that governs every API request.
Container Diagram
C4Container
title Drop — Container Diagram (C4 Level 2)
Person(user, "End User", "Norwegian resident 18+, authenticated via BankID")
Person(merchant, "Merchant", "Business owner receiving QR payments")
System_Boundary(drop, "Drop Platform") {
Container(web, "drop-web", "Next.js 15, React 19, Tailwind v4", "Server-side rendered web application. Handles login redirect, dashboard, send money, QR scan, bank accounts, transaction history, notifications, settings, merchant dashboard.")
Container(api, "drop-api", "Hono v4, Node.js 22", "REST API server. 26+ endpoints under /v1/. BankID OIDC callback, transaction processing, recipient management, merchant registration, GDPR compliance, admin operations.")
Container(mobile, "drop-mobile", "Expo SDK 54, React Native", "Native mobile app for iOS and Android. BankID auth via expo-web-browser deep linking. AsyncStorage for token persistence. No offline support.")
ContainerDb(db, "Database", "PostgreSQL 16 (all environments)", "19 tables: 12 core (users, transactions, bank_accounts, sessions, merchants, recipients, etc.) + 7 compliance (audit_log, aml_alerts, str_reports, screening_results, consents, data_access_requests, complaints). Drizzle ORM.")
}
System_Ext(bankid, "BankID OIDC", "Norwegian eID provider. OIDC authorize/token/JWKS endpoints for Strong Customer Authentication.")
System_Ext(sumsub, "Sumsub", "KYC/AML identity verification. WebSDK (web), React Native SDK (mobile), webhooks for status updates.")
System_Ext(openbanking, "Open Banking APIs", "PSD2 AISP (read balances) and PISP (initiate payments) via licensed provider.")
System_Ext(sepa, "SEPA/SWIFT Networks", "International payment rails for remittance settlement to 30+ countries.")
Rel(user, web, "HTTPS", "Browser")
Rel(user, mobile, "HTTPS", "Native app")
Rel(merchant, web, "HTTPS", "Merchant dashboard")
Rel(web, api, "HTTPS REST", "/v1/* endpoints, JSON, Bearer token or httpOnly cookie")
Rel(mobile, api, "HTTPS REST", "/v1/* endpoints, JSON, Bearer token")
Rel(api, db, "SQL", "Type-safe queries via Drizzle ORM (src/shared/db/)")
Rel(api, bankid, "OIDC", "Authorization code flow, JWKS token verification")
Rel(api, sumsub, "REST + Webhooks", "Applicant creation, document checks, status webhooks")
Rel(api, openbanking, "PSD2 API", "AISP balance reads, PISP payment initiation with SCA")
Rel(api, sepa, "ISO 20022", "Remittance settlement via banking partner")
Container Responsibilities
| Container | Technology | Responsibilities | Port |
|---|---|---|---|
| drop-web | Next.js 15, React 19, Tailwind v4 | SSR web app, BankID redirect initiation, UI rendering for all 10 screens (Login, Onboarding, Dashboard, SendMoney, BankAccounts, TransactionHistory, ScanQR, Profile, Notifications, MerchantDashboard) | 3000 |
| drop-api | Hono v4, Node.js 22 Alpine | REST API, BankID OIDC callback handling, JWT session management, transaction processing, GDPR endpoints, admin operations, audit logging | 3001 |
| drop-mobile | Expo SDK 54, React Native | iOS/Android native app, BankID via expo-web-browser + deep link (drop://auth/callback), AsyncStorage for token persistence, push notifications |
N/A |
| Database | PostgreSQL 16 (all environments) | 19 tables, foreign keys enforced. Drizzle ORM schema in src/shared/db/schema.ts. Local: Docker port 5433. Production: AWS RDS. |
5432 |
Request Lifecycle
sequenceDiagram
participant Client as Client (Web/Mobile)
participant CORS as CORS Middleware
participant ReqID as Request ID Middleware
participant IP as Client IP Middleware
participant RL as Rate Limiter
participant Auth as Auth Middleware
participant Route as Route Handler
participant DB as Database
participant ErrH as Error Handler
Client->>+CORS: HTTPS Request
CORS->>CORS: Validate Origin against allowlist
CORS->>+ReqID: Pass if origin allowed
ReqID->>ReqID: Extract x-request-id or generate UUID
ReqID->>ReqID: Set x-request-id response header
ReqID->>+IP: Forward request
IP->>IP: Extract IP from x-real-ip / x-forwarded-for
IP->>IP: Set clientIp context variable
alt Rate-limited endpoint
IP->>+RL: Forward to rate limiter
RL->>DB: SELECT count, reset_at FROM rate_limits WHERE key = ?
DB-->>RL: Current count
alt Under limit
RL->>RL: UPDATE count + 1
RL->>+Auth: Forward request
else Over limit
RL-->>Client: 429 Too Many Requests
end
else Non-rate-limited endpoint
IP->>+Auth: Forward request
end
alt Authenticated endpoint
Auth->>Auth: Extract token (Bearer header or drop_token cookie)
Auth->>Auth: Verify JWT (jose, HS256/RS256)
Auth->>DB: SELECT session (check revoked = 0, expires_at > now)
DB-->>Auth: Session record
Auth->>DB: SELECT user WHERE id = ? AND deleted_at IS NULL
DB-->>Auth: User record
Auth->>Auth: Set user context variable
Auth->>+Route: Forward authenticated request
else Public endpoint
IP->>+Route: Forward directly
end
Route->>DB: Business logic queries (parameterized)
DB-->>Route: Query results
Route-->>Client: JSON response { data: {...} }
Note over ErrH: On any unhandled error
Route-->>ErrH: Error thrown
ErrH->>ErrH: Log error, capture in Sentry
ErrH-->>Client: { error: "internal_error", message: "..." }
Middleware Chain
The Hono v4 API (drop-api) applies middleware in the following order for every request:
| Order | Middleware | Source | Purpose |
|---|---|---|---|
| 1 | CORS | hono/cors in app.ts:23-30 |
Validates Origin header against allowlist (localhost:3000, localhost:3001, APP_URL). Sets credentials: true for cookie transport. |
| 2 | Request ID | app.ts:33-38 |
Reads x-request-id header or generates crypto.randomUUID(). Sets on context and response header for distributed tracing. |
| 3 | Client IP | app.ts:41-47 |
Extracts IP from x-real-ip then x-forwarded-for (first in chain), falls back to 127.0.0.1. Stored in context for rate limiting and audit. |
| 4 | Rate Limiter | middleware/rate-limit.ts |
Per-IP rate limiting backed by rate_limits DB table. Configurable limit and window per route. Cleans expired entries every 100 calls. |
| 5 | Auth | middleware/auth.ts |
Extracts JWT from Authorization: Bearer header or drop_token cookie. Verifies signature (jose HS256/RS256), checks session not revoked, loads user record. |
| 6 | Merchant | middleware/auth.ts:21-29 |
Standalone middleware that independently verifies auth (calls extractToken and verifyAndGetUser) and checks user.role === 'merchant'. Does NOT extend or chain authMiddleware. Returns 403 if not merchant. |
| 7 | Global Error Handler | middleware/error-handler.ts |
Catches all unhandled errors. HTTPException returns structured JSON with status. Other errors return 500, log to stdout, and capture in Sentry. |
Rate Limit Configuration
| Endpoint Group | Limit | Window | Source |
|---|---|---|---|
| BankID initiate | 10 req | 60s | routes/auth.ts:19 |
| BankID callback | 10 req | 60s | routes/auth.ts:43 |
| Remittance | 10 req/60s per-IP + 3 req/60s per-user | 60s | routes/transactions.ts |
| QR Payment | 10 req/60s per-IP + 3 req/60s per-user | 60s | routes/transactions.ts |
| Exchange rates | 120 req | 60s | routes/rates.ts |
Communication Patterns
Web Client to API
The Next.js web app communicates with the Hono API over HTTPS REST:
- Authentication: httpOnly cookie (
drop_token) set on BankID callback redirect. Cookie attributes:HttpOnly,Path=/,Max-Age=604800(7 days),SameSite=Lax. - CSRF protection: CORS origin validation +
SameSitecookie attribute. - Content type:
application/jsonfor all request/response bodies. - Error envelope:
{ error: "code", message: "human-readable", details: [...] }.
Mobile Client to API
The Expo mobile app uses Bearer token authentication:
- Token storage:
AsyncStorage(React Native encrypted storage). - Auth header:
Authorization: Bearer <jwt>. - BankID flow:
expo-web-browseropens BankID authorize URL, redirects back via deep linkdrop://auth/callback?code=&state=. - Token refresh:
POST /v1/auth/refresh— revokes old sessions, issues new JWT, sets cookie (web) and returns token in body (mobile).
API to Database
- Abstraction layer:
db.tsprovidesquery(),getOne(),run(),runIgnore(),runUpsert(),transaction(). - Driver detection:
DATABASE_URLenv var present = PostgreSQL viapg.Pool, absent = SQLite viabetter-sqlite3. - SQL compatibility: Automatic conversion of SQLite dialect to PostgreSQL (placeholders
?to$N,datetime('now')toCURRENT_TIMESTAMP,INSERT OR IGNOREtoON CONFLICT DO NOTHING). - Transaction isolation: SQLite uses
BEGIN/COMMIT/ROLLBACKon the single connection. PostgreSQL uses pool client with explicit transaction.
API to External Services
| Service | Protocol | Authentication | Data Flow |
|---|---|---|---|
| BankID OIDC | HTTPS (OpenID Connect) | Client ID + Client Secret | Auth code exchange, JWKS token verification, pid extraction |
| Sumsub KYC | REST + Webhooks | API key + HMAC signature | Applicant creation, document verification, status webhooks |
| Open Banking | PSD2 REST API | OAuth2 (provider-specific) | AISP balance reads (cached in bank_accounts.balance), PISP payment initiation |
| SEPA/SWIFT | ISO 20022 (via banking partner) | Banking partner credentials | Remittance settlement to 30+ countries |
Cross-References
- API endpoints: API-REFERENCE.md — Full endpoint documentation with request/response examples
- Database schema: DATABASE-SCHEMA.md — All 19 tables with column definitions
- Authentication: AUTHENTICATION.md — BankID OIDC flow, JWT structure, session management
- Middleware: MIDDLEWARE.md — Detailed middleware documentation
- Security: SECURITY-ARCHITECTURE.md — Threat model, security headers, input validation
- Deployment: deployment-architecture.md — AWS + Cloudflare topology, CI/CD pipeline
- Feature flags: FEATURE-FLAGS.md — Runtime feature gating system
No comments to display
No comments to display