Skip to main content

Backend Architecture

Backend Architecture Document

Project: {{PROJECT_NAME}}Drop Version: {{VERSION}}0.1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}Platform Architect (AI) Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Platform Architect (AI) Initial draft from source code analysis

1. Architecture Pattern

Pattern: {{Modular Monolith | MicroservicesNext.js |App MonolithRouter |with Event-Drivenco-located Microservices}}API routes

Pattern Considered Pros Cons Decision
Monolith (Next.js all-in-one) Simple deploy, lowsingle codebase, lowest latency Scaling bottleneck, teamfrontend/backend couplingcoupled {{Selected/Rejected}}Selected
Modular Monolith Organized,Module isolation within single deploy, module isolationdeploy SharedExtra DBabstraction riskoverhead for small team {{Selected/Rejected}}Partially adopted (lib/ modules)
Microservices Independent scaling,scaling teamper autonomyservice Operational complexitycomplexity, too expensive for MVP {{Selected/Rejected}}Rejected

Rationale:

Drop

TODO:is 3-5a sentencestwo-person explainingMVP (Alem + AI). The Next.js App Router pattern co-locates API routes (app/api/) with the decisionfrontend, consideringenabling teamfull-stack size,development scalein requirements,a andsingle operationalTypeScript maturity.codebase with zero additional runtime complexity. The src/lib/ directory provides module isolation (db, middleware, services, features) without microservice overhead. App Runner handles scaling concerns at the infrastructure level.

Pass-Through Model (Critical Architecture Constraint): Drop NEVER holds customer money. All payments are pass-through via PSD2:

  • AISP (Account Information): reads bank balance from user's real bank account
  • PISP (Payment Initiation): initiates transfers directly from user's bank account
  • bank_accounts.balance = last AISP-read value from external bank (cached for UI display, NOT a Drop-held balance)

2. Technology Stack

pg
Layer Technology Version Notes
Runtime {{Node.js}}js {{20.x22 LTS}}(Alpine) LTS, Dockerfile base image
Framework {{NestJSNext.js /(App Express / Fastify / Hono}}Router) {{10.x}}16.1.6Standalone output for Docker
FrontendReact19.2.3
ORMLanguage {{Prisma / TypeORM / Drizzle}}TypeScript {{5.x}}^5 Strict mode
PrimaryDatabase DB(production)PostgreSQL (via pg)16 (RDS) {{PostgreSQL}} {{16.x}}Managed: {{RDS / Supabase}}^8.18.0
CacheDatabase (MVP/staging) SQLite (via {{Redis}}better-sqlite3) {{7.x}}^12.6.2 Managed:Auto-detected when no {{ElastiCache / Upstash}}DATABASE_URL
Queue{{BullMQ / SQS / RabbitMQ}}{{5.x}}
Search{{Elasticsearch / MeiliSearch / Typesense}}{{8.x}}Optional
File storage{{AWS S3 / Cloudflare R2}}API
Auth {{Custom JWT /(via Auth0 / Supabase Auth}}jose) ^6.1.3HS256, httpOnly cookie
Password hashingbcryptjs^3.0.3Legacy — BankID replaces email/password
Identity (eID)BankID OIDCNorwegian eIDMandatory for all users
KYCSumsub WebSDK + APIProduction-readyOnly connected external service
Open BankingTBD (Swan deprecated)AISP/PISP provider selection pending
StylingTailwind CSS^4
LoggingUI Components {{PinoRadix / Winston}}UI {{8.x}}^1.4.3 Accessible, {{Datadogunstyled / Loki}}primitives
APMIcons {{DatadogLucide / Sentry / Elastic APM}}React ^0.563.0
API docsTheme {{Swagger / OpenAPI 3.1}}next-themes {{3.1}}^0.4.6Dark/light mode
ToastsSonner^2.0.7
Testing (unit)Vitest^4.0.18
Testing (E2E)Playwright^1.58.2
LintingESLint^9

3. ProjectApplication Structure

src/drop-app/
├── modules/                # Feature modules (NestJS) / route handlers (Express)src/
│   ├── users/app/                    # Next.js App Router
│   │   ├── users.module.tsapi/                # API ├──route users.controller.tshandlers (REST │   ├── users.service.ts
│   │   ├── users.repository.ts
│   │   ├── dto/endpoints)
│   │   │   ├── create-user.dto.tsauth/           # BankID OIDC + session management
│   │   │   │   ├── bankid/     # initiate + callback endpoints
│   │   │   │   ├── me/         # current user + bank accounts
│   │   │   │   ├── logout/     # session revocation
│   │   │   │   └── update-user.dto.tsrefresh/    # token refresh
│   │   │   ├── transactions/   # remittance, qr-payment, history, disclosure, receipt
│   │   │   ├── recipients/     # recipient CRUD
│   │   │   ├── rates/          # exchange rates (public)
│   │   │   ├── merchants/      # merchant registration + dashboard
│   │   │   ├── notifications/  # push notification management
│   │   │   ├── settings/       # user preferences
│   │   │   ├── consents/       # GDPR consent management
│   │   │   ├── complaints/     # Finansavtaleloven §3-53 complaints
│   │   │   ├── cards/          # [FUTURE] feature-flagged card management
│   │   │   ├── user/           # GDPR data export + account deletion
│   │   │   └── entities/health/         # health check endpoint
│   │   └── user.entity.(frontend pages)    # Next.js pages
│   └── lib/                    # Shared application library
│       ├── db.ts               # Database abstraction (PostgreSQL + SQLite)
│       ├── middleware.ts        # Auth, rate limiting, CSRF, session revocation
│       ├── middleware/          # Modular middleware library
│       │   ├── auth-middleware.ts  # Bearer token auth (mobile)
│       │   ├── error-handler.ts   # AppError class + error response formatting
│       │   └── validation.ts      # Input validation functions
│       ├── alerts.ts           # Slack alerting + error spike detection
│       ├── secrets.ts          # Pluggable secrets provider (env / Doppler / AWS SM)
│       ├── feature-flags.ts    # Environment-variable-based feature flags
│       ├── features.ts         # Feature tracking system (dev tool)
│       └── services/           # External service integrations
│           ├── index.ts        # Service initialization
│           ├── mock-sumsub.ts  # Sumsub KYC (production-ready)
│           ├── mock-swan.ts    # Swan Open Banking (DEPRECATED)
│           └── mock-stripe.ts  # Stripe Issuing (mock only, FUTURE)
├── tests/                      # Test suite
│   ├── setup.ts                # Vitest setup (NODE_ENV=test, in-memory DB)
│   ├── *.test.ts               # Unit + integration tests
│   └── e2e/                    # Playwright E2E tests
│       ├── user-flows.spec.ts
│       ├── auth/
│   ├── notifications/
│   └── {{FEATURE}}/
├── common/
│   ├── decorators/         # Custom decorators
│   ├── filters/            # Exception filters
│   ├── guards/             # Auth / role guards
│   ├── interceptors/       # Logging, transform interceptors
│   ├── pipes/              # Validation pipes
│   └── middleware/         # Request middleware
├── database/
│   ├── migrations/
│   └── seeds/
├── config/
│   ├── app.config.ts
│   ├── database.config.full-flows.spec.ts
│       └── redis.config.input-chaos.spec.ts
└── scripts/
    ├── jobs/backup.sh               # BackgroundSQLite jobbackup definitionsscript
    └── main.tsqa-report.js            # ApplicationQA entrymetrics point

test/
├── unit/
├── integration/
└── e2e/generator

4. RequestDatabase Processing PipelineLayer

4.1
Dual-Database Architecture

Drop auto-detects the database driver at startup:

  • sequenceDiagramDATABASE_URL participantset Client participantPostgreSQL Gateway(pg asdriver)
  • API
  • DATABASE_URL Gatewaynot /set LB participantSQLite Middleware(better-sqlite3)
  • as
Middleware

Source: Stacksrc/lib/db.ts

participant

4.2 Guard as Guards participant Pipe as Validation Pipe participant Controller participant Service participant Repository participant DB asKey Database Client->>Gateway: HTTP Request Gateway->>Middleware: Forward (with tracing headers) Middleware->>Middleware: Request ID, CORS, Security Headers, Rate Limit Middleware->>Guard: Authenticated request Guard->>Guard: JWT verification, Role check Guard->>Pipe: Authorized request Pipe->>Pipe: Schema validation (Zod/class-validator) Pipe->>Controller: Validated DTO Controller->>Service: Business method call Service->>Repository: Data access call Repository->>DB: Query DB-->>Repository: Result Repository-->>Service: Domain entity Service-->>Controller: Response data Controller-->>Client: HTTP Response (transformed)

5. Middleware Stack Configuration

Execution order (applied left-to-right):

Tables :BankID = : ,
OrderMiddlewareTable Purpose Global?Notes
1usersUser accounts, KYC status, BankID linkage helmetkyc_status Securitypending/approved/rejected; headersnational_id_hash: (CSP,SHA-256 HSTS,of etc.) Yespid
2corssessions CORSJWT policysession enforcementtracking + revocation YesSHA-256 hash of JWT, revoked flag
3bank_accountsLinked bank accounts (AISP data) request-idInject X-Request-IDbalance header Yeslast AISP read (NOT Drop-held funds)
4transactionsAll payments (remittance + QR) compressiontype gzipremittance/qr_payment; responsestatus: compressionYesprocessing/completed/failed
5body-parserrecipients ParseSaved JSON/urlencodedinternational bodiesrecipients YesBank account masked in API responses
6merchantsMerchant profiles, QR data rate-limiterorg_number unique (9 digits, Norwegian)
notificationsUser notificationsFeature-flagged
rate_limits IP-based rate limiting (Redis)persistent) Yeskey: IP address, window-based counter
7audit_logSecurity + compliance audit trail request-loggeraction Structuredresource_type, requestresource_id, loggingYesdetails
8aml_alerts Route-specificAML/financial middlewarecrime alerts Auth,status: validation per routeNoopen/closed/filed

Security headers configured via Helmet:

app.use(helmet({
  contentSecurityPolicy: { /* ... */ },
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'same-origin' },
}));

6. Authentication & Authorization Flow

flowchart TD
    Login["POST /auth/login\n{email, password}"] --> ValidateCreds["Validate credentials\n(bcrypt compare)"]
    ValidateCreds -->|Invalid| Reject["401 Unauthorized"]
    ValidateCreds -->|Valid| IssuePair["Issue token pair\naccess (15m) + refresh (30d)"]
    IssuePair --> StoreRefresh["Store refresh token\n(hashed, Redis or DB)"]
    IssuePair --> ReturnTokens["Return tokens to client"]

    AuthReq["Authenticated Request"] --> ExtractJWT["Extract Bearer token"]
    ExtractJWT --> VerifyJWT["Verify signature + expiry"]
    VerifyJWT -->|Invalid/Expired| RefreshFlow["POST /auth/refresh"]
    VerifyJWT -->|Valid| CheckRoles["Role/permission check"]
    CheckRoles -->|Unauthorized| Forbidden["403 Forbidden"]
    CheckRoles -->|Authorized| Handler["Route Handler"]

    RefreshFlow --> VerifyRefresh["Verify refresh token\n(hash match, not revoked)"]
    VerifyRefresh -->|Invalid| Logout["Force logout → 401"]
    VerifyRefresh -->|Valid| RotateToken["Rotate tokens\n(old token revoked)"]

RBAC / ABAC:

  • Roles: {{admin | manager | user | viewer}}
  • Permissions: {{resource:action}} e.g. users:delete
  • Role-permission mapping: {{database table | config file | code}}

7. Database Access Patterns

Pattern: {{Repository Pattern}}

// Repository interface — decouples business logic from storage
interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  findMany(filters: UserFilters): Promise<PaginatedResult<User>>;
  create(data: CreateUserData): Promise<User>;
  update(id: string, data: UpdateUserData): Promise<User>;
  delete(id: string): Promise<void>;
}

Query performance rules:

  • All queries accessing > 1000 rows must have paginator
  • All filterable fields must have DB indexes (document in migration)
  • N+1 queries forbidden — use include/JOIN or dataloader pattern
  • Raw SQL allowed only when ORM cannot express the query efficiently

8. Caching Architecture

graph LR
    Request --> L1["L1: In-Memory\n(node-cache)"]
    L1 -->|Cache miss| L2["L2: Redis\n(shared, distributed)"]
    L2 -->|Cache miss| DB["Database"]

    DB --> L2
    L2 --> L1
    L1 --> Response
Transaction (in-process)Map all
Layerstr_reports TechnologySuspicious TTL What'sReports CachedFiled with Finanstilsynet
L1consents GDPR consent records node-cacheconsent_type: /terms/privacy/marketing/cookies_analytics/cookies_marketing
data_access_requests 30GDPR secexport/erasure requests Config,type: export/erasure
complaintsUser complaints (Finansavtaleloven §3-53)15-business-day response SLA
exchange_ratesNOK → destination currency ratesUpdated externally
feature_flagsRuntime feature flag overridesComplement to env-var flags
L2 (distributed)cards Redis[FUTURE] Virtual/physical cards PerFeature-flagged, resource Userflags sessions,default API responses
L3 (CDN edge)Cloudflare / CloudFrontPer routePublic API responsesfalse

Cache-aside

4.3 patternData Auto-Detection (L2):

db.ts)

async// functionAuto-detects getCachedUser(id:driver string):based Promise<User>on {DATABASE_URL env var
const cacheKeydriver = `user:${id}`process.env.DATABASE_URL ? 'pg' : 'sqlite';
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await userRepository.findById(id);
  await redis.setex(cacheKey, 300, JSON.stringify(user)); // 5 min TTL
  return user;
}

5. Authentication Architecture

5.1 BankID OIDC Flow (Primary Auth)

CacheAuth invalidationmethod: strategy:Norwegian BankID — mandatory for all users. Email/password auth deprecated (returns 410 Gone).

Token: JWT (HS256), stored in drop_token httpOnly cookie (web) or Authorization Bearer header (mobile).

Token lifetime: 24h (web), 7d (mobile)

BankID Web Flow:

  1. GET /api/auth/bankid → generate state + nonce, set bankid_state cookie, return redirect URL
  2. User authenticates with BankID at provider
  3. GET /api/auth/bankid/callback?code=&state= → verify state, exchange code for tokens, verify JWKS signature, parse pid, hash pid → SHA-256, find/create user, issue JWT cookie

User creation on first BankID login:

  • OnParse update/delete:pid await(Norwegian redis.del(cacheKey)national ID, 11 digits) from ID token
  • Pattern-based:Hash pid with SHA-256 → awaitnational_id_hash redis.del(column
  • KYC status automatically approved (BankID = verified identity)
  • Password set to sentinel 'user:*'EIDONLY' — no password login possible

Age verification: pid encodes date of birth — must be >= 18 years old.

5.2 Session Management

Login  → Create session record (SHA-256 of JWT) in sessions table
Request → requireAuth() checks: cookie present + JWT valid + session not revoked
Logout → revokeAllSessions(userId) — sets revoked=1 on all user sessions

5.3 CSRF Protection

  • Web: State parameter in BankID OIDC flow (httpOnly cookie)
  • API: Origin header validation in requireAuth() (useagainst sparinglyallowed — expensive)origins
  • Tag-based:Mobile: {{ioredis-tagN/A |(Bearer customtoken, tagging}}no cookies)

9.6. BackgroundMiddleware Job ProcessingStack

Library: {{BullMQ | Agenda | pg-boss}} Queue storage: {{Redis | PostgreSQL}}

AppliedAllMerchant-onlyAuth
QueueMiddleware Job TypesFunction Concurrency Retry PolicyTo
emailsrequireAuth() Welcome,CSRF resetcheck password, notificationscookie extraction → JWT verify → user lookup → session revocation check 10 3protected retries, exp. backoffroutes
uploadsrequireMerchant() ImagerequireAuth() processing,+ filerole conversioncheck (role === 'merchant') 5 2 retriesroutes
syncrateLimit(ip, limit) ExternalPersistent APIIP-based sync,counter datavia aggregationrate_limits DB table, 60s window 3 5endpoints retries(10/min), public rates (120/min)
reportsgetClientIp() PDFExtract generation,IP exportsfrom x-forwarded-for 2All rate-limited routes
jsonError() 1Standardized retryJSON error responseAll routes
featureGate(flag)Returns 404 if feature flag disabledCards, spending limits, notifications
Input validationvalidateEmail, validatePhone, validateAmount, validateName, sanitizeText, etc.All mutation endpoints
Error handlerAppError class with predefined constructorsAll routes via createErrorResponse()

7. API Design Principles

  1. JobConsistent schema:response envelope:

    • Success: interface{ EmailJob"data": { type: 'welcome' | 'password_reset' | 'notification'; to: string; templateId: string; data: Record<string, unknown>;... } }
     or { "data": [...], "pagination": { ... } }
  2. Error: { "error": "code", "message": "...", "details": [...] }
  3. Monitoring:No wallet model: Drop never holds funds. {{Bullbank_accounts.balance Boardis |AISP-read BullMQcache Metricsonly.

    API}}
  4. KYC gate: Remittance requires kyc_status === 'approved'adminenforced UIin atroute handler.

  5. Atomic transactions: Balance deduction and transaction creation in a single DB transaction.

  6. Data masking: Bank account numbers masked in responses ({{/admin/queues}}*****5678), card numbers PCI-masked.

  7. GDPR by design: Data export, account deletion (soft delete), consent records all implemented.

  8. Compliance-first: STR reports, AML alerts, audit log, complaint system (Finansavtaleloven §3-53), PITR retention (5 years per hvitvaskingsloven).


10.8. FileSecurity Storage & Media HandlingArchitecture

Storage provider: {{AWS S3 | Cloudflare R2 | MinIO}} Bucket naming: {{company-project-env}} (e.g., alai-app-production)

Upload flow:

  1. Client requests pre-signed URL from API (POST /uploads/presigned)
  2. API validates file type, size, generates pre-signed URL (expiry: 15 min)
  3. Client uploads directly to storage (bypasses API server)
  4. Client notifies API of upload completion (POST /uploads/confirm)
  5. API validates file exists, creates database record, triggers processing job

File size limits:

TypeControl Max SizeImplementation
Profile imagesAuth 5BankID MBOIDC (Norwegian eID) — mandatory
DocumentsSession tokens 25httpOnly, MBsecure, sameSite=strict JWT cookies
VideosRate limiting 500Persistent MBDB-backed per-IP (10/min auth, 120/min public)
Input validationCustom validators (no external dep) — email, phone, amount, IBAN, name (XSS-resistant)
SQL injectionParameterized queries via pg / better-sqlite3
XSSCSP headers (strict production — no unsafe-eval) + HTML sanitization in sanitizeText()
CSRFOrigin header validation + BankID state parameter
SecretsAWS Secrets Manager / Fly.io secrets — never in code or .env
Error maskingcreateErrorResponse() masks internal errors in production
Password hashingbcryptjs (legacy users)
Card dataPCI-masked (never expose full card number or CVV)
Audit trailaudit_log table — all sensitive actions logged
AMLaml_alerts + str_reports tables — compliance framework

11.9. LoggingExternal &Service ObservabilityIntegrations

Logger: {{Pino}} — structured JSON logs Log aggregation: {{Datadog / Loki / CloudWatch}}

Log levels policy:

to use butorderdiagnostictracing
LevelService WhenStatus Purpose
errorSumsub Exceptions,PRODUCTION failures(only requiringconnected attentionexternal service)KYC/identity verification — WebSDK + webhook
warnBankID OIDC UnexpectedPRODUCTION Norwegian handledeID situationsauthentication
infoOpen Banking (AISP/PISP) SignificantTBD business eventsprovider (userselection created,pending Bank placed)balance read + payment initiation
debugSwan Open Banking DetailedDEPRECATED Was infoplanned, no dev/staginglonger onlyselected
traceStripe Issuing VerboseMOCK request(future) Card issuanceneverno inSDK, productionno API keys
SlackPRODUCTIONOperational alerting via webhook
BetterStackPRODUCTIONExternal uptime monitoring

Always


log:

Never log:

  • Passwords, tokens, API keys
  • Full request/response bodies with PII
  • Payment card data

12. Configuration Management

Pattern: Typed config module with validation on startup

// config/app.config.ts
const schema = z.object({
  NODE_ENV: z.enum(['development', 'staging', 'production']),
  PORT: z.coerce.number().default(4000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  // ...
});

export const config = schema.parse(process.env);
// App fails FAST at startup if any required variable is missing/invalid

Secrets management: {{HashiCorp Vault | AWS Secrets Manager | Doppler}} NO secrets in environment files committed to git.


13. Health Check Endpoints

EndpointTypeChecks
GET /health/liveLivenessProcess is running
GET /health/readyReadinessDB connected, Redis connected, app ready
GET /health/startupStartupMigrations run, config valid

Readiness check response:

{
  "status": "ok",
  "checks": {
    "database": { "status": "ok", "latency": 3 },
    "redis": { "status": "ok", "latency": 1 },
    "queue": { "status": "ok", "pendingJobs": 12 }
  },
  "version": "1.2.3",
  "uptime": 3600
}

14. Architecture Diagram

graph TB
    subgraph "Clients"
        Web["Web App"]
        Mobile["Mobile App"]
    end

    subgraph "Infrastructure"
        LB["Load Balancer\n(Nginx / ALB)"]
        API["API Server\n({{FRAMEWORK}})"]
        Workers["Background Workers\n(BullMQ)"]
    end

    subgraph "Data"
        DB["PostgreSQL\n(Primary + Replicas)"]
        Cache["Redis\n(Cache + Queue)"]
        Storage["Object Storage\n(S3 / R2)"]
    end

    subgraph "Observability"
        Logs["Log Aggregation\n(Datadog / Loki)"]
        APM["APM / Tracing"]
    end

    Web --> LB
    Mobile --> LB
    LB --> API
    API --> DB
    API --> Cache
    API --> Storage
    API --> Cache
    Workers --> Cache
    Workers --> DB
    API --> Logs
    Workers --> Logs
    API --> APM

Approval

Alem
Role Name Date Signature
Author Platform Architect (AI) 2026-02-23
Backend LeadReviewer
Tech Lead / ArchitectApprover
Security ReviewerBašić