Skip to main content

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 + SameSite cookie attribute.
  • Content type: application/json for 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-browser opens BankID authorize URL, redirects back via deep link drop://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.ts provides query(), getOne(), run(), runIgnore(), runUpsert(), transaction().
  • Driver detection: DATABASE_URL env var present = PostgreSQL via pg.Pool, absent = SQLite via better-sqlite3.
  • SQL compatibility: Automatic conversion of SQLite dialect to PostgreSQL (placeholders ? to $N, datetime('now') to CURRENT_TIMESTAMP, INSERT OR IGNORE to ON CONFLICT DO NOTHING).
  • Transaction isolation: SQLite uses BEGIN/COMMIT/ROLLBACK on 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