# 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 (security@bilko.io)
**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

```mermaid
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

```mermaid
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)

```typescript
// 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)

```typescript
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

---

## Related Documents
- Compliance Framework: [compliance-framework.md](compliance-framework.md)
- Data Encryption Policy: [data-encryption-policy.md](data-encryption-policy.md)
- Key Management Policy: [key-management-policy.md](key-management-policy.md)
- DPIA: [data-protection-impact-assessment.md](data-protection-impact-assessment.md)
- Breach Response: [data-breach-response-plan.md](data-breach-response-plan.md)
- Security Testing: [security-testing-policy.md](security-testing-policy.md)
- Bilko Security Docs: [../../products/Bilko/docs/security/](../../products/Bilko/docs/security/)

---

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