Each customer's workspace is walled off from every other, directly inside the database. Not a rule we enforce with the app — a rule enforced by the database engine itself. There is no path from one customer's session to another's.
Roles don't just hide buttons — they decide what every single action is allowed to do. Plus a second layer of rules on top: who can approve what, within which department, up to what amount. Checked on every click.
Every submission, approval, question, and role change is recorded. Nobody can edit the record after the fact — not us, not your administrator, not even the database's top-level user. The history is the history.
When someone joins, they get what their role requires — automatically, from your identity system. When someone leaves or changes role, their access ends within seconds. Auditors and board-level guests get a clear start date and expiry.
Your data is yours — no other customer can ever see it. Enforced by the database itself, not just by the app.
Every tenant-scoped query runs inside a transactional context that sets a Postgres session variable. Row-level security policies on every table match on that variable before returning a row. The bypass path requires a per-process randomized secret, so a developer can't accidentally hit cross-tenant data from a forgotten `db.query()`.
Two layers of control: what your role lets you do in general, and what's allowed for this specific request — in this specific department, at this specific amount.
RBAC determines the set of capabilities a role can exercise — a fine-grained permission system with clean separation between administrative and operational capability. ABAC then evaluates the specific request: can the submitter approve their own document, is the Head of Department acting within their department, does the amount exceed their threshold.
When a role changes or a user leaves, their active sessions are invalid within seconds.
Every active session is checked against a revocation signal on every request. A role change, deactivation, password change, access expiry, or tenant-wide revocation forces re-authentication on the next request. Two-factor sign-in uses standard time-based codes with secured recovery codes. Per-tenant IP allowlisting (both IPv4 and IPv6) is available as a hard sign-in gate.
Every action, permanent. Every export, regulator-ready.
The audit log records actor, role, action, entity, IP, user-agent, and payload for every meaningful event. Role changes and board-access lifecycle events have their own dedicated immutable trails. Database triggers block UPDATE and DELETE on audit rows — the only way to remove them would be to drop the table, which requires DDL that is not granted to the application user.
People come and go — access keeps up. Joiners get what they need; leavers lose access within seconds.
User provisioning flows in automatically from your identity provider (Okta, Microsoft Entra, Google Workspace). Auditor and board-level guests get a clear start date and expiry — access is withdrawn on the dot without manual clean-up. Document retention follows your plan; the audit trail itself is never purged while the account is active.
Encrypted from your browser all the way to our database — and again on top of that, for the most sensitive pieces like two-factor codes and single sign-on tokens.
All traffic is encrypted with modern TLS. The application database is managed PostgreSQL with encryption at rest and automated daily snapshots. Sensitive application secrets — two-factor codes, single sign-on tokens — are encrypted again at the application layer before being written, so even a database leak would not expose them. Attachments live in encrypted object storage and are delivered through short-lived, per-upload URLs.