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
Related Documents
- Compliance Framework: compliance-framework.md
- Data Encryption Policy: data-encryption-policy.md
- Key Management Policy: key-management-policy.md
- DPIA: data-protection-impact-assessment.md
- Breach Response: data-breach-response-plan.md
- Security Testing: security-testing-policy.md
- Bilko Security Docs: ../../products/Bilko/docs/security/
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Compliance Architect | 2026-02-23 | |
| CTO | |||
| DPO | |||
| Engineering Lead |
No comments to display
No comments to display