Skip to content

Keeping each firm's data apart

Every table checks, row by row, whether you're allowed to see it, based on your organization and your projects. (Row-level security keyed by org_id and project_id, with the role checks as security-definer functions the policies call.)

How a row decides who may see it

Every row knows which firm, and usually which project, it belongs to, so the check is one quick lookup. Every table carries org_id, and the project-scoped ones also carry project_id, denormalized so a policy is one indexed check. The policies call a small set of functions — lt_me(), lt_my_staff_orgs(), lt_my_projects(), lt_has_org_role(org, roles…), lt_is_platform_admin() — which run as security definer so they can read the membership tables the caller cannot.

Staff (employees and AI assistants) see their organization's projects. Outside parties — clients, subcontractors, vendors, partners — see only projects they were explicitly granted, never the staff directory or the firm's finances. Saved queries (views) are declared security_invoker, so a view can't read around the policy on its base tables.

Files follow the same rule

A file is visible to exactly the people who can see the record it belongs to. A storage object has no org_id, so the storage policy asks the database which record references the path — lt_evidence_ref(name) returns the project and organization that own it — and applies the same visibility as that record. An upload nothing references is visible to nobody but the uploader and platform staff.

A child cannot leave its project
A guard on nine child tables refuses a row whose org_id differs from its project's org_id, so moving a project moves its record and nothing is stranded.
Privilege does not escalate
There is no client write path to platform_admins. An org admin cannot grant themselves a role on another organization's project.
Proven, not described
The tests build the real database in memory and query it as each kind of person, with the security rules switched on (the real migrations, an in-process Postgres, RLS active). No network, no mocks.
Where to check it yourself
  • · db/002, db/005, db/052, db/058, db/060, db/075
  • · test/tenancy.test.mjs, test/external-access.test.mjs, test/evidence-access.test.mjs, test/project-org-guard.test.mjs

These are files in our codebase. If you’re a customer under agreement, we’ll walk you through any of them.