This page walks through the SQL primitives for enabling PostgreSQL
Row-Level Security on a CYPEX application-schema table and attaching the
baseline organization policy.
The v2.0.0 migration already enabled RLS on CYPEX core tables
(t_object, t_object_field, t_ui, etc.). This page is about enabling
RLS on client application-schema tables during the post-upgrade
multi-tenant setup.
Prefer the automated
client-add-organization-id SQL template
for bulk migrations. Use the primitives below when you need a single-table
or large-table (batched) path.
Prefer (organization_id, <natural_key>) composite indexes for hot list
queries; a lone organization_id index is the minimum.
2. Enable RLS
1
ALTERTABLE<schema>.<table>ENABLEROWLEVELSECURITY;
By default, RLS does not apply to the table owner. To force policies for
the table owner (and any other privileged role), add:
1
ALTERTABLE<schema>.<table>FORCEROWLEVELSECURITY;
Most deployments should FORCE so that ad-hoc sessions as the table
owner do not bypass the policies.
3. Attach the baseline policy
Session organization comes from JWT claims on the request.jwt.claims GUC,
read by cypex.current_organization_id() and cypex.is_admin(). There is no
separate organization session variable in the engine.
Always define bothUSING and WITH CHECK. A USING-only policy can
allow writes that the same policy would reject on read in edge cases;
explicit WITH CHECK is the contract you want for tenant isolation.
system-admin bypass is cypex.is_admin(), which reads the JWT claim
isSuperAdmin — not merely SET ROLE cypex_admin.
To also admit organization administrators across every org in their
organization_ids claim (core catalog style), extend both clauses with:
Do not keep an organization_id IS NULL visibility escape on client
business tables after backfill; NULL rows are visible to every tenant under
that pattern.
A normal user session cannot change organization_id to another org: the
WITH CHECK clause rejects the new row.
8. Disable RLS temporarily
If you need to bypass RLS for a one-off maintenance task, use a privileged
role (BYPASSRLS) or temporarily disable RLS:
1
2
3
ALTERTABLE<schema>.<table>DISABLEROWLEVELSECURITY;-- ... do the work ...
ALTERTABLE<schema>.<table>ENABLEROWLEVELSECURITY;
Re-enabling RLS does not re-evaluate existing policies; they remain in
place.
Pitfalls
Superusers bypass RLS
PostgreSQL superusers bypass RLS by default unless the table has
FORCE ROW LEVEL SECURITY. If your deployment uses a superuser for
application traffic, RLS will not apply. Use a non-superuser role with
BYPASSRLS only for migrations.
Wrong session variable
CYPEX helpers read only request.jwt.claims (org_id, organization_ids,
isSuperAdmin, isOrganizationAdmin). Setting any other session variable has
no effect on the policies. Ad-hoc psql tests must set that GUC (see above).
Literal DEFAULT left after backfill
Adding organization_id BIGINT NOT NULL DEFAULT <migration_org_id> is fine
for the initial backfill, but leaving that literal default in place stamps
every later INSERT that omits the column into the migration org. Always
switch to DEFAULT cypex.current_organization_id() (or drop the default and
require the application to supply the value).
Policy on WITH CHECK
A policy that defines only USING (read predicate) without WITH CHECK
is incomplete for write isolation. Always include both for tenant
predicates.
Enable RLS before backfill
If you enable RLS before every row has organization_id set, backfill
queries run under the policy and can miss rows. Backfill first, then attach
RLS (the template Option 2 order).
Next step
After enabling RLS on the application-schema tables, proceed to
Assign clients to organizations
if you still need to backfill or bulk-assign organizations, or to
Verify and roll back to confirm the
result.