Backend API reference, database, authentication, services, middleware API & Data API Reference Drop Backend API Reference Auto-generated from source code analysis. All file references are relative to src/drop-app/src/ . Overview Drop uses Next.js App Router API routes ( app/api/ ). All responses use a consistent JSON envelope: { "data": { ... } } // Success { "error": "code", "message": "...", "details": [...] } // Error Authentication is via httpOnly cookie ( drop_token ) containing a signed JWT (HS256, 24h expiry). Pass-Through Model: Drop uses a PSD2 pass-through model — it NEVER holds customer money. There is no wallet, no balance, no top-up. User funds remain in their bank account at all times. Drop uses: AISP (Account Information Service Provider) — reads bank balance via Open Banking PISP (Payment Initiation Service Provider) — initiates transfers directly from user's bank account The bank_accounts.balance field stores the last AISP-read balance from the user's real bank (cached for display) — NOT a Drop-held balance. Authentication POST /api/auth/register Create a new user account. Field Source File app/api/auth/register/route.ts Auth None Rate Limit 10 req/min per IP Request Body: Field Type Required Validation email string Yes RFC-like regex, unique password string Yes Min 8 chars, must contain letters + digits firstName string Yes validateName() — 1-100 chars, at least one letter, no HTML/script lastName string Yes Same as firstName phone string No International format +XXXXXXXXXXXX (8-15 digits) dateOfBirth string Yes ISO date string, must be >= 18 years old Success Response (201): { "data": { "id": "usr_...", "email": "user@example.com", "firstName": "...", "lastName": "...", "dateOfBirth": "...", "kycStatus": "pending", "createdAt": "2026-..." } } Error Responses: Status Code Condition 400 bad_request Invalid JSON body 409 conflict Email already registered 422 validation_error Field validation failures (returned in details array) 429 rate_limited Too many requests POST /api/auth/login Authenticate with email and password. Field Source File app/api/auth/login/route.ts Auth None Rate Limit 10 req/min per IP Request Body: Field Type Required email string Yes password string Yes Success Response (200): { "data": { "id": "usr_...", "email": "...", "firstName": "...", "lastName": "...", "kycStatus": "approved" } } Error Responses: Status Code Condition 400 bad_request Missing email or password 401 unauthorized Invalid credentials 429 rate_limited Too many requests GET /api/auth/me Get current authenticated user with bank accounts. Field Source File app/api/auth/me/route.ts Auth Required (cookie) Success Response (200): { "data": { "id": "usr_...", "email": "...", "firstName": "...", "lastName": "...", "totalBalance": 58030.0, "bankAccounts": [ { "id": "ba_1", "bankName": "DNB", "accountNumber": "1234.56.78901", "balance": 45230.0, "currency": "NOK", "isPrimary": true } ], "kycStatus": "approved", "createdAt": "..." } } POST /api/auth/logout Logout and revoke all sessions. Field Source File app/api/auth/logout/route.ts Auth Required (cookie) Calls revokeAllSessions() to invalidate all session records, then clears the auth cookie. Success Response (200): { "message": "Logged out" } POST /api/auth/refresh Refresh the authentication token (issue new JWT, create new session record). Field Source File app/api/auth/refresh/route.ts Auth Required (cookie) Success Response (200): { "data": { "userId": "usr_...", "email": "...", "role": "user" } } Transactions GET /api/transactions List user's transactions with pagination and filtering. Field Source File app/api/transactions/route.ts Auth Required Query Parameters: Param Type Default Notes page int 1 Min 1 limit int 20 Min 1, Max 50 type string - remittance or qr_payment status string - processing , completed , or failed Success Response (200): { "data": [ { "id": "tx_rem_1", "type": "remittance", "status": "completed", "amount": -2000, "currency": "NOK", "recipientName": "Mama Jasmina", "createdAt": "..." } ], "pagination": { "page": 1, "limit": 20, "total": 3 } } Note: amount is negated in the response (always shown as outgoing). GET /api/transactions/[id] Get single transaction details with exchange rate info. Field Source File app/api/transactions/[id]/route.ts Auth Required Success Response (200): { "data": { "id": "tx_rem_1", "type": "remittance", "status": "completed", "sendAmount": 2000, "sendCurrency": "NOK", "receiveAmount": 23400, "receiveCurrency": "RSD", "exchangeRate": 11.7, "fee": 10, "total": 2010, "recipientName": "Mama Jasmina", "recipientCountry": "Serbia", "createdAt": "...", "completedAt": "..." } } GET /api/transactions/summary Get transaction summary statistics (all-time and this month). Field Source File app/api/transactions/summary/route.ts Auth Required Success Response (200): { "data": { "allTime": { "totalCount": 3, "totalSent": 5000, "totalPaid": 129, "remittanceCount": 2, "qrPaymentCount": 1 }, "thisMonth": { "..." } } } POST /api/transactions/remittance Create a remittance (international money transfer). Field Source File app/api/transactions/remittance/route.ts Auth Required Rate Limit 10 req/min per IP KYC Must be approved Request Body: Field Type Required Validation recipientId string Yes Must belong to user amount number Yes 100-50,000 NOK, max 2 decimal places currency string No Defaults to NOK bankAccountId string No Defaults to primary bank account Business Logic: Verify recipient belongs to user Look up exchange rate for recipient's currency Verify bank account exists and has sufficient balance Fee: 0.5% of amount Debit bank account (atomic transaction) Create transaction record with status processing Success Response (201): { "data": { "id": "tx_rem_...", "type": "remittance", "status": "processing", "sendAmount": 2000, "sendCurrency": "NOK", "receiveAmount": 23400, "receiveCurrency": "RSD", "exchangeRate": 11.7, "fee": 10, "feePercent": 0.5, "total": 2010, "recipientName": "...", "recipientCountry": "Serbia", "fromAccount": "DNB", "eta": "1-2 business days", "createdAt": "..." } } Error Responses: Status Code Condition 400 bad_request Missing/invalid fields 400 no_bank_account No linked bank account 402 insufficient_balance Bank account balance too low 403 kyc_required KYC not approved 404 not_found Recipient not found 422 validation_error Unsupported currency corridor POST /api/transactions/qr-payment Create a QR payment to a merchant. Field Source File app/api/transactions/qr-payment/route.ts Auth Required Rate Limit 10 req/min per IP Request Body: Field Type Required Validation merchantId string Yes Must exist amount number Yes 1-100,000 NOK, max 2 decimal places Business Logic: Verify merchant exists Get user's primary bank account Fee: 1% of amount Debit bank account (atomic transaction) Create transaction with status completed (instant) Success Response (201): { "data": { "id": "tx_qr_...", "type": "qr_payment", "status": "completed", "amount": 129, "currency": "NOK", "fee": 1.29, "feePercent": 1, "merchantName": "Ahmetov Kebab", "merchantId": "mer_1", "fromAccount": "DNB", "createdAt": "..." } } Recipients GET /api/recipients List user's recipients with pagination. Field Source File app/api/recipients/route.ts Auth Required Query Parameters: page (default 1), limit (default 20, max 50) Bank account numbers are masked in response (e.g., *****5678 ). Supported Countries: RS (Serbia), BA (Bosnia), PL (Poland), PK (Pakistan), TR (Turkey) POST /api/recipients Add a new recipient. Field Source File app/api/recipients/route.ts Auth Required Request Body: Field Type Required Validation name string Yes validateName() country string Yes Must be in supported list currency string Yes - bankAccount string Yes - bankName string No Sanitized to 200 chars DELETE /api/recipients/[id] Delete a recipient. Field Source File app/api/recipients/[id]/route.ts Auth Required Returns 204 No Content on success. Returns 404 if recipient not found or not owned by user. Cards (FUTURE — feature-flagged, all flags default to false ) Note: The entire Cards section is a FUTURE feature, gated behind feature flags. All card-related feature flags default to false . These endpoints exist in code but return 404 when flags are disabled. Cards require a card issuing partner (e.g., Stripe Issuing) before activation. GET /api/cards List user's cards (excludes cancelled). Field Source File app/api/cards/route.ts Auth Required POST /api/cards Create a new card (virtual or physical). Field Source File app/api/cards/route.ts Auth Required Request Body: Field Type Required Notes type string No virtual (default) or physical GET /api/cards/[id] Get card details. Card number is masked ( ---- ---- ---- XXXX ), CVV is hidden ( --- ). Field Source File app/api/cards/[id]/route.ts Auth Required PCI-DSS compliant: never exposes full card number or CVV. PATCH /api/cards/[id] Freeze or unfreeze a card. Field Source File app/api/cards/[id]/route.ts Auth Required Request Body: { "status": "active" | "frozen" } DELETE /api/cards/[id] Cancel a card (soft delete — sets status to cancelled ). Field Source File app/api/cards/[id]/route.ts Auth Required POST /api/cards/[id]/physical Order physical version of a virtual card. Field Source File app/api/cards/[id]/physical/route.ts Auth Required Feature Flag physicalCards (returns 404 if disabled) Request Body: { "address": "..." } (min 10 chars) POST /api/cards/[id]/pin Set PIN for a card. Field Source File app/api/cards/[id]/pin/route.ts Auth Required Feature Flag cardPin (returns 404 if disabled) Request Body: { "pin": "1234" } (exactly 4 digits) PIN is hashed with bcrypt before storage. GET /api/cards/[id]/limits Get spending limits for a card. Field Source File app/api/cards/[id]/limits/route.ts Auth Required Feature Flag spendingLimits (returns 404 if disabled) PUT /api/cards/[id]/limits Set a spending limit for a card. Field Source File app/api/cards/[id]/limits/route.ts Auth Required Feature Flag spendingLimits (returns 404 if disabled) Request Body: Field Type Required Validation limitType string Yes daily , weekly , monthly , or transaction amount number Yes Must be positive Replaces any existing limit of the same type. Exchange Rates See also: Currency Rates for provider source, cache/freshness behavior, fallback, and circuit-breaker verification. GET /api/rates Get all persisted exchange rates from NOK. This endpoint reads exchange_rates ; it does not call the external FX providers directly. Field Source File apps/drop-api/src/routes/rates.ts Auth None Rate Limit 120 req/min per IP Success Response (200): { "data": { "baseCurrency": "NOK", "rates": { "RSD": 10.17, "BAM": 0.17, "PLN": 0.374, "PKR": 26.5, "TRY": 3.39, "EUR": 0.087 }, "updatedAt": "...", "stale": false } } GET /api/rates/:currency Get one persisted NOK → target-currency rate. Field Source File apps/drop-api/src/routes/rates.ts Auth None Rate Limit 120 req/min per IP Response includes fee: 0.005 (0.5% remittance fee) and stale freshness status. Notifications GET /api/notifications List all notifications for user. Field Source File app/api/notifications/route.ts Auth Required Feature Flag notifications (default: enabled) PATCH /api/notifications Mark notifications as read. Field Source File app/api/notifications/route.ts Auth Required Feature Flag notifications Request Body: { "notificationIds": ["noti_..."] } Max 100 IDs per request IDs validated against format ^[a-z]+_[a-f0-9]{16}$ Settings GET /api/settings Get user settings (creates defaults if none exist). Field Source File app/api/settings/route.ts Auth Required Defaults: currency=NOK, language=nb, pushEnabled=true, emailEnabled=true PATCH /api/settings Update user settings. Field Source File app/api/settings/route.ts Auth Required Request Body (all optional): Field Type Validation currency string Whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR language string Whitelist: nb, en, bs, sq pushEnabled boolean - emailEnabled boolean - Merchants POST /api/merchants/register Register as a merchant. Field Source File app/api/merchants/register/route.ts Auth Required Request Body: Field Type Required Validation businessName string Yes validateName() orgNumber string Yes Exactly 9 digits, unique address string No Sanitized to 300 chars bankAccount string Yes Payout account Upgrades user role to merchant . Returns a QR code URI ( drop://pay/{merchantId} ). GET /api/merchants/dashboard Get merchant dashboard stats. Field Source File app/api/merchants/dashboard/route.ts Auth Required (merchant role) Query Parameters: period — today (default), week , month Returns: revenue, transactionCount, fees, netRevenue, nextPayout, payoutTime. GET /api/merchants/qr Get merchant QR code data. Field Source File app/api/merchants/qr/route.ts Auth Required (merchant role) Returns: merchantId, businessName, qrValue ( drop://pay/{id} ), address. GET /api/merchants/transactions List merchant's QR payment transactions with pagination. Field Source File app/api/merchants/transactions/route.ts Auth Required (merchant role) Query Parameters: page , limit Customer names are partially anonymized (first name + last initial). GDPR & Compliance GET /api/user/data-export Export all user data (GDPR right to data portability). Field Source File app/api/user/data-export/route.ts Auth Required Creates a data_access_request record with type export and status completed . Success Response (200): { "data": { "user": { "id": "usr_...", "email": "...", "first_name": "...", "last_name": "...", "phone": "+47...", "date_of_birth": "1995-03-15", "kyc_status": "approved", "role": "user", "created_at": "..." }, "transactions": [ {...}, {...} ], "recipients": [ {...}, {...} ], "bankAccounts": [ {...} ], "settings": { "currency": "NOK", "language": "nb", ... }, "consents": [ {...}, {...} ] }, "exportedAt": "2026-02-17T..." } DELETE /api/user/account Request account deletion (GDPR right to erasure). Field Source File app/api/user/account/route.ts Auth Required Behavior: Soft-deletes user (sets deleted_at timestamp) Revokes all active sessions Creates data_access_request with type erasure and status completed Important: Data retained for 5 years per AML/KYC legal requirements (hvitvaskingsloven) Success Response (200): { "message": "Account scheduled for deletion", "retentionNote": "Data retained for 5 years per AML requirements" } GET /api/consents List user's GDPR consents. Field Source File app/api/consents/route.ts Auth Required Success Response (200): { "data": [ { "id": "con_...", "user_id": "usr_...", "consent_type": "terms", "granted": 1, "granted_at": "2026-02-17T...", "withdrawn_at": null, "ip_address": "192.0.2.1" } ] } POST /api/consents Grant or withdraw a consent. Field Source File app/api/consents/route.ts Auth Required Request Body: Field Type Required Validation consentType string Yes Must be one of: terms , privacy , marketing , cookies_analytics , cookies_marketing granted boolean Yes true to grant, false to withdraw Behavior: If consent exists: updates granted field and sets either granted_at or withdrawn_at If consent doesn't exist: creates new consent record Records user's IP address with consent action Success Response (200 for update, 201 for new): { "data": { "id": "con_...", "consent_type": "marketing", "granted": 1, "granted_at": "2026-02-17T...", "withdrawn_at": null, "ip_address": "192.0.2.1" } } Error Responses: Status Code Condition 400 bad_request Invalid consent type or missing fields GET /api/complaints List user's complaints. Field Source File app/api/complaints/route.ts Auth Required Query Parameters: Param Type Default Notes page int 1 Pagination page number limit int 10 Items per page, max 100 Success Response (200): { "data": [ { "id": "cmp_...", "category": "transaction", "subject": "Transaction delayed", "description": "My remittance to Serbia is delayed...", "status": "received", "resolution": null, "created_at": "2026-02-17T...", "resolved_at": null } ], "pagination": { "page": 1, "limit": 10, "total": 3, "totalPages": 1 } } POST /api/complaints Submit a complaint (Finansavtaleloven §3-53 compliance). Field Source File app/api/complaints/route.ts Auth Required Request Body: Field Type Required Validation category string Yes Must be one of: transaction , service , fees , privacy , technical , other subject string Yes Max 200 chars, sanitized description string Yes Max 2000 chars, sanitized Success Response (201): { "data": { "id": "cmp_...", "category": "fees", "subject": "High transfer fee", "description": "...", "status": "received", "created_at": "2026-02-17T..." }, "commitmentNote": "We will review and respond to your complaint within 15 business days per Finansavtaleloven §3-53" } Error Responses: Status Code Condition 400 bad_request Invalid category or empty fields POST /api/transactions/disclosure Get full transaction fee and exchange rate disclosure before initiating payment. Field Source File app/api/transactions/disclosure/route.ts Auth Required Request Body: Field Type Required Notes type string Yes remittance or qr_payment amount number Yes Must be positive currency string No Defaults to NOK recipientId string Conditional Required for remittance Success Response (200): { "amount": 2000, "fee": 10, "feePercentage": 0.5, "exchangeRate": 10.17, "receiveAmount": 20340, "receiveCurrency": "RSD", "estimatedDelivery": "1-2 business days", "totalCost": 2010 } Fee Calculation: Remittance: 0.5% of amount QR payment: 1.0% of amount Delivery Time: QR payment: "Instant" Remittance (EEA): "1-2 business days" Remittance (non-EEA): "2-4 business days" GET /api/transactions/[id]/receipt Get transaction receipt with full details. Field Source File app/api/transactions/[id]/receipt/route.ts Auth Required Success Response (200): { "data": { "transactionId": "tx_rem_1", "date": "2026-02-17T...", "type": "remittance", "amount": 2000, "currency": "NOK", "fee": 10, "exchangeRate": 10.17, "receiveAmount": 20340, "receiveCurrency": "RSD", "recipient": { "name": "Mama Jasmina", "country": "RS" }, "reference": "tx_rem_1", "status": "completed", "estimatedCompletion": null, "completedAt": "2026-02-17T..." } } Error Responses: Status Code Condition 404 not_found Transaction not found or not owned by user Health Check GET /api/health System health check (no auth required). Field Source File app/api/health/route.ts Auth None Success Response (200): { "status": "ok", "version": "0.1.0", "uptime": 3600, "db": "connected", "dbLatencyMs": 1, "timestamp": "..." } Returns 503 with status: "error" if database is unreachable. Authentication Drop Authentication System Sources: src/drop-app/src/app/api/auth/bankid/ , src/drop-api/src/lib/bankid.ts , src/drop-api/src/routes/auth.ts Overview Drop uses BankID OIDC as the sole authentication method. Email/password login has been removed to comply with PSD2/SCA requirements. Auth method: BankID OIDC (Norwegian eID) JWT Algorithm: HS256 (HMAC-SHA256), RS256 opt-in Library: jose ( SignJWT / jwtVerify ) Token lifetime: 24h (web cookie), 7d (mobile Bearer token) Web cookie: drop_token (httpOnly, secure, sameSite=strict) Mobile: Bearer token in Authorization header Phase 2 (planned) Vipps Login — same OIDC pattern, user dedup by national_id_hash Idura aggregator optional (single integration point for BankID + Vipps) Authentication Flow BankID Login (Web) Browser Next.js BFF BankID OIDC | | | | GET /api/auth/bankid | | |--------------------------->| | | | 1. Rate limit check | | | 2. Generate state + nonce | | | 3. Set bankid_state cookie | | { redirectUrl } | | |<---------------------------| | | | | | Browser redirects to BankID authorize URL | |---------------------------------------------------------->| | | | | User authenticates with BankID | | | | | BankID redirects to /api/auth/bankid/callback?code=&state= |<----------------------------------------------------------| | | | | GET /callback?code&state | | |--------------------------->| | | | 4. Verify state vs cookie | | | 5. Exchange code for tokens | | |----------------------------->| | | { id_token, access_token } | | |<-----------------------------| | | 6. Verify ID token (JWKS) | | | 7. Parse pid, verify age | | | 8. Find/create user | | | 9. Create session + cookie | | 302 /dashboard | | |<---------------------------| | BankID Login (Mobile) Mobile App Hono API BankID OIDC | | | | GET /v1/auth/bankid/initiate?platform=mobile | |--------------------------->| | | { redirectUrl, state } | | |<---------------------------| | | | | | Open BankID in secure browser (expo-web-browser) | |---------------------------------------------------------->| | | | | User authenticates with BankID | | | | | Redirect to drop://auth/callback?code=&state= | |<----------------------------------------------------------| | | | | POST /v1/auth/bankid/callback | | { code, state, platform } | |--------------------------->| | | | 1. Exchange code for tokens | | |----------------------------->| | | { id_token } | | |<-----------------------------| | | 2. Verify ID token (JWKS) | | | 3. Parse pid, verify age | | | 4. Find/create user | | | 5. Create session | | { token, data } | | |<---------------------------| | | | | | Store token in AsyncStorage | User Creation BankID login automatically creates user accounts: Parse pid from BankID ID token (Norwegian national ID, 11 digits) Hash pid with SHA-256 for storage ( national_id_hash column) Check existing user by national_id_hash If new: Create user with: kyc_status = 'approved' (BankID = verified identity) kyc_method = 'bankid' auth_provider = 'bankid' password_hash = 'EIDONLY' (sentinel — no password auth) Age check: Must be >= 18 (parsed from pid birthdate) JWT Structure Payload interface JwtPayload { userId: string; // e.g., "usr_a1b2c3d4e5f6g7h8" email: string; // e.g., "usr_xxx@bankid.drop.local" role: string; // "user" or "merchant" } Claims Claim Value exp Current time + 24h (web) / 7d (mobile) iat Current time iss drop-api (Hono) / none (Next.js) aud drop (Hono) / none (Next.js) Session Revocation On login: sessions record created with SHA-256 hash of JWT On each request: Verify session not revoked + not expired On logout: All user sessions marked revoked = 1 CSRF Protection Web: State parameter in BankID OIDC flow (stored in httpOnly cookie) API: Origin header validation against allowed origins Mobile: N/A (Bearer token, no cookies) Rate Limiting Endpoint Limit BankID initiate 10/min per IP BankID callback 10/min per IP Auth me/logout/refresh No additional limit (auth required) Authorization Role-Based Access Two roles: user and merchant . Route Auth Role GET /auth/bankid/initiate None - POST /auth/bankid/callback None - GET /auth/me Required Any POST /auth/logout Required Any POST /auth/refresh Required Any POST /merchants/register Required Any (upgrades to merchant) GET /merchants/dashboard Required Merchant Deprecated Endpoints These endpoints return 410 Gone : Endpoint Replacement POST /auth/login BankID OIDC flow POST /auth/register Automatic via BankID login POST /auth/verify-otp Not needed (BankID replaces OTP) Environment Variables Required (Production) BANKID_CLIENT_ID # BankID OIDC client ID BANKID_CLIENT_SECRET # BankID OIDC client secret BANKID_CALLBACK_URL # Web callback URL (e.g., https://getdrop.no/api/auth/bankid/callback) BANKID_CALLBACK_URL_MOBILE # Mobile deep link (e.g., drop://auth/callback) JWT_SECRET # JWT signing secret (min 32 chars) Optional BANKID_AUTHORIZE_URL # Default: BankID prod authorize endpoint BANKID_TOKEN_URL # Default: BankID prod token endpoint BANKID_JWKS_URL # Default: BankID prod JWKS endpoint BANKID_ISSUER # Default: BankID prod issuer BANKID_MOCK=true # Dev mode: mock OIDC flow (no real BankID needed) JWT_ALGORITHM # "HS256" (default) or "RS256" JWT_EXPIRY # Default: "24h" Merchant Flow Merchants use the same BankID login as regular users. After logging in: Navigate to merchant registration Fill in business details (business name, org number, bank account) POST /merchants/register with auth token User role upgraded from user to merchant Merchant dashboard becomes accessible Services & Middleware Services Drop External Services Source: src/drop-app/src/lib/services/ Overview Drop uses a PSD2 pass-through model — it never holds customer money. AISP reads bank balances via Open Banking, PISP initiates payments from the user's own bank account. Drop integrates with external service providers. Each service has a different readiness level — see status tags below. For backend FX-rate sourcing, see Currency Rates . Current drop-api source is configured for Norges Bank primary rates with ECB fallback; /api/rates reads the persisted exchange_rates table rather than calling providers directly. Legend: [PRODUCTION] = real SDK, production-ready. [MOCK/DEV] = mock only, NOT connected to real APIs. [PLANNED] = future roadmap. [DEPRECATED] = no longer the chosen provider. Service mode is controlled by NEXT_PUBLIC_SERVICE_MODE env var (default: mock ). Source: services/index.ts:21-30 export const config = { mode: (process.env.NEXT_PUBLIC_SERVICE_MODE || 'mock') as 'mock' | 'production', endpoints: { sumsub: process.env.SUMSUB_API_URL || 'https://api.sumsub.com', }, } Note on Swan: Swan was previously listed as the Open Banking provider but has been deprecated. The pass-through PSD2 model will use a different AISP/PISP provider (TBD). Note on Stripe: Card issuing is a future feature gated behind feature flags. No Stripe SDK is integrated — only a mock file exists. Note on Vipps/Nets: Sometimes mentioned in business discussions but have ZERO code in the codebase. Swan — Open Banking / PSD2 Provider [DEPRECATED] ⚠️ DEPRECATED: Swan is no longer the planned Open Banking provider. Mock code remains but will be removed. File: services/mock-swan.ts Production docs: https://docs.swan.io/ Status: DEPRECATED mock — no production integration, no contract, no API keys. Interfaces Interface Description SwanAccount Bank account with IBAN, BIC, balance, status SwanTransaction SEPA credit/debit with status tracking Functions Function Signature Description createAccount (userId) → SwanAccount Create new bank account with IBAN getAccount (accountId) → SwanAccount | null Retrieve account details getBalance (accountId) → {available, pending} Get balance breakdown initiateTransfer ({fromAccountId, toIban, amount, ...}) → SwanTransaction Initiate SEPA credit transfer simulateIncoming ({toAccountId, amount, fromIban}) → SwanTransaction Simulate incoming transfer getTransactions (accountId, limit?) → SwanTransaction[] List recent transactions onWebhook (callback) → void Register webhook listener Mock Behavior 200-800ms simulated latency per call IBAN generated in BA format (Bosnia mock) Transfers start as Pending , settle to Booked after 2 seconds State persisted to localStorage (browser) or in-memory (server) _testHelpers.reset() clears all mock data Account Statuses Opened | Closing | Closed Transaction Types SepaCredit | SepaDebit | CardTransaction Transaction Statuses Pending | Booked | Rejected Stripe — Card Issuing [MOCK/DEV] ⚠️ MOCK ONLY: No Stripe SDK installed. Mock file for UI development only. File: services/mock-stripe.ts Production docs: https://stripe.com/docs/issuing Status: Mock implementation only — no real Stripe API calls, no SDK, no API keys. Interfaces Interface Description StripeCard Card with type, brand, status, spending limits StripeCardDetails Full card number, CVC, expiry (virtual only) StripeAuthorization Card authorization with merchant info Functions Function Signature Description createVirtualCard ({cardholderName, spendingLimit?}) → StripeCard Issue virtual Visa card orderPhysicalCard ({cardholderName, shippingAddress}) → StripeCard Order physical card getCardDetails (cardId) → StripeCardDetails Get full card details (virtual only) setCardStatus (cardId, active) → StripeCard Freeze/unfreeze card updateSpendingLimit (cardId, limit) → StripeCard Update spending limit getCards () → StripeCard[] List all cards simulateAuthorization ({cardId, amount, merchant}) → StripeAuthorization Simulate card purchase getAuthorizations (cardId?) → StripeAuthorization[] List authorizations Mock Behavior Virtual cards created instantly with active status, expire in 3 years Physical cards start as pending , activate after 5 seconds (simulating shipping) Default spending limit: 5,000 (virtual), 10,000 (physical) Authorization declined if: card not active OR spending limit exceeded Brand is always Visa Mock card numbers: 4242 4242 4242 {last4} Card Statuses active | inactive | canceled | pending Authorization Statuses pending | approved | declined Sumsub — KYC/Identity Verification [PRODUCTION] ✅ PRODUCTION-READY: Sumsub is the only external service with real production API integration. File: services/mock-sumsub.ts Production docs: https://docs.sumsub.com/ Status: Production-ready — real API calls, WebSDK integration, webhook handling. Interfaces Interface Description SumsubApplicant KYC applicant with review status SumsubDocument Identity document (passport, ID card, etc.) SumsubVerificationResult Verification outcome with per-check breakdown Functions Function Signature Description createApplicant ({externalUserId, email?, phone?}) → SumsubApplicant Create KYC applicant getAccessToken (applicantId) → {token, expiresAt} Get WebSDK token (30min) submitDocument (applicantId, document) → void Submit ID document submitSelfie (applicantId, selfieData) → void Submit selfie for liveness getApplicantStatus (applicantId) → SumsubApplicant Check applicant status getVerificationResult (applicantId) → SumsubVerificationResult Get verification details forceApprove (applicantId) → void Force approve (testing only) onWebhook (callback) → void Register webhook listener Mock Behavior Verification completes after 3-second delay 90% approval rate in mock mode Risk score: 15 (approved) or 85 (rejected) Rejected with label DOCUMENT_UNREADABLE , type RETRY Applicant Statuses init | pending | queued | completed | onHold Review Answers GREEN (approved) | RED (rejected) | RETRY Verification Checks Check Description documentAuthenticity Document is genuine livenessCheck Selfie is a real person facematch Selfie matches document photo sanctionsCheck Not on sanctions lists pepCheck Not a politically exposed person Document Types PASSPORT | ID_CARD | DRIVERS | RESIDENCE_PERMIT Service Initialization Source: services/index.ts:36-48 // Call on app startup await initializeServices() // Reset all mocks (testing) resetMockServices() Service Status Summary Service Status Description Sumsub [PRODUCTION] Real API integration, WebSDK, webhook handling — READY FX rates [IMPLEMENTED/RISK] drop-api uses Norges Bank primary + ECB fallback for cron refresh; see verified parser/conversion risk in Currency Rates Stripe [MOCK/DEV] Mock file only for UI development — NO SDK, NO API keys Swan [DEPRECATED] No longer the planned Open Banking provider — mock will be removed Vipps [PLANNED] Future consideration — ZERO code currently Nets [PLANNED] Future consideration — ZERO code currently Important Notes Sumsub is the ONLY production-ready service — all others are mocks or deprecated. Console warnings are emitted on module load for mock services to make usage visible. Mock state uses localStorage in browser, in-memory on server — resets on server restart. Production API endpoints are configurable via environment variables. The current backend API routes do NOT call these service modules directly — they use the database layer ( db.ts ) for all operations. The services are available for future integration when real providers are connected. Middleware Design Document Middleware Design Document Project: Drop Version: 0.1.0 Date: 2026-02-23 Author: Platform Architect (AI) Status: In Review Reviewers: Alem Bašić (CEO) Document History Version Date Author Changes 0.1 2026-02-23 Platform Architect (AI) Initial draft from source code analysis 1. Overview Drop has two middleware layers: src/lib/middleware.ts — The active middleware used by all API routes. Provides requireAuth , requireMerchant , rateLimit , getClientIp , jsonError , CSRF protection, and session revocation. src/lib/middleware/ — A modular middleware library with auth-middleware.ts (Bearer token for mobile), error-handler.ts (AppError class), and validation.ts (input sanitization functions). Both layers are used in production. Routes import from @/lib/middleware (auth, rate limiting) and @/lib/middleware/validation (input validation). 2. Active Middleware ( lib/middleware.ts ) 2.1 requireAuth(request?) Source: middleware.ts:42–80 Authenticates the current request via cookie-based JWT. Returns: { user: User, error: null } | { user: null, error: NextResponse } Steps: CSRF origin check — if Origin header present, must match allowed origins ( NEXT_PUBLIC_APP_URL , http://localhost:3000 , http://localhost:3001 ) Cookie extraction — reads drop_token from request cookies JWT verification — validates HS256 signature and expiry using jose library User lookup — loads user from users table by userId from JWT payload Session revocation check — verifies at least one non-revoked session exists for this user Usage: const { user, error } = await requireAuth(request); if (error) return error; // Returns NextResponse with JSON error // user is guaranteed non-null here Error responses: 401 unauthorized — missing cookie, invalid JWT, expired token, user not found, all sessions revoked 2.2 requireMerchant(request?) Source: middleware.ts:101–108 Extends requireAuth with a merchant role check. const { user, error } = await requireMerchant(request); if (error) return error; // 401 if not authenticated, 403 if not merchant Returns 403 forbidden if user exists but role !== 'merchant' . Applied to: GET /api/merchants/dashboard , GET /api/merchants/qr , GET /api/merchants/transactions 2.3 rateLimit(ip, limit, windowMs?) Source: middleware.ts:7–31 Persistent IP-based rate limiter using the rate_limits database table. Parameter Default Description ip — Client IP address limit — Max requests per window windowMs 60,000ms Window size in milliseconds Returns: boolean — true if request is allowed, false if rate limited. Implementation: Uses runUpsert for atomic counter creation/update Cleans expired entries on each call (removes rows where expires_at < now ) Counter stored in rate_limits table: (key, count, expires_at) Rate limit table schema: CREATE TABLE rate_limits ( key TEXT PRIMARY KEY, -- IP address count INTEGER DEFAULT 1, expires_at INTEGER -- Unix timestamp (ms) ); Usage: const ip = getClientIp(request); if (!(await rateLimit(ip, 10))) { // 10 req/min return jsonError("rate_limited", "Too many requests", 429); } Applied limits: Endpoint Limit Window /api/auth/bankid/initiate 10/min 60s /api/auth/bankid/callback 10/min 60s /api/auth/register (deprecated) 10/min 60s /api/auth/login (deprecated) 10/min 60s /api/transactions/remittance 10/min 60s /api/transactions/qr-payment 10/min 60s /api/rates 120/min 60s /api/rates/[currency] 120/min 60s 2.4 getClientIp(request) Source: middleware.ts:33–35 Extracts the client's real IP address from the x-forwarded-for header (first IP in the chain — the originating client). Falls back to '127.0.0.1' if header not present. Note: When behind App Runner (AWS managed proxy), x-forwarded-for is set automatically with the real client IP. 2.5 jsonError(error, message, status, details?) Source: middleware.ts:37–39 Creates a standardized JSON error NextResponse . return jsonError("validation_error", "Validation failed", 422, ["Email required"]); // Response body: { "error": "validation_error", "message": "Validation failed", "details": ["Email required"] } 2.6 revokeAllSessions(userId) Source: middleware.ts:83–85 Sets revoked=1 on all sessions for a user. Called by POST /api/auth/logout . UPDATE sessions SET revoked = 1 WHERE user_id = $1; 2.7 generateCsrfToken() / validateCsrf(request, token) Source: middleware.ts:88–99 CSRF token generation (32 random bytes hex-encoded) and validation via x-csrf-token header. Status: Implemented but not actively required on any route. CSRF protection is handled via: BankID OIDC state parameter (login flow) Origin header validation (in requireAuth ) 3. Middleware Library ( lib/middleware/ ) 3.1 Error Handler ( middleware/error-handler.ts ) AppError class: class AppError extends Error { constructor( public code: string, message: string, public status: number = 500, public details?: unknown ) {} } Predefined error constructors: Constructor Code HTTP Status Errors.unauthorized(msg?) UNAUTHORIZED 401 Errors.forbidden(msg?) FORBIDDEN 403 Errors.notFound(resource) NOT_FOUND 404 Errors.badRequest(msg, details?) BAD_REQUEST 400 Errors.conflict(msg) CONFLICT 409 Errors.tooManyRequests(msg?) RATE_LIMIT_EXCEEDED 429 Errors.internal(msg?) INTERNAL_ERROR 500 Error response format: { "error": { "code": "BAD_REQUEST", "message": "Amount must be between 100 and 50000 NOK", "details": "validation_error" } } Production masking: createErrorResponse() masks internal error messages in production — only returns "An unexpected error occurred" for 500 errors. 3.2 Auth Middleware ( middleware/auth-middleware.ts ) Alternative auth middleware for mobile clients using Bearer token pattern. requireAuth(request) : Extracts JWT from Authorization: Bearer header Verifies JWT signature + expiry Returns userId from payload In-memory rate limiter (for Bearer token routes): DEFAULT_RATE_LIMIT : 100 req/min STRICT_RATE_LIMIT : 10 req/min Auto-cleanup every 5 minutes Rate limit headers: X-RateLimit-Limit , X-RateLimit-Remaining , X-RateLimit-Reset getClientIP(request) : Checks X-Forwarded-For → X-Real-IP → falls back to 'unknown' . 3.3 Validation ( middleware/validation.ts ) Input validation functions — no external dependencies, all custom implementations. Function Description Rules validatePhone(phone) International phone Starts with + , 8–15 digits validateAmount(amount) Positive monetary amount > 0 , max 2 decimal places validateIBAN(iban) European IBAN Country code + alphanumeric, mod-97 checksum validatePIN(pin) Card PIN Exactly 4 digits validateEmail(email) Email address Basic x@y.z pattern validateCurrency(currency) ISO 4217 code Whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR validateDateISO(date) ISO 8601 date Parseable by Date.parse() validateName(name) Name field 1–100 chars, at least one letter, XSS-safe validateLanguage(lang) Language code Whitelist: nb, en, bs, sq sanitizeText(text, maxLength?) Text sanitization Strips HTML tags + control chars, trims, enforces max length (default 500) validate(condition, msg) Assert helper Throws AppError (400) if false required(value, name) Required field check Throws AppError (400) if null/undefined Security notes: validateName checks for: