How workspace isolation works
Postgres row-level security keeps your workspace data invisible to every other workspace, even if application code has bugs. Here is how Ochre uses it.
Multi-tenant SaaS gets isolation wrong all the time. The usual failure mode is "we filter by org_id in the application," which works until somebody forgets to filter once. Ochre uses Postgres row-level security so that filter is enforced by the database itself.
The model in one sentence
Every public table in Ochre carries an org_id column, and every public table has an RLS policy that says: a request can only see rows where org_id matches the caller's authenticated org.
That is the whole rule. The rest of this article is what it means in practice.
What org_id is
When you sign up, Ochre creates an organization, called an org. Every conversation, message, customer, integration, routing rule, AI draft, and webhook subscription gets stamped with that org_id. Your team members are linked to the org through a memberships table, also RLS-scoped.
There is no row in any public table that does not belong to a specific org. Cross-org joins are not possible through the public API path.
How RLS is enforced
When the application makes a query as a normal authenticated user, Supabase runs that query under a Postgres role with RLS turned on. The policy on each table reads roughly:
> Allow this row if org_id equals the org the caller is currently a member of.
If the application code forgets to add where org_id = ?, the database still returns nothing for other orgs. There is no leak path through application bugs.
The policy uses a current_org_id() helper that resolves from the authenticated session, so policies stay short and consistent across tables.
What about the service role?
Some operations cannot be expressed under RLS: webhook receivers that need to look up an org by an external ID, billing webhooks from Stripe that arrive without a user session, scheduled jobs that span the system. These run under a Postgres service role that bypasses RLS.
Service-role code is treated like a sharp tool:
- It only runs on the server, never in the browser. The service key is not in any client bundle.
- Every entry point that uses it re-checks the caller's org explicitly before reading or writing anything tenant-specific.
- Inbound webhooks are signature-verified before any service-role code runs. See How Ochre verifies inbound webhooks.
When we add a new endpoint that needs the service role, the review checklist asks: does this endpoint receive a user session? If yes, does it pin all reads and writes to that user's org? If no, does it have an alternate trust boundary (signed webhook, internal cron)?
What this means for you
A few practical consequences.
- Bugs in our application code cannot expose other orgs. RLS is the floor, not the ceiling.
- Sharing a conversation across orgs is not possible. There is no UI for it because there is no schema for it.
- Slack Connect channels are tenant-scoped. When two orgs talk to each other through Slack Connect, each side sees only its own copy.
- Workspace deletion really means deletion. Every row tagged with that
org_idis gone after the 30-day grace period.
Common questions
Is RLS slow? No. RLS policies compile down to predicates the planner can index. Every org_id column in Ochre is indexed.
What about analytics or reports? Reports run as the calling user, so they see only that user's org. Aggregate metrics across customers (for our own benchmarking) are computed on derived data without identifiers.
Can your engineers read my data? A small group of engineers has access to production Postgres for incident response. Access is logged, scoped, and audited at the platform layer (Supabase). We do not browse customer data for any other reason.
Related
Was this article helpful?