Architecture Overview
Architecture Overview
System Design
Drop Srbija is a fintech payment app for the Serbian market, enabling phone-based remittance and instant transfers via NBS IPS (Narodna Banka Srbije instant payment system).
Core principle: Money never touches Drop — it stays in user's bank account. Drop facilitates transfers only.
Technology Stack
| Component | Choice | Rationale |
|---|---|---|
| Language (Backend) | Kotlin | ALAI standard 2026-03-17; full type safety + Ktor native support |
| Framework | Ktor 3.1.2 | Lightweight, async-first, Production-ready with netty transport |
| Database | PostgreSQL 16 | ALAI mandate; full ACID, timezone support, JSON operators |
| ORM | Exposed (Kotlin DSL) | Kotlin-native, composable, maps to SQL closely |
| Migrations | Flyway | Version control for schema; integration with Ktor startup |
| Connection Pool | HikariCP | Battle-tested, fast, connection reuse |
| Auth | JWT (HS256) | Stateless, easy to validate on backend + frontend |
| Language (Frontend) | TypeScript + Next.js 15 | React 19, server components, app router |
| Styling | Tailwind CSS 4 + shadcn/ui | ALAI standard; component library prebuilt |
| Icons | Lucide React | ALAI standard across all products |
| Testing (Backend) | Kotest + Testcontainers | Kotlin-native, containerized DB for integration tests |
| Testing (Frontend) | Vitest + Playwright | Unit + E2E, headless browser automation |
Module Structure
DropSrbija/
├── backend/ # Kotlin + Ktor 3.1.2
│ ├── src/main/kotlin/no/alai/dropsrbija/
│ │ ├── Application.kt # Main entry point
│ │ ├── plugins/ # Ktor configuration
│ │ │ ├── Database.kt
│ │ │ ├── Routing.kt
│ │ │ ├── Authentication.kt
│ │ │ └── RateLimit.kt
│ │ ├── auth/ # JWT services
│ │ ├── models/ # Database tables (Exposed DSL)
│ │ └── modules/ # Business logic
│ │ ├── auth/ # Phone OTP flow
│ │ ├── user/ # User management
│ │ ├── transactions/ # Transaction handling
│ │ ├── recipients/ # Recipient management
│ │ └── ips/ # NBS IPS integration
│ └── src/main/resources/db/migration/ # Flyway migrations
└── frontend/ # Next.js 15 + React 19
└── src/app/
├── layout.tsx
├── page.tsx # Landing page (Serbian)
└── (app)/ # Authenticated app
Database Schema (Migrations V1-V9)
V1: Core Tables (Users, PhoneVerifications, Transactions, Recipients, NbsIpsLogs, Merchants, Settings)
Users Table:
id— UUID (primary key)phone— +381XXXXXXXXX (unique)first_name,last_name— User identityemail— optionalkyc_status— pending | verified | rejectedrole— user | merchant | adminphone_verified— booleandeleted_at— soft delete support
PhoneVerifications Table:
id— UUID (primary key)phone— +381XXXXXXXXXotp— 6-digit hash (bcrypt)attempts— failed attempts counter (max 5)expires_at— 10-minute expiryverified_at— timestamp of successful verification
Transactions Table:
id— UUID (primary key)user_id— references userstype— phone_transfer | qr_payment | bank_transferstatus— processing | completed | failedamount— integer RSDcurrency— RSD | EUR | USDrecipient_phone— +381XXXXXXXXX (if phone-based)nbs_ips_id— NBS transaction IDnbs_ips_status— PENDING | ACCEPTED | REJECTED | SETTLED
NbsIpsLogs Table:
id— UUID (primary key)transaction_id— references transactionsrequest_type— initiate | check_status | cancelrequest_body— JSON payload sent to NBSresponse_body— JSON response from NBSresponse_status— HTTP status codeerror_message— any error text
V2: ISO 20022 Message Support
- Added
iso20022_message_id,iso20022_end_to_end_idcolumns tonbs_ips_logs
V3: Linked Bank Accounts
- New table:
linked_accounts(user_id, bank_id, iban, account_name, verified_at)
V4: Transaction Idempotency
- Added
idempotency_key_hash(SHA-256 hash, unique constraint) totransactions
V5: KYC Sessions
- New table:
kyc_sessions(user_id, status, jmbg_verified, biometric_data, completed_at)
V6: National ID Support
- Added
national_id_hash,national_id_encryptedtouserstable
V7: AML Flags
- New table:
aml_flags(user_id, flag_type, reason, severity, created_by, resolved_at)
V8: User Disclosure Acknowledgment
- Added
disclosure_acknowledged_attouserstable
V9: Complaints System
- New table:
complaints(user_id, category, description, status, resolved_at)
Ports & Services
| Service | Port (Local) | Description |
|---|---|---|
| PostgreSQL | 5434 | Drop Srbija database (separate from Drop Norway) |
| Redis | 6380 | Rate limiting and caching |
| Backend API | 3003 | Ktor HTTP server |
| Frontend | 3000 | Next.js dev server |
Payment Flow Diagram
flowchart LR
A[User Enters Phone + OTP] -->|POST /auth/verify-otp| B{OTP Valid?}
B -->|Yes| C[JWT Issued]
B -->|No| D[Increment Attempts]
C --> E[User Initiates Payment]
E -->|POST /v1/ips/initiate| F[Create Transaction Record]
F --> G[Call NbsIpsService]
G -->|ISO 20022 Message| H[NBS IPS API]
H -->|ACCP/RJCT| I[Log Response to NbsIpsLogs]
I -->|Async| J{Status?}
J -->|ACCP| K[Update Transaction: completed]
J -->|RJCT| L[Update Transaction: failed]
K --> M[Recipient Receives RSD]
L --> N[Notify User of Failure]
Branch Model
Feature branches are created off the previous feature branch, following a linear progression:
feat/drop-srbija-models (T1)
↓
feat/drop-srbija-otp (T2)
↓
feat/drop-srbija-jwt (T3)
↓
feat/drop-srbija-ips (T6)
↓
...
feat/drop-srbija-disclosure-complaints (T13)
↓
feat/drop-srbija-docs (T29 — this documentation)
Merge strategy: TBD — will be established when merging to main/production branch.
Key Architectural Decisions
-
Bank Partner Adapter Pattern (Task 6): NBS IPS is ISO 20022 mTLS, not REST; single NBS direct integration impossible without bank license. Solution: Partner with licensed Serbian bank that provides IPS gateway access.
-
Agent Model Year 1 → Own PI License Year 2: Start as registered agent under Serbian bank (Article 24 of Payment Services Law), then pursue own Payment Institution license once proven.
-
Idempotency via SHA-256 Hash (Task 4): Store hash of idempotency key, not the raw key itself, to minimize PII surface area.
-
Lock Threshold 5 vs Spec 6: Industry-standard practice is 5 failed OTP attempts before account lock, rather than 6 mentioned in some specs.
Security Considerations
-
PII Encryption:
- National ID stored encrypted (KMS key rotation)
- Phone numbers indexed as hashes for lookups
- Passwords NOT stored (auth via OTP only)
-
OTP Security:
- 6-digit, 10-minute expiry
- Max 5 attempts per verification
- Rate limit: 3 requests per minute per phone
- SMS delivery via Twilio (TLS, no logging)
-
JWT Validation:
- HS256 with strong secret (64+ bytes)
- Issuer: "dropsrbija-api", Audience: "dropsrbija"
- httpOnly cookies (frontend sets automatic)
- 24-hour expiry (refresh token TBD)
-
NBS IPS Integration:
- mTLS for NBS API calls
- Request signing (HMAC-SHA256)
- Audit log for all NBS transactions
- Retry logic with exponential backoff
-
Rate Limiting:
- Global: 1000 req/min per IP
- Auth: 10 OTP requests/hour per phone
- Transaction: 50 transactions/hour per user
- NBS API: Respect rate limits from NBS docs
Monitoring & Observability
Logs
- Structured JSON logs (Logback)
- Sensitive fields redacted (phone last 4 digits only)
- Log level: INFO (debug only in dev)
Metrics
- Transaction latency (p50, p95, p99)
- NBS API response time
- OTP verification success rate
- Error rates by endpoint
Alerts
Last Updated: 2026-04-16
Status: Architecture scaffold complete — ready for Phase 2 (Frontend Onboarding UI)