The guarantees
Tenant isolation
Every exposed table has row-level security. A query from one company cannot return another’s rows — on the web path and the MCP path alike.
No admin bypass on private context
personal_context and focus_items are owner-scoped. No role reads around it, and private records never enter org-scoped retrieval, logs or analytics.AI cannot change shared truth
Company writes go through proposals and human approval. Direct writes are private-scope only.
Versioned history
Shared-truth writes create a version row in the same transaction, enforced by a database trigger. Restoring creates a new version; history is never rewritten.
How this is enforced rather than promised
Row-level security is the tenancy mechanism, not a layer on top of one. The consequence worth understanding: the retrieval path that assembles context for an AI does no permission filtering of its own, by design. A bug there returns too little context rather than someone else’s. Versioning is a trigger rather than a convention, so no write path can forget it. Roles are checked in the database rather than in the description a model reads before calling a tool.Asserted in CI, not documented in a checklist
Asecurity_lint() function runs against the schema and checks:
- RLS coverage on every exposed table
- role-scoped policies
search_pathpinning on security-definer functionsPUBLICgrants on internal functions
PUBLIC grant that left the rate limiter callable, and default privileges exposing a table intended to be internal — which is the argument for having it.
Tenant isolation is covered by integration tests that run against a real Postgres as genuine signed-in users, not as a privileged client. A test that passes because it used the service key proves nothing.