Tenant isolation
Every customer gets its own Postgres schema. There is no shared "data" table behind a tenant_id filter — a query in tenant A literally cannot see tenant B's rows because the schema isn't on its search path.
django-tenants · schema-per-tenant · enforced at connection level
Encrypted at rest
Integration OAuth tokens, signup payloads and other sensitive fields are encrypted with Fernet (AES-128 in CBC + HMAC-SHA256). The keys live outside the database — even a stolen DB dump is inert.
cryptography.fernet · key rotation supported
Server-side RBAC
Permissions are checked at the API, not the UI. Every protected endpoint inherits a single mixin that filters querysets by the caller's resource × action × scope. The web client mirrors the rules for UX, but a forbidden API call never reaches the row.
DRF · ScopedQuerysetMixin · HasRolePermission
EU hosting
Data lives in EU regions (Belgium / Frankfurt). Backups, replicas, and worker queues never leave the EU. The application clock is Europe/Brussels and currency is € — both intentional, both relevant for procurement.
Belgian-incorporated · EU data residency · no US sub-processors for storage
Audit history
Every change to tasks, comments, time entries, attachments and tags is captured by django-simple-history. Queryable for compliance, exportable on Business. Tag adds / removes and viewer changes get their own audit events — not just diffs of the parent row.
retention: 90 d (Free) · 1 y (Pro) · 3 y (Business)
Account safety
Passwords hashed with Argon2 — the current state-of-the-art, winner of the Password Hashing Competition. Resets use a 6-digit OTP with a 15-minute TTL and a hard 5-attempt cap. Brute-force protection via django-axes; signup endpoints rate-limited per IP and per email domain.
Argon2 · django-axes · OTP TTL 15 min · 5-attempt lockout
Last-admin safeguard
The API refuses to deactivate the workspace's last admin, demote the last admin's role, or delete the last admin user — returning HTTP 409 with a clear error. The org-locking accident that takes down a workspace at 4 PM on a Friday literally cannot happen.
enforced server-side · tested in apps/roles/tests/test_roles.py
Documented API
Every endpoint is described by an auto-generated OpenAPI 3 schema at /api/schema/ with interactive Swagger UI and ReDoc. Our own TypeScript clients regenerate from the same schema, so the docs can't drift from the code.
drf-spectacular · Swagger UI · ReDoc · Orval-generated TS clients