High-Level Design (HLD)
High-Level Design Document
Project: {{PROJECT_NAME}}Drop
Version: {{VERSION}}1.0
Date: {{DATE}}2026-02-23
Author: {{AUTHOR}}Petter Graff, Senior Enterprise Architect
Status: Draft | In Review | Approved
Reviewers: {{REVIEWERS}}Alem Bašić (CEO), John (AI Director)
Document History
| Version |
Date |
Author |
Changes |
| 0.1 |
{{DATE}}2026-02-21 |
{{AUTHOR}}Standards Architect |
Initial draft from source code analysis |
| 1.0 |
2026-02-23 |
Petter Graff |
Filled from real architecture docs |
1. Executive Summary
Purpose: {{ONE_LINE_DESCRIPTION_OF_SYSTEM}}Drop is a PSD2 pass-through payment application that enables Norwegian residents (18+) to send money internationally (remittance) and pay merchants via QR code — without Drop ever holding customer funds.
Business Context: {{WHY_THIS_SYSTEM_EXISTS}}Sending money from Norway is expensive and complex. Diaspora communities and internationally connected residents pay high fees through traditional remittance services. Drop removes that friction by operating as a licensed AISP (Account Information Service Provider) and PISP (Payment Initiation Service Provider) under PSD2 / Betalingstjenesteloven — reading balances directly from users' banks and initiating payments on their behalf. ALAI Holding AS builds Drop as a product for the Norwegian market, targeting all residents of Norway and Scandinavia, not just diaspora communities.
Key Outcomes:
{{OUTCOME_1}}Users send money to 30+ countries at lower fees (0.5% vs. industry 2-5%)
{{OUTCOME_2}}Merchants accept QR payments without POS hardware — mobile-first
{{OUTCOME_3}}Drop avoids EMI licensing complexity (350K EUR capital requirement) by adopting the PISP/AISP pass-through model (20-50K EUR capital requirement)
- Strong regulatory compliance: BankID SCA, Sumsub KYC/AML, GDPR, AML (hvitvaskingsloven)
Scope: This document covers {{IN_SCOPE}}the Drop platform — web app (drop-web), API server (drop-api), and mobile app (drop-mobile) — and their integrations with BankID, Open Banking (AISP/PISP), Sumsub KYC, and payment rails. It excludes {{OUT_OF_SCOPE}}.the Drop landing/marketing site, the Cards feature (feature-flagged, future), and future Vipps Login integration.
2. System Context (C4 Level 1)
C4Contextgraph titleTB
subgraph actors["External Actors"]
sender["Sender<br/>(Norwegian Resident, 18+)<br/>Sends money abroad via PISP"]
receiver["Receiver<br/>(30+ countries)<br/>Receives remittance"]
merchant["Merchant<br/>(Norwegian Business)<br/>Accepts QR payments"]
end
subgraph drop_system["Drop Payment System Context(ALAI Holding AS)"]
drop["Drop<br/>Next.js 15 + Hono v4 + Expo SDK 54<br/>PSD2 Pass-through App<br/>(AISP + PISP)"]
end
subgraph banking["Banking & Open Banking"]
bankid["BankID Norway<br/>OIDC Identity Provider<br/>Strong Customer Authentication"]
nordic_banks["Nordic Banks<br/>(DNB, SpareBank1, Nordea)<br/>Berlin Group NextGenPSD2 APIs<br/>AISP: Read balance<br/>PISP: Initiate payment"]
payment_rails["Payment Rails<br/>SEPA (EEA) / SWIFT (non-EEA)<br/>30+ remittance corridors"]
end
subgraph compliance["Compliance & KYC"]
sumsub["Sumsub<br/>KYC/AML Provider<br/>Document verification<br/>PEP/sanctions screening"]
finanstilsynet["Finanstilsynet<br/>Norwegian FSA<br/>PISP/AISP registration<br/>Regulatory oversight"]
okokrim["Okokrim / EFE<br/>Financial Intelligence Unit<br/>STR/SAR filing"]
end
subgraph infrastructure["Infrastructure"]
aws["AWS App Runner<br/>eu-north-1 (Stockholm)<br/>Container hosting + auto-scaling"]
cloudflare["Cloudflare<br/>CDN, WAF, DDoS protection<br/>DNS, TLS termination<br/>getdrop.no"]
sentry["Sentry<br/>Error tracking<br/>Performance monitoring"]
end
sender -->|"BankID login, view balance (AISP), send money (PISP), QR payments"| drop
receiver -.->|"Receives funds via bank transfer"| payment_rails
merchant -->|"Register business, view dashboard, generate QR code"| drop
drop -->|"OIDC authorize, ID token verification, age/identity check"| bankid
drop -->|"AISP: GET /accounts /balances; PISP: POST /payments"| nordic_banks
drop -->|"PISP payment routing — {{PROJECT_NAME}}SEPA Person(user,for EEA, SWIFT for non-EEA"| payment_rails
drop -->|"{{PRIMARY_USER_TYPE}}Applicant creation, document upload, webhook results"| sumsub
drop -.->|",License registration, regulatory reporting"| finanstilsynet
drop -.->|"{{USER_DESCRIPTION}}STR filing (hvitvaskingsloven)")| Person(admin,okokrim
drop -->|"SystemDeploy Administrator",containers, auto-scale"| aws
drop -->|"ManagesDNS androuting, configuresTLS, theWAF, system")DDoS System(system,protection"| cloudflare
drop -->|"{{SYSTEM_NAME}}Error events, performance traces"| sentry
nordic_banks -->|",Execute "{{SYSTEM_SHORT_DESCRIPTION}}")transfers"| System_Ext(extSystem1, "{{EXTERNAL_SYSTEM_1}}", "{{EXT_SYSTEM_1_DESCRIPTION}}")
System_Ext(extSystem2, "{{EXTERNAL_SYSTEM_2}}", "{{EXT_SYSTEM_2_DESCRIPTION}}")
System_Ext(emailSystem, "Email Provider", "Sends transactional emails")
Rel(user, system, "Uses", "HTTPS")
Rel(admin, system, "Manages", "HTTPS")
Rel(system, extSystem1, "Calls", "REST/HTTPS")
Rel(system, extSystem2, "Publishes events to", "AMQP")
Rel(system, emailSystem, "Sends emails via", "SMTP/API")payment_rails
3. Container Diagram (C4 Level 2)
C4Container
title Drop — Container Diagram —(C4 {{PROJECT_NAME}}Level 2)
Person(user, "{{PRIMARY_USER_TYPE}}End User", "Norwegian resident 18+, authenticated via BankID")
Container_Boundary(system,Person(merchant, "{{SYSTEM_NAME}}Merchant", "Business owner receiving QR payments")
System_Boundary(drop, "Drop Platform") {
Container(webApp,web, "Web Application"drop-web", "{{FRONTEND_TECH}}"Next.js 15, React 19, Tailwind v4", "Single-pageSSR applicationweb servedapp. to10 users"screens: Login, Onboarding, Dashboard, SendMoney, BankAccounts, TransactionHistory, ScanQR, Profile, Notifications, MerchantDashboard. BankID auth via httpOnly cookie.")
Container(api, "drop-api", "Hono v4, Node.js 22 Alpine", "REST API Gatewayserver. 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 iOS/Android app. BankID auth via expo-web-browser deep linking (drop://auth/callback). AsyncStorage for token. 4 tabs: Hjem, Send, QR, Profil.")
ContainerDb(db, "Database", "SQLite (dev) / Backend",PostgreSQL "{{BACKEND_TECH}}16 (prod)", "Handles19 businesstables: logicusers, andsessions, orchestration")transactions, Container(workerService,bank_accounts, recipients, merchants, notifications, settings, cards, spending_limits, exchange_rates, audit_log, aml_alerts, str_reports, screening_results, consents, data_access_requests, complaints, rate_limits."Background Worker", "{{WORKER_TECH}}", "Processes async jobs and scheduled tasks")
ContainerDb(database, "Primary Database", "{{DB_TECH}}", "Stores persistent application data")
ContainerDb(cache, "Cache Layer", "Redis", "Session storage and hot data caching")
Container(messageQueue, "Message Queue", "{{QUEUE_TECH}}", "Async event bus between services")
}
System_Ext(extApi,bankid, "{{EXTERNAL_API}}"BankID OIDC", "Third-partyNorwegian integration"eID provider. OIDC authorize/token/JWKS endpoints. auth.bankid.no (prod).")
System_Ext(sumsub, "Sumsub", "KYC/AML identity verification. WebSDK (web), React Native SDK (mobile), webhooks for status updates.")
System_Ext(openbanking, "Open Banking APIs", "Berlin Group NextGenPSD2. AISP (balance reads) and PISP (payment initiation) via Neonomics aggregator (planned).")
System_Ext(sepa, "SEPA/SWIFT Networks", "International payment rails for remittance settlement to 30+ countries.")
Rel(user, webApp,web, "Visits"HTTPS", "HTTPS"Browser — getdrop.no")
Rel(webApp,user, mobile, "HTTPS", "iOS/Android app")
Rel(merchant, web, "HTTPS", "Merchant dashboard")
Rel(web, api, "Calls"HTTPS REST", "REST/HTTPS"/api/* and /v1/* endpoints, JSON, httpOnly cookie")
Rel(mobile, api, "HTTPS REST", "/v1/* endpoints, JSON, Bearer token")
Rel(api, database,db, "Reads/Writes"SQL", "TCP"Parameterized queries via db.ts dual-driver abstraction")
Rel(api, cache,bankid, "Reads/Writes"OIDC", "TCP"Authorization code flow, JWKS token verification")
Rel(api, messageQueue,sumsub, "PublishesREST events"+ Webhooks", "AMQP")Applicant Rel(workerService,creation, messageQueue,document "Consumeschecks, events",HMAC-verified "AMQP")
Rel(workerService, database, "Reads/Writes", "TCP"webhooks")
Rel(api, extApi,openbanking, "Calls"Berlin Group NextGenPSD2", "REST/HTTPS"AISP balance reads, PISP payment initiation with SCA")
Rel(api, sepa, "ISO 20022 (via banking partner)", "Remittance settlement to 30+ countries")
4. Component Overview
| Component |
Responsibility |
Technology |
Owner Team |
{{COMPONENT_1}}drop-web |
{{RESPONSIBILITY_1}}SSR web application, user onboarding, dashboard, send money, QR scan, merchant dashboard |
{{TECH_1}}Next.js 15, React 19, Tailwind v4, shadcn/ui |
{{TEAM_1}}ALAI — Frontend |
{{COMPONENT_2}}drop-api |
{{RESPONSIBILITY_2}}REST API, BankID OIDC, JWT sessions, payment processing, KYC/GDPR/AML compliance |
{{TECH_2}}Hono v4, Node.js 22 |
{{TEAM_2}}ALAI — Backend |
{{COMPONENT_3}}drop-mobile |
{{RESPONSIBILITY_3}}Native iOS/Android, BankID auth, send money, QR scan, transaction history |
{{TECH_3}}Expo SDK 54, React Native |
{{TEAM_3}}ALAI — Mobile |
| Database |
Persistent storage, 19 tables, dual-driver (SQLite/PostgreSQL) |
SQLite 3 (dev) / PostgreSQL 16 (prod) |
ALAI — Backend |
| BankID OIDC |
Strong Customer Authentication (SCA), Norwegian identity provider |
OIDC 1.0 |
BankID Norge |
| Sumsub KYC |
Document verification, PEP/sanctions screening, AML risk scoring |
Sumsub API + SDK |
Sumsub |
| Open Banking (AISP/PISP) |
Balance reads from user's bank, payment initiation from user's bank |
Berlin Group NextGenPSD2, Neonomics aggregator |
ALAI + Neonomics |
Component Descriptions
{{COMPONENT_1}}drop-web
Responsibility: {{DETAILED_RESPONSIBILITY}}Server-side rendered web application serving all 10 core screens. Handles the BankID OIDC redirect initiation, authentication callback (sets httpOnly cookie), and renders the full UI from Login to MerchantDashboard. Acts as BFF (Backend For Frontend) for the Next.js API routes at /api/auth/*.
Key Interfaces: {{INTERFACE_DESCRIPTION}}HTTP GET/POST to Next.js API routes (/api/auth/bankid/*); Hono API REST calls (/v1/*) via fetch with cookie credentials.
Rationale: {{WHY_SEPARATE}}Separate from the API to allow independent scaling, enable SSR for SEO on the landing/marketing site, and encapsulate web-specific auth session management (httpOnly cookie).
drop-api
Responsibility: Central REST API serving both web and mobile clients. Owns all business logic: BankID OIDC code exchange, JWT issuance, transaction processing (remittance, QR payment), KYC initiation, GDPR endpoints, merchant management, admin operations, and AML compliance. Applies a 7-step middleware chain on every request.
Key Interfaces: 26+ endpoints under /v1/. External calls to BankID token endpoint, Sumsub API, and Open Banking PISP/AISP.
Rationale: Single source of business logic truth, consumed by both web (cookie auth) and mobile (Bearer token auth). Hono v4 chosen for performance on Node.js 22 (see ADR-008).
drop-mobile
Responsibility: Native iOS and Android application. Provides the core payment features: BankID login, dashboard with balance, send money, QR scanner, transaction history, and profile management.
Key Interfaces: Same Hono API /v1/* endpoints as web, using Bearer token (Authorization: Bearer <jwt>) instead of cookies. BankID auth via expo-web-browser + deep link drop://auth/callback.
Rationale: Separate from drop-web to allow platform-native UX, native push notifications (future), and biometric auth (future).
5. Technology Stack
| Layer |
Technology |
Version |
Rationale |
| Frontend Framework |
{{FE_FRAMEWORK}}Next.js |
{{VERSION}}15 (App Router) |
{{RATIONALE}}SSR + RSC for performance; BFF capability for auth cookie management |
UI Component LibraryFramework |
{{UI_LIB}}React |
{{VERSION}}19 |
{{RATIONALE}}Concurrent features, server components |
| Styling |
Tailwind CSS |
v4 |
Utility-first, design token support |
| UI Components |
shadcn/ui (Radix UI) |
Latest |
Accessible primitives, keyboard nav, unstyled baseline |
| Mobile Framework |
Expo (React Native) |
SDK 54 |
Cross-platform iOS/Android, managed workflow, OTA updates |
| Backend Language |
{{LANG}}TypeScript / Node.js |
{{VERSION}}Node 22 LTS |
{{RATIONALE}}Type safety end-to-end, team expertise, shared types with frontend |
| Backend Framework |
{{BE_FRAMEWORK}}Hono |
{{VERSION}}v4 |
{{RATIONALE}}Ultrafast edge-compatible framework; better performance than Express; native middleware chaining |
| Primary Database (prod) |
{{DB}}PostgreSQL |
{{VERSION}}16 |
{{RATIONALE}}ACID compliance, row-level security, rich indexing, AWS RDS managed |
CacheDevelopment Database |
{{CACHE}}SQLite (better-sqlite3) |
{{VERSION}}3.x |
{{RATIONALE}}Zero-config local dev, WAL mode, dual-driver abstraction switches transparently |
Message QueueAuthentication |
{{QUEUE}}BankID OIDC + jose |
{{VERSION}}2.0 |
{{RATIONALE}}Norwegian legal requirement for SCA; jose for JWKS verification |
Search EngineKYC/AML |
{{SEARCH}}Sumsub |
{{VERSION}}API v1 |
{{RATIONALE}}Document verification, PEP/sanctions, Norwegian compliance coverage |
ObjectOpen StorageBanking |
{{STORAGE}}Berlin Group NextGenPSD2 via Neonomics (planned) |
{{VERSION}}v1.3.12+ |
{{RATIONALE}}PSD2 AISP/PISP; Neonomics aggregator for Nordic bank coverage |
| Error Tracking |
Sentry |
SDK v8 |
Full-stack error capture, session replay, performance tracing |
| Container Runtime |
{{CONTAINER}}Docker |
{{VERSION}}24+ |
{{RATIONALE}}Multi-stage build (4 stages), non-root user, Node 22 Alpine |
| Orchestration |
{{ORCHESTRATION}}AWS App Runner |
{{VERSION}}- |
{{RATIONALE}}Auto-scaling, managed TLS, no Kubernetes operational overhead |
APIEdge Gateway/ CDN |
{{GATEWAY}}Cloudflare |
{{VERSION}}- |
{{RATIONALE}}WAF, DDoS protection, CDN for static assets, geo-blocking |
Auth ProviderSecrets |
{{AUTH}}AWS Secrets Manager |
{{VERSION}}- |
{{RATIONALE}} | JWT_SECRET,
BANKID_CLIENT_SECRET, DATABASE_URL, Observability |
{{OBSERVABILITY}} |
{{VERSION}} |
{{RATIONALE}}SENTRY_DSN |
| CI/CD |
{{CICD}}GitHub Actions (planned) |
{{VERSION}}- |
{{RATIONALE}}Automated: tsc → lint → vitest → Docker build → ECR push → App Runner deploy |
6. Data Flow Overview
6.1 PrimaryRemittance WritePayment Flow (Write)
flowchart LR
A([User — Web/Mobile]) -->|"POST /v1/transactions/remittance"| B[Hono API]
B -->|"1. Verify JWT + session"| C[(PostgreSQL)]
B -->|"2. Validate: KYC approved, recipient exists, amount 100-50000 NOK"| B
B -->|"3. Lookup exchange rate"| C
B -->|"4. Begin atomic transaction"| C
C -->|"INSERT transactions status=processing"| C
C -->|"INSERT audit_log"| C
C -->|"INSERT notifications"| C
B -->|"5. Initiate PISP payment"| D[Open Banking API]
D -->|"SCA redirect URL"| B
B -->|"6. Return 201 + redirect"| A
A -->|"7. User completes BankID SCA at bank"| D
D -->|"8. Webhook: payment confirmed"| B
B -->|"9. UPDATE transactions status=completed"| C
6.2 Balance Read Flow (Read — AISP)
flowchart LR
A([User]) -->|HTTPS"GET POST|/api/auth/me"| B[APINext.js Gateway]BFF / Hono API]
B -->|Authenticate|"Verify JWT cookie"| C[Auth Service](PostgreSQL)]
C -->|JWT"bank_accounts.balance validated|(cached)"| B
B -->|Route"If request|stale: GET /v1/accounts/{id}/balances"| D[BusinessOpen Service]Banking AISP]
D -->|Validate"Live input| D
D -->balance"|Write| E[(Database)]
D -->|Publish event| F[Message Queue]
F -->|Consume| G[Worker Service]
G -->|Side effects| H[External APIs]
G -->|Notify| I[Email/Push]
D -->|Cache invalidate| J[(Cache)]
D -->|Return 201| B
B -->|Response|"UPDATE Abank_accounts
SET 6.2balance, Primarybalance_synced_at"| Read Flow
flowchart LR
A([User]) -->|HTTPS GET| B[API Gateway]C
B -->|Authenticate| C[Auth Service]
B -->|Route| D[Business Service]
D -->|Cache check| E[(Redis Cache)]
E -->|Cache hit| D
E -->|Cache miss| F[(Database)]
F -->|Read| D
D -->|Populate cache| E
D -->|"Return 200|{totalBalance, accounts}"| A
7. Integration Points
7.1 External Integrations
| System |
Direction |
Protocol |
Auth |
Data Exchanged |
SLA/Criticality |
{{EXT_SYSTEM_1}}BankID OIDC |
Outbound |
REST/OIDC 1.0 / HTTPS |
APIClient KeyID + Client Secret (code flow) |
{{DATA}}ID token (pid, name, birthdate), access token |
{{SLA}}99.9% / {{CRITICALITY}}Critical — all auth blocked if down |
{{EXT_SYSTEM_2}}Sumsub KYC |
Outbound + Inbound webhooks |
REST HTTPS + Webhooks |
HMACAPI token + HMAC-SHA256 |
{{DATA}}Applicant data, documents, verification results, risk scores |
{{SLA}}99.5% / {{CRITICALITY}}High — new registrations blocked |
{{EXT_SYSTEM_3}}Open Banking (Neonomics/ASPSP) |
BidirectionalOutbound |
gRPCBerlin Group NextGenPSD2 / HTTPS |
mTLSeIDAS QWAC cert + OAuth2 |
{{DATA}}Account lists, balances (AISP); payment initiations, payment status (PISP) |
{{SLA}}99.5% / {{CRITICALITY}}Critical — payments blocked if PISP down; AISP degrades to cached balance |
| SEPA/SWIFT |
Outbound via banking partner |
ISO 20022 |
Banking partner credentials |
Remittance transfers (amounts, IBANs, reference) |
Best-effort / High — delays expected on bank outages |
| Cloudflare |
Inbound (proxied) |
DNS + HTTPS |
Cloudflare API key |
HTTP traffic, TLS, WAF rules |
99.99% / Critical — all traffic routed via Cloudflare |
| AWS Secrets Manager |
Outbound |
HTTPS |
IAM role |
JWT_SECRET, BANKID_CLIENT_SECRET, DATABASE_URL, SENTRY_DSN |
99.99% / Critical — startup fails if unavailable |
| Sentry |
Outbound |
HTTPS (SDK) |
DSN token |
Error events, stack traces, performance traces |
Best-effort / Low — observability only |
7.2 Internal Service Integrations
| Service |
Integration Type |
Protocol |
Notes |
{{INTERNAL_SERVICE_1}}drop-web → drop-api |
Synchronous |
REST HTTPS |
{{NOTES}}Web auth via httpOnly cookie (drop_token); API calls to /v1/* |
{{INTERNAL_SERVICE_2}}drop-mobile → drop-api |
AsynchronousSynchronous |
EventsREST HTTPS |
{{NOTES}}Bearer token in Authorization header; same /v1/* API endpoints |
| drop-api → PostgreSQL |
Synchronous |
TCP (SQL) |
db.ts dual-driver abstraction; parameterized queries only |
8. Deployment Overview
flowchart TB
subgraph Internet
CDN[CDNUsers[End /Users Edge— Cache]Browser DNS[DNS]+ Mobile]
end
subgraph Cloud[Cloudflare["CloudCloudflare ProviderEdge (getdrop.no)"]
DNS[DNS]
CDN[CDN — {{CLOUD_PROVIDER}}Static Assets /_next/static/*]
WAF[WAF — OWASP CRS + custom rules]
DDoS[DDoS Protection L3/L4/L7]
end
subgraph AWS["AWS eu-north-1 (Stockholm)"]
subgraph LoadBalancer[AppRunner["LoadAWS BalancerApp Layer"]Runner LB[Application Load Balancer]
end
subgraph AppTier["Application Tier — {{REGION}}(PLANNED)"]
directionWebApp[drop-web<br/>Next.js LR15 API1[APIstandalone<br/>Node.js Pod22 1]Alpine<br/>Port API2[API3000<br/>1-5 Podinstances]
2]API[drop-api<br/>Hono API3[APIv4<br/>Node.js Pod22 N]Alpine<br/>Port end3001<br/>1-10 subgraph WorkerTier["Worker Tier"]
W1[Worker Pod 1]
W2[Worker Pod N]instances]
end
subgraph DataTier["Data Tier"]
DB_PRIMARY[RDS[(DBRDS Primary)PostgreSQL 16<br/>db.t3.medium → db.r6g.large<br/>Multi-AZ — prod<br/>100GB gp3, auto-scale to 500GB<br/>30-day backup retention)]
DB_REPLICA[(DB Replica)]
REDIS[(Redis Cluster)]
MQ[Message Queue]
end
subgraph Observability[Supporting["Observability Stack"Supporting"]
LOGS[LogECR[ECR Aggregator]— METRICS[MetricsContainer Registry<br/>Image scanning enabled]
SM[Secrets Manager<br/>JWT_SECRET / Prometheus]BANKID_CLIENT_SECRET<br/>DATABASE_URL TRACES[Distributed/ Tracing]SENTRY_DSN]
CW[CloudWatch<br/>Logs + Metrics + Alarms]
end
end
Users --> DNS
DNS --> CDN
CDN --> LBWAF
LBWAF --> API1DDoS
& API2 & API3
API1 & API2 & API3DDoS --> DB_PRIMARYWebApp
API1 & API2 & API3DDoS --> REDISAPI
API1 & API2 & API3WebApp --> MQRDS
DB_PRIMARYAPI --> DB_REPLICARDS
MQAppRunner --> W1ECR
& W2
API1 & API2 & API3AppRunner --> LOGSSM
&AppRunner METRICS--> & TRACESCW
Environments
| Environment |
URL |
Purpose |
Database |
BankID |
Scale |
| Development |
http://localhost:{{PORT}}3000 + :3001 |
Local dev via docker compose up |
SQLite (./data/drop.db) |
Mock (BANKID_MOCK=true) |
Single instance |
| Staging |
https://staging.{{DOMAIN}}getdrop.no |
Pre-prodrelease testingvalidation, QA, E2E |
MinimalRDS PostgreSQL (separate) |
BankID test environment |
1 replica)replica |
| Production |
https://{{DOMAIN}}getdrop.no |
Live traffic |
RDS PostgreSQL Multi-AZ |
BankID production |
Auto-scaled (1-5 web, 1-10 API) |
9. Cross-Cutting Concerns
9.1 Authentication & Authorization
- Strategy:
{{AUTH_STRATEGY}}BankID OIDC (e.g.,Authorization JWTCode BearerFlow) tokens— /email/password OAuth2removed, /returns Session-based)410 Gone
- Identity Provider:
{{IDP}}BankID Norge (e.g.,auth.bankid.no) Auth0,— Keycloak,OIDC custom)1.0, JWKS-verified ID tokens
- Authorization Model:
{{AUTHZ_MODEL}}Role-based — user (e.g.,default) RBAC,vs ABAC)merchant (gated). Middleware authMiddleware + merchantMiddleware enforce per-endpoint.
- Token Lifetime:
Access:Web: {{ACCESS_TTL}}24h |httpOnly Refresh:cookie {{REFRESH_TTL}}(drop_token); Mobile: 7d Bearer JWT in AsyncStorage
- MFA:
{{MFA_REQUIRED}}Yes — {{MFA_METHOD}}BankID provides strong two-factor SCA (possession + knowledge/inherence) on every login and payment
9.2 Logging
- Framework:
{{LOGGING_FRAMEWORK}}Hono native + console.log, captured by Sentry
- Format: JSON structured logs where available; request ID propagated via
x-request-id header
- Levels:
DEBUGERROR/WARN (dev),sent to Sentry; INFO (staging/prod),to WARN/ERROR (alerts)CloudWatch
- Correlation IDs:
X-Request-IDx-request-id generated per request (UUID), echoed in response header propagated across all services
- Retention:
{{LOG_RETENTION_DAYS}}90 days in {{LOG_STORAGE}}CloudWatch
- PII Handling: National IDs stored as SHA-256 hash only; raw PII
fieldsnever masked/redacted before logginglogged
9.3 Error Handling
- API Errors:
RFCJSON 7807envelope Problem{ Detailserror: format"code", message: "...", details: [...] }
- Retry Strategy:
ExponentialExternal API calls: exponential backoff with[1s, jitter2s, (4s], max {{MAX_RETRIES}}3 retries)retries
- Circuit Breaker:
EnabledOpen onBanking externalAPI: calls3 —failures threshold:in {{CB_THRESHOLD}}%60s failure→ rate60s cooldown
DeadGlobal LetterError Queue:Handler: Failedmiddleware/error-handler.ts messages— →catches DLQall withunhandled {{DLQ_RETENTION}}errors, retentionlogs to Sentry, returns 500
9.4 Caching
Strategy: Cache-aside pattern
Cache Invalidation: {{INVALIDATION_STRATEGY}}
TTLs: Session: {{SESSION_TTL}} | API responses: {{API_CACHE_TTL}} | Reference data: {{REF_TTL}}
Cache Penetration Protection: Bloom filter / null value caching
9.5 Rate Limiting
- Implementation:
{{RATE_LIMIT_IMPLEMENTATION}}Redis-less; (e.g.,DB-backed Redisrate_limits slidingtable window)with SQLite/PostgreSQL dual support
- Default Limits:
{{REQUESTS_PER_MINUTE}}Auth endpoints: 10 req/min60s per IP; Transactions: 10 req/60s per IP |+ {{AUTH_REQUESTS_PER_MINUTE}}3 per-user; Exchange rates: 120 req/min60s
per- Cloudflare
authenticatedWAF: user/v1/auth/* → challenge at 20 req/10s; /v1/transactions/* → block at 30 req/10s
- Response: HTTP 429
with(no Retry-After header currently; planned)
9.65 Secrets Management
9.6 Feature Flags
10. Quality Attributes & Architectural Trade-offs
| Quality Attribute |
Target |
Approach |
Trade-off |
| Availability |
{{SLA_PERCENT}}99.5% uptime |
AWS App Runner multi-instance, Cloudflare 99.99% edge, RDS Multi-AZ deployment, health checks, auto-restart |
Higher infrastructureAWS cost vs single-AZ |
| Performance (p99 latency) |
< {{P99_LATENCY}}ms200ms API responses |
Caching,No queryexternal optimization,cache (SQLite WAL / PostgreSQL handles load), Cloudflare CDN for static assets |
CacheNo invalidationRedis complexitycache — acceptable at current scale |
| Scalability |
{{CONCURRENT_USERS}}1,000 concurrent users (MVP) |
HorizontalApp scaling,Runner auto-scale: 1-10 API instances, 1-5 web instances; stateless servicesAPI (JWT) |
DistributedAll-or-nothing statescaling challenges(monolith) |
| Security |
OWASP Top 10 compliant |
Cloudflare WAF, inputparameterized validation,SQL, RBAChttpOnly cookies, BankID SCA, HMAC webhooks |
AddedBankID latencyadds fromauth securityflow checkscomplexity |
| Regulatory Compliance |
PSD2, GDPR, AML (hvitvaskingsloven) |
BankID SCA for payments, Sumsub KYC, 19-table compliance schema, STR filing |
Compliance overhead slows feature delivery |
| Maintainability |
{{DEPLOY_FREQUENCY}}Weekly deploys/weekdeploys |
CI/CDMonolith-first pipeline,(ADR-005), vitest test coveragesuite, >TypeScript {{TEST_COVERAGE}}%strict mode |
InitialModule investmentboundary inerosion toolingrisk without process isolation |
| Data Consistency |
{{CONSISTENCY_MODEL}}Strong (per transaction) |
{{CONSISTENCY_APPROACH}}Atomic DB transactions for all financial operations, idempotency keys on payments |
{{CONSISTENCY_TRADEOFF}}No eventual consistency — simpler but single-DB dependency |
11. Key Architectural Decisions
| ADR |
Decision |
Status |
Date |
| ADR-001 |
{{DECISION_SUMMARY_1}}Consolidate to single Hono backend (remove dual middleware) |
Accepted |
{{DATE}} |
ADR-002 |
{{DECISION_SUMMARY_2}} |
Accepted |
{{DATE}}2026-02-12 |
| ADR-003 |
{{DECISION_SUMMARY_3}}Adopt PSD2 pass-through model — no wallet, no held funds |
ProposedAccepted |
{{DATE}}2026-02-12 |
| ADR-004 |
JWT in httpOnly cookies (web) + Bearer tokens (mobile) |
Accepted |
2026-02-12 |
| ADR-005 |
Monolith-first architecture — extract microservices when team/scale demands |
Accepted |
2026-02-21 |
| ADR-006 |
Dual-driver DB abstraction: SQLite (dev) / PostgreSQL (prod) |
Accepted |
2026-02-21 |
| ADR-007 |
BankID as sole identity provider (email/password removed) |
Accepted |
2026-02-21 |
| ADR-008 |
Hono v4 as the API framework |
Accepted |
2026-02-21 |
| ADR-012 |
AWS App Runner for container hosting |
Accepted |
2026-02-21 |
12. Constraints & Assumptions
12.1 Constraints
| # |
Constraint |
Category |
Impact |
| C1 |
{{CONSTRAINT_1}}Users must be Norwegian residents (18+) with Norwegian BankID and +47 phone number |
Technical/Regulatory/BusinessRegulatory |
{{IMPACT}}Limits market to Norway; no international expansion without separate licensing |
| C2 |
{{CONSTRAINT_2}}Drop must never hold customer funds (PSD2 pass-through model) |
Technical/Regulatory/BusinessRegulatory |
{{IMPACT}}PISP/AISP architecture mandatory; wallet model legally excluded |
| C3 |
{{CONSTRAINT_3}}BankID SCA required for every financial operation (PISP payment) |
Technical/Regulatory/Regulatory / PSD2 RTS |
Each payment requires bank SCA redirect — adds UX friction |
| C4 |
5-year AML data retention (hvitvaskingsloven) |
Regulatory |
Compliance tables cannot be purged; storage costs grow over time |
| C5 |
GDPR Art. 17 right to erasure — soft delete + 5yr AML retention override |
Regulatory |
Cannot hard-delete user data if AML records exist |
| C6 |
Finanstilsynet PISP/AISP license not yet obtained (Phase 2 blocker) |
Regulatory |
Live Open Banking API calls not permitted until license or agent arrangement secured |
| C7 |
Monolith-first — all containers deploy together |
Technical |
No independent scaling per module; full deploy required for any change |
| C8 |
Budget: AWS Secrets Manager, App Runner, RDS — cost scales with usage |
Business |
{{IMPACT}}Architecture chosen for low base cost; scales to higher tiers on growth |
12.2 Assumptions
| # |
Assumption |
Validation Method |
Risk if Wrong |
| A1 |
{{ASSUMPTION_1}}Neonomics or Tink will provide Open Banking aggregator service for Phase 2 Nordic bank connectivity |
{{HOW_TO_VALIDATE}}Contract negotiation in Phase 2 |
{{RISK}}Direct per-bank ASPSP integration required (significantly higher effort) |
| A2 |
{{ASSUMPTION_2}}BankID Norge will approve Drop's OIDC client registration |
{{HOW_TO_VALIDATE}}BankID developer portal application |
{{RISK}}Must use Vipps Login or alternative OIDC provider |
| A3 |
PostgreSQL on RDS handles expected transaction volume without read replicas at MVP |
Load testing before production launch |
Must add read replicas or implement caching layer |
| A4 |
App Runner rolling updates are sufficient (no true blue/green needed at MVP scale) |
Monitor during first production deploy |
Must implement custom blue/green via ALB traffic shifting |
13. Risks & Mitigations
| Risk |
Likelihood |
Impact |
Score |
Mitigation |
Contingency |
{{RISK_1}} | Finanstilsynet {{1-5}} | license {{1-5}} | delayed {{L×I}} | (>12 {{MITIGATION}} |
{{CONTINGENCY}} |
{{RISK_2}} |
{{1-5}} |
{{1-5}} |
{{L×I}} |
{{MITIGATION}} |
{{CONTINGENCY}} |
{{RISK_3}} |
{{1-5}} |
{{1-5}} |
{{L×I}} |
{{MITIGATION}} |
{{CONTINGENCY}} |
Single database bottleneckmonths) |
3 |
5 |
15 |
ReadUse replicas,licensed connectionPSP poolingagent arrangement (1-3 months setup) while applying |
AddDemo/mock readmode replicas,continues; implementpartner CQRSwith licensed PSP |
Third-partyBankID APIintegration unavailability | blocked 4 | (client 3 | not 12 |
Circuit breaker, cached fallback |
Fallback to cached data, async retry |
Data breach via injectionapproved) |
2 |
5 |
10 |
InputApply validation,early; parameterizedprepare queries,Vipps WAFLogin as alternative OIDC (same pid claim) |
IncidentVipps Login fallback (same architecture, different OIDC endpoints) |
| Open Banking ASPSP API unavailability (AISP) |
4 |
2 |
8 |
Show cached balance with staleness indicator |
Degrade gracefully: display last-known balance |
| Open Banking ASPSP API unavailability (PISP) |
3 |
5 |
15 |
Circuit breaker; notify user to retry |
Payment cannot proceed — user notified with ETA |
| Single database bottleneck (PostgreSQL) |
2 |
4 |
8 |
Connection pooling, read replicas when needed, App Runner horizontal scale |
Add read replicas, implement CQRS for transaction reads |
| Data breach via SQL injection |
1 |
5 |
5 |
Parameterized queries (db.ts enforces), WAF, no raw SQL strings in routes |
GDPR breach notification within 72h, incident response plan,plan |
GDPR
notification
| Sumsub KYC outage (new user registrations blocked) |
2 |
3 |
6 |
Retry queue; existing approved users unaffected |
Queue new registrations; manual KYC review for priority users |
Approval
| Role |
Name |
Date |
Signature |
| Author |
Petter Graff |
2026-02-23 |
|
| Technical Lead |
John (AI Director) |
|
|
| Security Review |
|
|
|
ArchitectApprover (CEO) |
| Alem |
|
Approver (CTO/Lead) |
Bašić |
|
|