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 (HR via AZOP) | Zakon o računovodstvu RS/HR/BA | Zakon o PDV RS/BA/HR
Architecture Model: Bilko is a multi-tenant cloud accounting SaaS. It processes invoices, expenses, VAT returns, and financial reports on behalf of organizations in Serbia, Bosnia & Herzegovina, and Croatia. Each organization's data is 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)
Why JWT:
- Stateless — scales horizontally on Railway
- Works with Next.js frontend and PWA
- Industry standard for multi-tenant SaaS
2.2 Token Types
| Token | Lifetime | Storage | Contains |
|---|---|---|---|
| Access token | 15 minutes | Authorization: Bearer header (memory only) | userId, organizationId, role |
| Refresh token | 7 days | httpOnly cookie (secure, sameSite=strict) | userId |
Refresh tokens stored as HMAC-SHA-256 hash in database. Raw token never stored. Rotation on every use.
2.3 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->>API: jwt.sign({sub}, JWT_REFRESH_SECRET, 7d)
API->>DB: INSERT refreshToken (hashed, expiresAt)
API-->>FE: 200 { accessToken } + Set-Cookie: refreshToken (httpOnly, secure)
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
FE->>API: POST /api/v1/auth/refresh (Cookie: refreshToken)
API->>DB: SELECT refreshToken WHERE hash = hash(token) AND expiresAt > NOW()
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
FE->>FE: Clear accessToken from memory
2.4 Two-Factor Authentication (2FA)
Method: TOTP (RFC 6238) — Google Authenticator, Authy, 1Password, Microsoft Authenticator
- Setup:
POST /api/v1/auth/2fa/setup→ QR code + base32 secret - Verify:
POST /api/v1/auth/2fa/verify { code }→ 2FA enabled - Login:
POST /api/v1/auth/login→{ requires2FA: true, tempToken }→POST /api/v1/auth/2fa/login - Backup codes: 10 single-use codes, bcrypt-hashed, marked used after redemption
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 | ✅ | ❌ | ❌ | ❌ |
| Delete org | ✅ | ❌ | ❌ | ❌ |
3.2 Organization Scoping (IDOR Prevention)
Every query scoped to authenticated user's organizationId via middleware:
// 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 — no sequential IDs enabling enumeration.
3.3 RBAC Middleware
function requireRole(roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.post('/api/v1/invoices', requireRole(['owner', 'admin']), createInvoice);
app.delete('/api/v1/invoices/:id', requireRole(['owner']), deleteInvoice);
4. Encryption
4.1 In Transit: TLS 1.3
- Frontend (Vercel): Automatic HTTPS, TLS 1.3
- Backend (Railway): Automatic HTTPS, TLS 1.3
- Cloudflare: TLS 1.3 termination at edge, re-encrypted to origin
- HSTS:
max-age=63072000; includeSubDomains; preload
4.2 At Rest: AES-256
| Store | Method | Location |
|---|---|---|
| PostgreSQL | AES-256 disk encryption (Railway TDE) | Railway EU West (Frankfurt/Paris) |
| PostgreSQL backups | AES-256 (Railway automatic) | Railway EU West — 30-day retention |
| Tax IDs, IBAN | AES-256-GCM field encryption (application layer) | Railway EU West |
| Cloudflare R2 (files) | AES-256 server-side | Cloudflare EU region |
4.3 Password Security
- bcrypt, 12 salt rounds. Min 8 chars. Block top 10K common passwords. Retain last 5 hashes.
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 committed | 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 calls | 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 |
Implementation: express-rate-limit, per-IP, 429 Too Many Requests.
7. Input Validation
All inputs validated with Zod schemas:
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 (receipts, invoice attachments). Max 10 MB.
- MIME type + extension validation. Stored in Cloudflare R2 (EU region).
- Planned Phase 2: ClamAV virus scanning before R2 upload.
9. Audit Trail
LoggedAction Table (APPEND-ONLY)
| Field | Type | Description |
|---|---|---|
| eventId | BIGSERIAL | Auto-incrementing |
| tableName | TEXT | Which table was mutated |
| action | TEXT | INSERT / UPDATE / DELETE |
| userId | UUID | Who performed the action |
| actionTimestamp | TIMESTAMPTZ | UTC timestamp |
| rowData | JSONB | Full row snapshot before mutation |
| changedFields | JSONB | { field: { old: X, new: Y } } |
| clientIp | INET | Requester IP |
Never delete from LoggedAction. On GDPR erasure: userId anonymized to "deleted-user". Financial log entries retained for legal compliance period (10-11 years).
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'; img-src 'self' data: https: |
| X-Content-Type-Options | nosniff |
| X-Frame-Options | DENY |
| X-Powered-By | Removed |
| Referrer-Policy | strict-origin-when-cross-origin |
11. Pre-Launch Security Checklist
- JWT_SECRET generated (32+ chars, CSPRNG) — Railway env secret
- JWT_REFRESH_SECRET generated (separate key, 32+ chars)
- FIELD_ENCRYPTION_KEY generated (32 bytes hex) — for tax IDs and IBAN
- HTTPS enforced — no HTTP
- CORS whitelist: 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
- File upload restrictions active
- LoggedAction trigger active on all tables
- Error responses sanitized — no stack traces
- Dependabot alerts enabled
- Railway region = EU West confirmed
- DPAs signed (Railway, Vercel, Cloudflare, SendGrid)
- Data deletion workflow tested end-to-end
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 Architecture: ../../products/Bilko/docs/security/SECURITY-ARCHITECTURE.md
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Compliance Architect | 2026-02-23 | |
| CTO | |||
| DPO | |||
| Engineering Lead |