Skip to main content
CYPEX Documentation
Support
v2.0.0 Latest stable release View changelog ->

Behavior changes

4 min read

This page lists the changes in CYPEX v2.0.0 that alter the default behavior of the platform without breaking existing integrations. You should be aware of these changes, but they typically do not require action.

1. Default password policy now enforced

A new row is inserted into cypex.t_config with key password_policy and value:

1
{"minLength": 4, "minUppercase": 0, "minLowercase": 0, "minNumbers": 0, "minSpecial": 0}

This is very permissive by design — it ships a working policy rather than locking existing users out. The insert uses ON CONFLICT (key) DO NOTHING, so:

  • If a password policy is already present in cypex.t_config, the existing policy is kept.
  • If no password policy is present, the v2.0.0 default is inserted.

Admins can override the policy at any time through the admin panel or by updating the row directly. See Default password policy for the verification query and override procedure.

2. All existing data assigned to the Default Organization

A row is inserted into cypex.t_organization with organization_domain = 'default'. Every existing module is mapped to this organization through cypex.t_module_organization, and every existing role is mapped through cypex.t_role_organization.

The effect:

  • The Default Organization is the implicit tenant for all v1.x data.
  • Existing rows in tenant-scoped tables have organization_id IS NULL.
  • RLS policies explicitly allow organization_id IS NULL to be visible to all users mapped to the Default Organization, which is everyone.

This is why existing data remains visible after the upgrade — see RLS impact on existing data for the full mechanism.

3. organization_id is not stamped automatically

The migration creates the function cypex.validate_root_table_organization(), but it is not attached to any table — no CREATE TRIGGER references it. On the cypex and cypex_log tables, nothing in the database stamps or validates organization_id on insert.

The four sso_gateway tables are the exception: they carry a BEFORE INSERT OR UPDATE trigger that stamps organization_id from the caller’s claims and rejects a cross-organization write.

New rows created through CYPEX get organization_id because the application sets it. Direct database writes must supply the column themselves, either explicitly or through a column default such as DEFAULT cypex.current_organization_id().

Warning

A row left with a NULL organization_id is visible to every organization, because the baseline policies include an organization_id IS NULL clause for legacy data. After any direct-SQL load, verify the column is populated:

1
SELECT count(*) FROM cypex.t_ui WHERE organization_id IS NULL;

4. cypex_admin retains full access

The cypex_admin role keeps all its INSERT/UPDATE/DELETE privileges on tenant-scoped tables. Administrative reach is written into the policies themselves: cypex.is_admin() returns true when the JWT carries isSuperAdmin, so every policy admits the system administrator explicitly. No CYPEX role is granted the PostgreSQL BYPASSRLS attribute.

This means:

  • System administrators continue to see and edit every row, regardless of organization — still as a policy decision, not a bypass.
  • Existing admin workflows are not affected.

5. cypex_user sees only rows it can act on

A cypex_user role mapped to the Default Organization sees:

  • All rows where organization_id IS NULL (legacy data).
  • All rows where organization_id matches one of the organizations the user has access to (per cypex.t_role_organization).

A cypex_user mapped to a non-default organization sees only the second category. This is the intended multi-tenant behavior.

6. Direct database sessions must present JWT claims

RLS policies read the caller’s organization from the request.jwt.claims GUC, which PostgREST sets from the access token. Direct database integrations (custom reports, ETL jobs, maintenance scripts) get no claims by default, so they must set the GUC themselves:

1
2
3
4
BEGIN;
SET LOCAL "request.jwt.claims" = '{"org_id": 1, "organization_ids": [1], "isSuperAdmin": false, "isOrganizationAdmin": false}';
SELECT * FROM cypex.t_ui;
COMMIT;

Organization IDs are bigint. Look yours up with SELECT id FROM cypex.t_organization WHERE organization_domain = 'default';.

With no claims set, cypex.current_organization_id() returns NULL: a cypex_user mapped to the Default Organization still sees legacy rows through the organization_id IS NULL clause, while a user mapped to a non-default organization sees nothing. See RLS impact on existing data.

Summary

ChangeWhat you should know
Default password policy enforcedVery permissive; can be overridden.
Existing data assigned to Default OrganizationTransparent to end users.
organization_id is not stamped by a triggerThe application sets it; direct writes must supply it.
cypex_admin retains full accessAdmin workflows unaffected.
cypex_user sees only its own rowsPer t_role_organization.
Direct DB sessions need request.jwt.claimsSet the GUC before querying.