Organizations and isolation
Organizations never see or affect each other's data. Three independent layers make sure of it, so a bug in one is caught by the next.
Every organization’s data lives in the same Postgres database, and every row records the organization it belongs to. This page explains how a request for one organization is kept from reading or writing another organization’s rows, even when the code serving it has a bug.
Layer 1: one scoped transaction per request
Request handlers never get a connection pool. The only way a handler reaches the database is a transaction the server opens for it after it:
- reads the
Authorization: Bearercredential, - verifies it and resolves it to an organization and an active user of that organization, together with how they authenticated, whether the session has passed its second factor, and any OAuth scopes,
- opens a transaction and sets the organization for that transaction only.
A handler cannot make up a context for another organization: only the verifier creates one. Login and OAuth endpoints, which run before anyone is signed in, get a narrower transaction scoped to the organization named in the URL.
Queries still filter by organization explicitly. The next layer is the backstop, not the only check.
Layer 2: Postgres row level security
Every table has row level security enabled and forced, so it applies to the table owner too. Every table that holds an organization’s data carries the same policy for reads and writes:
CREATE POLICY org_isolation ON users TO happy_enterprise_org
USING (org_id = app_current_org_id())
WITH CHECK (org_id = app_current_org_id());org_id is the row’s organization ID, and app_current_org_id()
returns the organization set for the current transaction. Three properties
follow:
- A query that forgets to filter by organization still sees only its own
organization’s rows.
USINGfilters everySELECT,UPDATE, andDELETE. - A write cannot produce a row of another organization.
WITH CHECKrefuses any inserted or updated row whose organization is not the transaction’s. - No organization means no rows. When no organization is set, the comparison is never true and every policy matches nothing. A request that somehow skipped the first layer would see and change nothing.
The setting is transaction-local, so a pooled connection never carries one organization’s scope into its next use.
One table carries a second policy: objects also have a restrictive policy,
object_access, that limits a member to the objects of restricted types
they may see, down to the objects shared with them.
A restrictive policy can only narrow what the organization policy shows,
never widen it, so it cannot reach across organizations either.
Database roles
The server connects with two runtime roles, neither a superuser and neither able to bypass row level security:
| Role | Used by | Can reach |
|---|---|---|
happy_enterprise_org | the organization API | organization data, under the policy above; column-level grants keep it from changing ids, user kinds, or email addresses |
happy_enterprise_management | the management API | only the list of organizations and their identity provider keys: no grants on organization data |
Migrations run as a third, owning role (DATABASE_URL). The migrations
check that the runtime roles cannot bypass row level security, and the server
refuses to start if either connection is logged in as the wrong role or as a
role that can. Triggers run with the caller’s rights, so they too see only
the caller’s organization, and there are no functions or views that run with
elevated rights.
Layer 3: tests
Integration tests run against a real Postgres and check, for every table of organization data found in the database catalog, that:
- reads across organizations return nothing, including
SELECTs with noWHERE, - inserts and updates across organizations fail, and
UPDATE/DELETEwithout aWHEREtouch only the caller’s rows, - a transaction without an organization sees and changes nothing,
- the organization scope does not outlive its transaction,
- every table has forced row level security, organization tables have exactly the expected policy (objects also the restrictive one), and the role grants are minimal.
Because the tests read the catalog, a new table without the policy fails them.
Isolation and authorization rules are enforced by PostgreSQL row-level security.
A formally verified model
The access rules are also written as a formally verified model: machine-checked proofs show that the rules, as modelled, keep their promises. Among them:
- an organization-scoped read returns only that organization’s rows, whatever the query, and a write leaves every other organization’s rows unchanged;
- without an organization in scope, nothing is read or written;
- a credential is accepted only for its own organization, and a token is never accepted by two of the organization API, the MCP endpoint, and the management API.
The code is tested against that model, with the integration tests above and the permission rules. The model is verified; the code itself is not formally verified.
What this does not cover
These layers keep a request handler that forgets to filter by organization,
or filters by the wrong one, from reaching another organization’s data. They
are not a sandbox for code that sets out to get around them, and they do not
make it safe to run arbitrary agent-written code inside the server.
Code running in the server process has the server’s own configuration: the
connection strings for both runtime roles (and for the owning role, when the
server runs migrations) and SERVER_SECRET_KEY. Such code could open its own
connection or sign its own session tokens.
Review server code an agent writes as you would any other change to the server. The one place Happy Enterprise runs untrusted code is custom validators, in a sandbox with no filesystem, network, or imports.
Ids
Every id is a cuid2: 24 characters
matching ^[a-z][a-z0-9]{23}$, enforced by the Postgres domain cuid and
generated by the application. Ids are not secret, but they are not
sequential either, so they say nothing about how many rows exist.
Not revealing what exists
Isolation also covers what an outsider can learn. Every authentication
failure is the same 401 unauthorized, whether the organization, user, key,
session, invite, or kid exists or not. Login and invite endpoints answer
for an unknown organization exactly as for a failed login, and OAuth
discovery and client registration answer for one exactly as for a real one.
Resources a caller may not see (a restricted object type, another member’s
avatar) are 404, not 403.