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)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. It processesProcesses invoices, expenses, VAT returns, and financial reports on behalf offor 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
—JWT, scales horizontally onRailwayRailway. Works with Next.js frontend and PWAIndustry standard for multi-tenant SaaS
2.2 Token Types
| Access | |||
Refresh tokens stored(15 asmin, HMAC-SHA-256memory-only) hash+ inrefresh database.tokens Raw(7 tokendays, neverhttpOnly stored.cookie). Rotation on every use.refresh. Revocation via hashed token storage in DB.
2.32 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)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->>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.43 Two-Factor Authentication (2FA)
Method: TOTP (RFC 6238) — Google Authenticator, Authy, 1Password, Microsoft Authenticator1Password
- 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→returns{ requires2FA: true, tempToken }→POST /api/v1/auth/2fa/login Backup codes:Backup: 10 single-use codes, bcrypt-hashed, marked used after redemptionhashed
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)
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 throughout — no sequential IDsID enablingenumeration enumeration.possible.
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
Frontendtraffic(Vercel):HTTPS.Automatic HTTPS,Cloudflare TLS 1.3Backend (Railway): Automatic HTTPS, TLS 1.3Cloudflare: TLS 1.3 terminationat edge, re-encrypted tooriginRailway. - HSTS:
max-age=63072000; includeSubDomains; preload
All
4.2 At Rest: AES-256
| Store | Method | Location |
|---|---|---|
| PostgreSQL | AES-256 |
Railway EU West (Frankfurt/Paris) |
| PostgreSQL backups | AES-256 |
Railway EU West — |
| Tax |
AES-256-GCM field encryption |
Application layer — Railway |
| Cloudflare R2 ( |
AES-256 server-side | Cloudflare EU region |
4.3 Password Security
bcrypt, 12 salt rounds. Min 8 chars. Block top 10K common passwords.
Retain lastLast 5hashes.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 |
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 |
Implementation: express-rate-limit, per-IP, 429 Too Many Requests.
7. Input Validation (Zod)
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).PDF. Max 10 MB.- MIME
type+ extension validation. Stored in Cloudflare R2(EU region). PlannedEU. Phase 2: ClamAVvirus scanning before R2 upload.
9. Audit Trail
— LoggedAction Table (APPEND-ONLY)
| Field | Description | |
|---|---|---|
| eventId | Auto-incrementing | |
| tableName | ||
| action | INSERT / UPDATE / DELETE | |
| userId | ||
| actionTimestamp | UTC |
|
| rowData | Full row |
|
| changedFields | { field: { old: X, new: Y } } |
|
| clientIp | Requester IP |
Never delete from LoggedAction. On GDPR erasure: userId anonymized to→ "deleted-user". Financial log entries retained for11 legal compliance periodyears (10-11law). years).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
generatedseparate key (separate key,32+ chars) - FIELD_ENCRYPTION_KEY generated (32 bytes hex) — for
taxPIB/JMBG/OIB/JIBIDs and+ IBAN - HTTPS enforced
— no HTTP -
CORS whitelist: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
-
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:Docs: ../../products/Bilko/docs/security/SECURITY-ARCHITECTURE.md
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Compliance Architect | 2026-02-23 | |
| CTO | |||
| DPO | |||
| Engineering Lead |