Skip to main content

Security Architecture

Security Architecture Document

Project: BilkoDropBalkanFintech AccountingPayment SaaSApp (Remittance + QR Payments) Version: 1.0 Date: 2026-02-23 Author: ComplianceALAI ArchitectSecurity Team Status: Draft Reviewers: CISO, CTO, DPO, Engineering LeadDPO Classification: Confidential

Document History

Version Date Author Changes
0.1 2026-02-2312 ComplianceSecurity ArchitectAgent (ALAI) Initial draft — Bilko security architectureaudit
1.02026-02-23Security Architect (ALAI)Architecture documentation

1. Security Architecture Overview

Security Owner: Compliance ArchitectCISO ([email protected])Alem Bašić / ALAI Holding AS) Last Security Review: 2026-02-2312 (full audit); 2026-02-13 (hardening verification) Next Scheduled Review: 2026-08-232027-02-12 (annual) or after Phase 2 integration (BankID, Open Banking) Compliance Targets: GDPR | Zakon o zaštiti podataka o ličnosti RSPSD2 (ZZPL)Betalingstjenesteloven) | Zakon o zaštiti ličnih podataka BiH (ZZLP)AML/Hvitvaskingsloven | GDPR via AZOP (HR)DORA/IKT-forskriften | ZakonFinanstilsynet o računovodstvu RS/HR/BA | Zakon o PDV RS/BA/HRlicense

Architecture Model: BilkoDrop isoperates a multi-tenantPSD2 cloudpass-through accountingmodel. SaaS.Drop Processesnever invoices,holds expenses,customer VATfunds. returns,AISP andreads financialbank reportsbalances via Open Banking; PISP initiates payments directly from the user's bank account. Cards are a future feature, gated behind feature flags (all default to false).

Security Posture Summary (post-hardening 2026-02-13):

  • 0 Critical findings remaining (all 4 resolved)
  • 0 High findings remaining (all resolved)
  • 2 Medium findings remaining (CSP unsafe-inline, proxy X-Forwarded-For trust)
  • 4 Low findings acknowledged (out of scope for organizationscurrent inMVP Serbia,sprint)
  • Bosnia & Herzegovina, and Croatia. Each organization's data strictly isolated by organizationId at the database layer.

Defense-in-Depth Overview

graphInternet
  TD CLIENT["ClientWAF Browser(planned — Phase 2 infra hardening)
  → CDN / PWA"]Edge subgraphTLS NETWORK["Networktermination Layer"](planned)
  CF["Cloudflare WAF\nDDoSLoad Protection\nTLSBalancer (TLS 1.33)
  termination\nHSTS"] end

    subgraph APP_LAYER["Application Layer"]Layer HELMET["Helmet.js\nCSP(Next.js — Next.js 16.1.6)
      ├── Rate Limiting (SQLite-backed, persistent)
      ├── Origin/CSRF validation
      ├── JWT auth + X-Framesession revocation
      ├── RBAC (user / merchant roles)
      ├── Input validation + HSTS\nnosanitization
      X-Powered-By"]└── CORS["CORS Whitelist\nbilko.io only\nno wildcard *"]
        RATE["Rate Limiter\nexpress-rate-limit\n5 req/15min auth\n100 req/15min general"]
        AUTH_MW["Auth Middleware\nJWT verify (15min access)\norg-scope injection"]
        RBAC_MW["RBAC Middleware\nowner / admin / accountant / viewer"]
        ZOD["Zod Validation\nall request bodies\ntype-safe parsing"]
    end

    subgraph DATA_LAYER["Data Layer"]
        PRISMA_ORM["Prisma ORM\nparameterized queries\nno rawParameterized SQL forqueries
  user input\norg-scopedDatabase WHERELayer clauses"](SQLite PG_ENC["— MVP; PostgreSQL (Railwayplanned EUPhase West)\nAES-2562)
      disk└── encryption\nbackupStored: encryption"]bcrypt(password), endsession subgraphtoken AUDIT["Audithashes, Layer"]masked LOG["LoggedActioncard table\nAPPEND-ONLY\nIPtokens

+Monitoring: user + timestamp\nold/new valuesSentry (changedFields)"]error endtracking) CLIENT -->SIEM CFplanned -->Phase HELMET --> CORS --> RATE --> AUTH_MW --> RBAC_MW --> ZOD --> PRISMA_ORM --> PG_ENC
    PRISMA_ORM --> LOG3

2. Authentication Flows

2.1 Strategy:Current JWTMVP Authentication (JSON Web Tokens)

Stateless JWT, scales horizontally on Railway. Access tokens (15 min, memory-only)Email + refresh tokens (7 days, httpOnly cookie). Rotation on every refresh. Revocation via hashed token storage in DB.

2.2 JWT Auth FlowPassword)

sequenceDiagram
    actor User participant FE as Frontend (bilko.io — Vercel)
    participant API as Express API (api.bilko.io — Railway EU)
    participant DB as PostgreSQL (Railway EU West)

    User->>FE: Enter email + password
    FE->>API: POST /api/v1/auth/login API->>DB:{email, password}
     → Rate limit check (10 req/60s per IP — SQLite-backed)
     → SELECT user WHERE email = ?
     (parameterized)
    DB-->>API: User record (passwordHash)
    API->>API: bcrypt.compare(verify(password, hash) [cost 12factor rounds12]
     alt PasswordIf validvalid: API->>API:generate jwt.sign({sub,JWT org,(HS256, role},jose JWT_SECRET,^6.1.3, 15m)24h API->>DB:expiry)
     → INSERT refreshTokeninto sessions table (hashed, expiresAt)
        API-->>FE: 200 { accessToken } + Set-Cookie: refreshToken (httpOnly, secure, sameSite=strict)
        FE->>FE: Store accessToken in memory only
    else Password invalid
        API-->>FE: 401 Unauthorized (generic — no user enumeration)
    end

    Note over FE,API: 15 minutes later — access token expires
    FE->>API: POST /api/v1/auth/refresh (Cookie: refreshToken)
    API->>API: Rotate: delete old, issue new
    API-->>FE: 200 { newAccessToken } + Set-Cookie: newRefreshToken

    Note over User,DB: Logout
    FE->>API: POST /api/v1/auth/logout
    API->>DB: DELETE refreshToken WHERE userIdtoken_hash = ?SHA-256(token))
     API-->>FE: 204Set NohttpOnly Contentcookie (secure:true, sameSite:strict, maxAge:24h)
     → Return 200

Source: src/drop-app/src/lib/auth.ts, src/drop-app/src/lib/middleware.ts

2.32 Two-FactorSession Authentication (2FA)

Method: TOTP (RFC 6238) — Google Authenticator, Authy, 1Password

  • Setup: POST /api/v1/auth/2fa/setup → QR code + base32 secret
  • Verify: POST /api/v1/auth/2fa/verify { code }
  • Login: returns { requires2FA: true, tempToken } → POST /api/v1/auth/2fa/login
  • Backup: 10 single-use codes, bcrypt-hashed

3. Authorization (RBAC)

3.1 Role Permission MatrixLifecycle

StepAction owneradminaccountantviewerSource
Create invoiceLogin Session created, token_hash stored auth.ts:56-65
EditEach invoicerequest Session checked for revocation middleware.ts:66-74
Delete invoiceLogout All user sessions revoked server-side auth/logout/route.ts:5-14
ViewPassword invoicechange All sessions revoked Planned (Phase 2)

Session table schema:

References SHA-256ofExpiration
ColumnTypePurpose
id TEXT PK ses_<hex16> format
Approve expenseuser_id TEXT FK users.id
Generate reporttoken_hash TEXT JWT token
Invite userexpires_at TEXT timestamp
Edit org settingsrevoked INTEGER 0 = active, 1 = revoked

2.3 BankID OIDC Authentication (Phase 2 — Planned)

BankID is not yet integrated in the MVP codebase. Required for PSD2 SCA compliance before any live transactions. Planned integration:

User → Drop App → BankID OIDC (nivå høyt — eIDAS Level High)
     → BankID returns: name, fødselsnummer (national ID), verified identity
     → Drop validates: age >= 18 (from fødselsnummer), Norwegian residency
     → Dynamic linking for payment authorization (amount + payee bound to auth)

Regulatory requirement: PSD2 (Betalingstjenesteloven §§ 4-28, 4-29) requires SCA with two of three factors. BankID covers possession + knowledge. No live transactions without this.

Integration partner: TBD — BankID Norge AS (DPA required)

2.4 KYC Flow (Phase 2 — Planned via Sumsub)

Current state: Mock KYC with auto-approve (kyc_status field in users table). Production will use Sumsub:

User → Sumsub SDK → Document scan + liveness check
     → Sumsub webhook → Drop backend
     → Update users.kyc_status = 'approved'/'rejected'
     → Required for: remittance transactions

Source: legal/dpa-sumsub.md, legal/dpia-vurdering.md


3. Authorization Model

Model: RBAC (Role-Based Access Control) with resource-level user scoping

3.1 Roles

RoleDescriptionAccess
user Standard registered user Own data only (transactions, recipients, notifications, settings)
merchantMerchant with QR payment dashboardOwn data + merchant dashboard (/api/merchant/*)

KYC Status (enforced gate for financial operations):

StatusMeaningEffect
pendingDefault on registrationCannot initiate remittance
approvedKYC completedFull access to financial features
rejectedKYC failed or blockedBlocked from all financial operations

3.2 OrganizationResource-Level ScopingAccess Control (IDOR Prevention)

All data access queries include //AND Injecteduser_id by= auth? middlewareto onscope data to the authenticated user. Applied to:

  • recipients — scoped to user
  • transactions — scoped to user
  • bank_accounts — scoped to user
  • notifications — scoped to user
  • settings — scoped to user
  • cards — scoped to user (future feature)

Merchant endpoints verify both merchant role and ownership.

Source: src/drop-app/src/app/api/ — all /api/v1/*route routes app.use('/api/v1/*', (req, res, next) => { req.prismaWhere = { organizationId: req.user.organizationId }; next(); }); // Applied to every Prisma query await prisma.invoice.findMany({ where: { ...req.prismaWhere } });

UUID primary keys throughout — no sequential ID enumeration possible.


4. Encryption

4.1 In Transit: TLS 1.3

All traffic HTTPS. Cloudflare TLS 1.3 at edge, re-encrypted to Railway. HSTS: max-age=63072000; includeSubDomains; preload.handlers

4.23.3 AtPermission Rest: AES-256Summary

EU West (Frankfurt/Paris)EU West
StoreResource Methoduser LocationmerchantAdmin (TBD)
PostgreSQLOwn profile AES-256 TDE (Railway)CRUD RailwayCRUD
PostgreSQLOwn backupstransactions AES-256 auto-backupRead RailwayRead  30 days
TaxOwn IDsrecipientsCRUDCRUD
Merchant dashboardRead
All usersAdmin only
AML reportsCompliance only

4. Data Encryption

4.1 Encryption at Rest

IBAN
DataMethodStatus
Database (PIB/JMBG/OIB/JIB),SQLite) OS-level (filesystem)MVP — migrate to PostgreSQL Phase 2
Passwordsbcrypt, cost factor 12Implemented (bcryptjs ^3.0.3)
Session tokensSHA-256 hash stored (not plaintext)Implemented
JWT secretJWT_SECRET env var (fatal if missing in prod)Implemented
Fødselsnummer (national ID) AES-256-GCM fieldapplication encryptionlayer + HSM key ApplicationPlanned layerPhase — Railway env secret2
CloudflareCard R2 (receipts, PDFs)data AES-256Only server-sidelast_four + token_ref stored (PAN/CVV never stored) CloudflareImplemented EU(fix regionC1)
Bank account numbersOnly last 4 digits in API responsesImplemented

Note: Field-level encryption for PII (fødselsnummer) requires HSM-backed key management (planned Phase 2 with AWS KMS).

4.2 Encryption in Transit

ConnectionProtocolStatus
User → Drop APIHTTPS / TLS 1.3Production requirement
Drop → BankIDHTTPS / TLS 1.3Phase 2
Drop → SumsubHTTPS / TLS 1.3Phase 2
Drop → Neonomics (PSD2)HTTPS / TLS 1.3Phase 2
Drop → SwanHTTPS / TLS 1.3Phase 2
Internal (service-to-service)mTLSPhase 3 (when microservices introduced)

bcrypt,Source: 12 salt rounds. Min 8 chars. Block top 10K common passwords. Last 5 hashes retained.src/drop-app/src/lib/auth.ts:48-54

PropertyValuePurpose
httpOnlytruePrevents JavaScript access (XSS mitigation)
securetrue (production)HTTPS-only transport
sameSite"strict"CSRF prevention
maxAge86400 (24h)Session lifetime
path"/"Full site scope

5. Network Security

4.45.1 FinancialCurrent MVP Network Architecture

Internet → Next.js App (port 3000) → SQLite DB (local file)

Phase 2 target:

Internet
  → Cloudflare (DDoS + WAF)
  → AWS Load Balancer (TLS termination)
  → Private Subnet: Next.js App (ECS/Fargate)
  → Private Data PrecisionSubnet: PostgreSQL (RDS)
  → External APIs: BankID, Sumsub, Neonomics, Swan (all HTTPS)

5.2 Security Groups (Phase 2 planned)

SourceDestinationPortAction
InternetLoad Balancer443ALLOW
InternetAny80REDIRECT → 443
Load BalancerApp servers3000ALLOW
App serversPostgreSQL5432ALLOW
App serversExternal APIs443ALLOW (allowlist)
AnyData SubnetAnyDENY (default)

5.3 Rate Limiting

Source: src/drop-app/src/lib/middleware.ts:6-31

Endpoint TypeLimitWindowImplementation
Auth routes (login, register)10 req60 secondsSQLite-backed (persistent across restarts)
Transaction routes (remittance, qr-payment)10 req60 secondsSQLite-backed
Rate routes (/api/rates)120 req60 secondsSQLite-backed

Rate limit table: rate_limits — per-IP tracking via X-Forwarded-For header.

Known gap: X-Forwarded-For can be spoofed. Fix requires trusted proxy validation (planned Phase 2 with load balancer).


6. API Security

6.1 Input Validation

Source: src/drop-app/src/lib/middleware/validation.ts:149-203

All API inputs validated at controller level:

  • sanitizeText() — strips HTML tags, control characters, enforces max length
  • validateName() — rejects XSS payloads, script tags
  • validateEmail() — RFC format validation
  • validatePhone() — international format
  • validateAmount() — positive, finite, max 2 decimal places
  • validateIBAN() — format + checksum
  • validatePIN() — exactly 4 digits
  • validateCurrency() — whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR
  • validateLanguage() — whitelist: nb, en, bs, sq

Amount limits:

EndpointMinMax
Remittance100 NOK50,000 NOK
QR Payment1 NOK100,000 NOK

6.2 SQL Injection Prevention

All monetary24 amounts:API endpoints use parameterized queries exclusively (? placeholders). No string concatenation in SQL.

Source: NUMERIC(19,4)src/drop-app/src/app/api/neverall float.route Exchangehandlers; ratesverified lockedin atsecurity transactionaudit date.2026-02-12.

6.3 CSRF Protection

Origin header validation on all authenticated requests:

  • Validates Origin against: NEXT_PUBLIC_APP_URL, http://localhost:3000, http://localhost:3001
  • Combined with sameSite: "strict" cookies for defense-in-depth

Source: src/drop-app/src/lib/middleware.ts:44-55

6.4 Content Security Policy

Source: src/drop-app/next.config.ts:6-46

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  frame-ancestors 'none';

Known limitation (Medium severity): unsafe-inline and unsafe-eval required for Next.js dev mode. Production should use nonce-based CSP. Planned Phase 2.


5.7. OWASP Top 10 MitigationsMitigation Matrix

Dependabot+
OWASP Risk MitigationImplementation Status
A01: Broken Access Control RBAC + org-scopedAND WHEREuser_id += UUID? PKson all queries DesignedAll 24 API endpointsImplemented
A02: Cryptographic Failures bcrypt cost 12, JWT HS256, HTTPS TLS 1.33, +httpOnly AES-256 + bcrypt(12) + no PII in JWTcookies Designedauth.ts, utils-server.ts, next.config.tsImplemented
A03: Injection Prisma ORM parameterizedParameterized queries exclusively (no string SQL concat) DesignedAll API routesImplemented
A04: Insecure Design Multi-tenantPass-through orgmodel isolation(no atfund DBcustody), layer,feature immutableflags auditfor cards DesignedArchitecture, feature-flags.tsImplemented
A05: Security Misconfiguration Helmet.js,Security CORSheaders, whitelistdemo credentials gated (noNODE_ENV *!== 'production'), sanitized errors Designednext.config.ts, db.tsImplemented
A06: Vulnerable Components DependabotAll +deps weeklyrecent, npmno known CVEs (audit + lock file2026-02-12) Plannedpackage.jsonImplemented
A07: Auth Failures Ratebcrypt limitinghashing, +session JWTrevocation, rotationrate +limiting, 2FAno +SHA-256 bcrypt(12)legacy Designedauth.ts, middleware.ts, utils-server.tsImplemented
A08: Software Integrity SignedTBD — signed commits +planned CI/CDPhase +3 Planned
A09: Logging Failures ImmutableSentry LoggedAction(error tracking) — audit log table +planned RailwayPhase logs3 MVP: Sentry DesignedPartial
A10: SSRF ZodPass-through validationmodel +limits outbound surface; allowlist forplanned SEF/HR-FISK/FINAPhase API2 DesignedArchitecturePartial

6. Rate Limiting

EndpointLimitWindow
POST /api/v1/auth/login5 req15 min
POST /api/v1/auth/register3 req60 min
POST /api/v1/auth/refresh10 req15 min
GET /api/v1/reports/*10 req15 min
All other /api/v1/*100 req15 min

7. Input Validation (Zod)

const createInvoiceSchema = z.object({
  customerId: z.string().uuid(),
  invoiceDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  currencyCode: z.enum(['EUR', 'RSD', 'BAM']),
  items: z.array(z.object({
    description: z.string().min(1).max(500),
    quantity: z.number().positive(),
    unitPrice: z.number().nonnegative(),
    taxRate: z.number().min(0).max(100),
  })),
});

8. File Upload Security

Allowed: JPG, PNG, PDF. Max 10 MB. MIME + extension validation. Stored in Cloudflare R2 EU. Phase 2: ClamAV scanning.


9. Audit Trail — LoggedAction (APPEND-ONLY)

FieldDescription
eventIdAuto-incrementing
tableNameMutated table
actionINSERT / UPDATE / DELETE
userIdActor
actionTimestampUTC
rowDataFull row before mutation
changedFields{ field: { old: X, new: Y } }
clientIpRequester IP

On GDPR erasure: userId → "deleted-user". Financial entries retained 11 years (law). LoggedAction never deleted.


10. Security Headers (Helmet.js)Checklist

Source: src/drop-app/next.config.ts

script-src
Header Value Status
Strict-Transport-Security max-age=63072000; includeSubDomains; preloadImplemented (fix M2)
Content-Security-Policy default-srcSee 'self';§6.4 Partial 'self' 'unsafe-inline'inline remaining
X-Content-Type-Options nosniffImplemented
X-Frame-Options DENYImplemented
X-Powered-ByReferrer-Policy Removedstrict-origin-when-cross-originImplemented
Permissions-Policycamera=(self), microphone=(), geolocation=(self)Implemented
Cache-Control (auth responses)no-storeTBD — Phase 2

11.9. Pre-LaunchDependency Vulnerability Management

Last dependency review: 2026-02-12 (security audit)

PackageVersionRisk Assessment
jose^6.1.3Low — Well-maintained JWT library
bcryptjs^3.0.3Low — Pure JS bcrypt
better-sqlite3^12.6.2Low — Parameterized queries
next16.1.6Low — Recent version
react19.2.3Low — Latest major
radix-ui^1.4.3Low — UI components only

Remediation SLAs:

SeveritySLA
Critical (CVSS ≥ 9.0)24 hours
High (CVSS 7.0-8.9)7 days
Medium (CVSS 4.0-6.9)30 days
Low (CVSS < 4.0)90 days

Planned: Dependabot + Snyk integration in CI/CD pipeline (Phase 3).


10. Security ChecklistLogging & Audit Trail

Current (MVP): Sentry for error tracking. No structured audit log table.

Planned (Phase 3): Audit log table + SIEM integration.

EventPlanned LoggingAlert?
Login successuser_id, ip, user_agent, timestampNo
Login failureip, email_hash, attempt_count, timestampYes (> 5 failures)
Session revocationuser_id, timestamp, reasonYes
Transaction initiateduser_id, amount, currency, corridor, timestampNo
KYC status changeuser_id, old_status, new_status, timestampYes
AML flag triggereduser_id, rule, transaction_id, timestampYes
Password changeuser_id, ip, timestampYes

AML transaction monitoring thresholds (from legal/hvitvaskingsrutiner.md):

  • Single JWT_SECRETtransaction generated> (32+NOK chars,50,000 CSPRNG) manual Railway env secretreview
  • Daily JWT_REFRESH_SECRETcumulative separate> keyNOK (32+100,000 chars)→ manual review
  • Monthly FIELD_ENCRYPTION_KEYcumulative generated> (32NOK bytes500,000 hex) EDD for PIB/JMBG/OIB/JIB + IBANassessment
  • Structuring HTTPSpatterns enforced
  • automatic CORS: bilko.io only
  •  Rate limiting tested
  •  Helmet.js headers verified
  •  bcrypt rounds = 12
  •  All Prisma queries use org-scoped WHERE
  •  Zod validation on all endpoints
  •  LoggedAction trigger active on all tables
  •  Error responses sanitized
  •  Dependabot alerts enabled
  •  Railway region = EU West confirmed
  •  DPAs signed (Railway, Vercel, Cloudflare, SendGrid)
  •  Data deletion workflow testedflag

Related11. DocumentsIntegration Security