This page describes the multi-tenant Organizations model introduced in CYPEX v2.0.0 and why PostgreSQL Row-Level Security (RLS) is part of it.
Before v2.0.0, a CYPEX instance was effectively single-tenant:
- One database, one application schema, one set of users.
- Multi-tenancy was emulated at the application layer by filtering rows on a client identifier.
- PostgreSQL row-level security was not enabled on tenant-scoped tables.
This worked, but every enforcement decision lived in application code. A
single missing WHERE clause, an ad-hoc reporting query, or a hand-written
maintenance script could leak rows across tenants.
From v2.0.0 on, a CYPEX instance is multi-tenant by construction:
- An Organization is the unit of tenancy. Some tables carry an
organization_idcolumn directly; the rest derive their scope by joining to a table that does. - A user is mapped to one or more Organizations through
cypex.t_role_organization. - Every tenant-scoped table has Row-Level Security enabled, and the policies resolve the active Organization from the caller’s JWT claims.
- CYPEX passes the access token to PostgREST, which exposes its claims on the
request.jwt.claimsGUC. Helper functions such ascypex.current_organization_id()read that GUC inside the policies, so the database itself enforces visibility.
The simplest mental model:
TipOne Organization == one tenant == oneorganization_idvalue, resolved from the caller’s JWT claims and enforced by PostgreSQL RLS.
RLS gives CYPEX defense in depth:
- The application still filters on
organization_id(defense layer 1). - PostgreSQL re-checks the policy predicate against the organization in
request.jwt.claimsfor every row read or written (defense layer 2). - Even direct
psqlsessions and ad-hoc reports inherit the same restrictions, unless the role is privileged (see Roles and permissions).
For background on PostgreSQL Row Security Policies, see the official PostgreSQL documentation.
The upgrade from v1.9.3 to v2.0.0 follows this sequence:
- Backup — full logical backup (
pg_dump) plus, ideally, a filesystem snapshot. - Run migrations — apply the v2.0.0 migration scripts in order: Organizations schema, roles and helper functions, RLS policies, and the default password policy migration.
- Verify — run the verification queries listed in RLS impact on existing data.
- Cut traffic — restart CYPEX so the new role and JWT claims are picked up, then remove the maintenance banner.
The exact SQL each migration applies is documented in the additive changes and breaking changes pages.
| Concept | Where it lives | Purpose |
|---|---|---|
| Organization | cypex.t_organization | Tenant identifier |
| Module ↔ Organization | cypex.t_module_organization | Maps modules to their owning organization |
| Role ↔ Organization | cypex.t_role_organization | Maps roles to organizations they can access |
| Permission audit log | cypex_log.t_permission_audit_log | Records permission decisions |
| Helper functions | cypex.is_organization_admin(), etc. | Read JWT context inside SQL |
| Claims GUC | request.jwt.claims | Carries org_id and organization_ids into the session |
- CYPEX internals — for the underlying platform architecture.
- User management — for the user and role model that Organizations extend.
- Pre-upgrade checklist — for the operational steps before you run the migrations.