Petter Graff Architecture Review
Drop Fintech Platform — Architecture Review
Reviewer: Petter Graff (Software Architect, CTO Pratexo) Review Date: 2026-02-22 Subject: Drop — Norwegian fintech payment application (remittance + QR payments) Model: PSD2 pass-through (AISP/PISP) — Drop never holds customer funds
1. Overall Assessment
Grade: C+ (6.5/10)
Drop has a solid conceptual foundation for a PSD2-regulated fintech application. The architectural documentation is comprehensive, the compliance framework is thoughtfully designed, and the core technical decisions (monolith-first, dual-database abstraction, BankID-only auth) are appropriate for an MVP. However, critical production-readiness gaps exist across security, data integrity, and operational resilience that would prevent regulatory approval and create significant business risk.
What I see here: An engineering team that understands fintech compliance requirements at a documentation level but has not yet built the operational muscle memory to implement them correctly. The Vault Squad analysis is accurate — you have 17 CRITICAL findings that must be fixed before processing a single real transaction.
This is not a failing grade — it's a realistic early-stage fintech build. But you're not production-ready, and pretending otherwise would be dangerous.
2. What's Done Well
2.1 Regulatory Awareness
- 19-table schema includes 7 dedicated compliance tables (
audit_log,aml_alerts,str_reports,screening_results,consents,data_access_requests,complaints) — most fintechs bolt these on later - Pass-through PSD2 model correctly avoids e-money license complexity — Drop positions as PISP/AISP only
- BankID-only authentication is the right call for Norwegian fintech (SCA by design, no password management)
- Dual-database abstraction (
db.ts) with SQL compatibility translation is clean engineering — SQLite for dev speed, PostgreSQL for production reliability
2.2 Security Fundamentals
- Parameterized SQL throughout — zero SQL injection vulnerabilities
- bcrypt with 12 rounds for password hashing
- Idempotency keys on transactions with unique index — double-charge protection exists
- Structured audit logging with IP, user-agent, request-id correlation
2.3 AML/Compliance Framework
The transaction monitoring module (transaction-monitor.ts) has 5 rule types:
- Structuring detection (multiple small txns avoiding thresholds)
- Velocity checks (>5 txns/hour)
- High-amount flagging (>25K NOK)
- High-risk corridor detection (FATF grey-list countries)
- Unusual pattern analysis (3x user average)
Problem: It's never called from any API route (Vault Squad finding BE-C2). Dead code doesn't count.
2.4 Documentation Quality
C4 diagrams, ADRs, and HLD documents are better than 80% of fintech startups. Architecture is understandable, decisions documented with rationale, trade-offs acknowledged.
3. Critical Improvements (Production Blockers)
3.1 Authentication Catastrophe
| Finding | Impact | Source |
|---|---|---|
JWT secret defaults to hardcoded "dev-secret-change-in-production" |
Anyone can forge tokens if env var missing | auth.ts:8 |
No OIDC state validation |
CSRF on authentication flow | bankid.ts:80-108 |
No OIDC nonce validation |
ID token replay attacks | bankid.ts:93-101 |
Cookie missing Secure flag |
Tokens sent over HTTP in plaintext | auth.ts:206 |
Risk: Complete account takeover possible.
Fix:
// Startup validation
if (process.env.NODE_ENV === 'production' &&
process.env.JWT_SECRET === 'dev-secret-change-in-production') {
throw new Error('FATAL: Production requires real JWT_SECRET');
}
// BankID callback — validate state + nonce
const storedState = cookies.get('bankid_state');
const storedNonce = cookies.get('bankid_nonce');
if (state !== storedState) throw new Error('Invalid state');
if (idToken.nonce !== storedNonce) throw new Error('Invalid nonce');
// Cookie flags
cookies.set('drop_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1800 // 30min, not 7 days
});
3.2 PISP Call Inside Database Transaction
External HTTP call (30s timeout) while holding database write lock. Under load → deadlocks, data loss, double-charging.
Fix: Two-phase commit:
// Phase 1: Create pending transaction (fast)
const txId = await createPendingTransaction(...);
// Phase 2: Initiate payment OUTSIDE transaction
try {
const result = await initiateRemittance(...);
await updateTransactionStatus(txId, result.status);
} catch (error) {
await updateTransactionStatus(txId, 'failed');
}
3.3 TEXT Timestamps Everywhere
All 30+ timestamp columns use TEXT storing ISO 8601 strings. No timezone awareness, no native date math, ISO 20022 non-compliant.
Fix: Migrate to TIMESTAMPTZ in PostgreSQL migration.
3.4 No Database Connection Management
PostgreSQL pool has no timeout configuration. One slow query exhausts entire pool → cascading failure.
Fix:
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
statement_timeout: 30000,
query_timeout: 30000,
});
3.5 Zero Transaction Monitoring
AML monitoring code exists but is never called from any API route. Hvitvaskingsloven §7 requires real-time monitoring.
Fix: Wire checkTransaction() before PISP initiation. Block critical alerts.
3.6 Missing Open Banking Consent Lifecycle
No ob_consents table. Can't enforce 90-day consent renewal, access frequency limits, or consent revocation.
Fix: Create ob_consents table with consent tracking, expiry, access counting.
4. Strategic Improvements (Scale)
4.1 Network Topology & Failure Modes
No documented failure scenarios or retry strategies. Need failure domain mapping for BankID, PISP, PostgreSQL.
4.2 No Double-Entry Bookkeeping
Single-entry transaction model. Finanstilsynet will ask for general ledger proving credits = debits.
Fix: Add transaction_legs table with debit/credit entries per transaction.
4.3 Missing Composite Indexes
High-frequency queries will full-table-scan at scale.
Fix: Add composite indexes: (user_id, created_at DESC) on transactions, audit_log, notifications.
4.4 No Table Partitioning Strategy
audit_log will reach 90M rows in 5 years. Plan partitioning by month.
5. Architecture Smell Flags
5.1 Rate Limiting Writes to PostgreSQL
Every request writes to PostgreSQL for rate limiting. Should use in-memory (Map/Redis).
5.2 Fire-and-Forget Audit Logging
Critical actions (login, payment, consent) use async audit — should be synchronous for regulatory compliance.
5.3 No Circuit Breaker for External APIs
If PISP API goes down, every request waits 30s then fails. Need circuit breaker (fail fast after 5 failures, retry after 60s).
6. Questions Before Sign-Off
Security & Auth
- JWT key rotation process?
- BankID downtime fallback?
- Session hijacking detection (2 IPs simultaneously)?
- Direct database access logging?
Data Integrity
- DST transition handling with TEXT timestamps?
- Exchange rate locking between quote and execution?
- Transaction status if PISP call times out?
- Client-generated idempotency key security?
Compliance
- How to enforce 90-day AISP consent expiry?
- Who reviews AML alerts and how fast?
- STR filing integration with Økokrim?
- GDPR data export time for user request?
Operations
- Database backup RPO/RTO?
- Zero-downtime schema migration strategy?
- PagerDuty alert rules?
- AWS eu-north-1 failover plan?
7. Priority Roadmap
| Period | Focus | Priority |
|---|---|---|
| Month 1-2 | Security hardening (auth, CSRF, PISP fix, DB timeouts, AML wiring) | CRITICAL |
| Month 3-4 | Data integrity (timestamps, ob_consents, double-entry, indexes) | HIGH |
| Month 5-6 | Operational resilience (circuit breakers, Multi-AZ, DR runbook) | HIGH |
| Month 7+ | Scale preparation (Redis rate limit, read replicas, monitoring) | MEDIUM |
Final Verdict
"Fintech is not about having fancy architecture diagrams — it's about operational discipline. Every timeout needs a value. Every transaction needs an audit log. Every external call needs a circuit breaker. These aren't optional extras — they're the difference between a production system and a demo. You're 60% there. The remaining 40% is where most fintechs fail."
— Petter Graff, CTO Pratexo