Skip to main content

Security Architecture

Security Architecture Document

Project: Bilko — Balkan Accounting SaaS Version: 1.0 Date: 2026-02-23 Author: Compliance Architect Status: Draft Reviewers: CTO, DPO, Engineering Lead Classification: Confidential

Document History

Version Date Author Changes
0.1 2026-02-23 Compliance Architect Initial draft — Bilko security architecture

1. Security Architecture Overview

Security Owner: Compliance Architect ([email protected]) Last Security Review: 2026-02-23 Next Scheduled Review: 2026-08-23 Compliance Targets: GDPR | Zakon o zaštiti podataka o ličnosti RS (ZZPL) | Zakon o zaštiti ličnih podataka BiH (ZZLP) | GDPR via AZOP (HR) | Zakon o računovodstvu RS/HR/BA | Zakon o PDV RS/BA/HR

Architecture Model: Bilko is a multi-tenant cloud accounting SaaS. Processes invoices, expenses, VAT returns, and financial reports for organizations in Serbia, Bosnia & Herzegovina, and Croatia. Each organization's data strictly isolated by organizationId at the database layer.

Defense-in-Depth Overview

graph TD
    CLIENT["Client Browser / PWA"]

    subgraph NETWORK["Network Layer"]
        CF["Cloudflare WAF\nDDoS Protection\nTLS 1.3 termination\nHSTS"]
    end

    subgraph APP_LAYER["Application Layer"]
        HELMET["Helmet.js\nCSP + X-Frame + HSTS\nno 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 raw SQL for user input\norg-scoped WHERE clauses"]
        PG_ENC["PostgreSQL (Railway EU West)\nAES-256 disk encryption\nbackup encryption"]
    end

    subgraph AUDIT["Audit Layer"]
        LOG["LoggedAction table\nAPPEND-ONLY\nIP + user + timestamp\nold/new values (changedFields)"]
    end

    CLIENT --> CF --> HELMET --> CORS --> RATE --> AUTH_MW --> RBAC_MW --> ZOD --> PRISMA_ORM --> PG_ENC
    PRISMA_ORM --> LOG

2. Authentication

2.1 Strategy: JWT (JSON Web Tokens)

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

2.2 JWT Auth Flow

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: SELECT user WHERE email = ? (parameterized)
    DB-->>API: User record (passwordHash)
    API->>API: bcrypt.compare(password, hash) — 12 rounds
    alt Password valid
        API->>API: jwt.sign({sub, org, role}, JWT_SECRET, 15m)
        API->>DB: INSERT refreshToken (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 userId = ?
    API-->>FE: 204 No Content

2.3 Two-Factor 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 Matrix

Action owner admin accountant viewer
Create invoice
Edit invoice
Delete invoice
View invoice
Approve expense
Generate report
Invite user
Edit org settings

3.2 Organization Scoping (IDOR Prevention)

// Injected by auth middleware on all /api/v1/* 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.

4.2 At Rest: AES-256

Store Method Location
PostgreSQL AES-256 TDE (Railway) Railway EU West (Frankfurt/Paris)
PostgreSQL backups AES-256 auto-backup Railway EU West — 30 days
Tax IDs (PIB/JMBG/OIB/JIB), IBAN AES-256-GCM field encryption Application layer — Railway env secret
Cloudflare R2 (receipts, PDFs) AES-256 server-side Cloudflare EU region

4.3 Password Security

bcrypt, 12 salt rounds. Min 8 chars. Block top 10K common passwords. Last 5 hashes retained.

4.4 Financial Data Precision

All monetary amounts: NUMERIC(19,4) — never float. Exchange rates locked at transaction date.


5. OWASP Top 10 Mitigations

OWASP Risk Mitigation Status
A01: Broken Access Control RBAC + org-scoped WHERE + UUID PKs Designed
A02: Cryptographic Failures TLS 1.3 + AES-256 + bcrypt(12) + no PII in JWT Designed
A03: Injection Prisma ORM parameterized queries exclusively Designed
A04: Insecure Design Multi-tenant org isolation at DB layer, immutable audit Designed
A05: Security Misconfiguration Helmet.js, CORS whitelist (no *), sanitized errors Designed
A06: Vulnerable Components Dependabot + weekly npm audit + lock file Planned
A07: Auth Failures Rate limiting + JWT rotation + 2FA + bcrypt(12) Designed
A08: Software Integrity Signed commits + CI/CD + Dependabot Planned
A09: Logging Failures Immutable LoggedAction table + Railway logs + Sentry Designed
A10: SSRF Zod validation + allowlist for SEF/HR-FISK/FINA API Designed

6. Rate Limiting

Endpoint Limit Window
POST /api/v1/auth/login 5 req 15 min
POST /api/v1/auth/register 3 req 60 min
POST /api/v1/auth/refresh 10 req 15 min
GET /api/v1/reports/* 10 req 15 min
All other /api/v1/* 100 req 15 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)

Field Description
eventId Auto-incrementing
tableName Mutated table
action INSERT / UPDATE / DELETE
userId Actor
actionTimestamp UTC
rowData Full row before mutation
changedFields { field: { old: X, new: Y } }
clientIp Requester IP

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


10. Security Headers (Helmet.js)

Header Value
Strict-Transport-Security max-age=63072000; includeSubDomains; preload
Content-Security-Policy default-src 'self'; script-src 'self' 'unsafe-inline'
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-Powered-By Removed

11. Pre-Launch Security Checklist

  • JWT_SECRET generated (32+ chars, CSPRNG) — Railway env secret
  • JWT_REFRESH_SECRET separate key (32+ chars)
  • FIELD_ENCRYPTION_KEY generated (32 bytes hex) — for PIB/JMBG/OIB/JIB + IBAN
  • HTTPS enforced
  • 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 tested


Approval

Role Name Date Signature
Author Compliance Architect 2026-02-23
CTO
DPO
Engineering Lead