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

Governance evidence

6 min read

A change that affects a tenant boundary should be reconstructable afterwards. CYPEX records part of that automatically and leaves the rest to you. This page documents the three layers where evidence lives, in order of increasing persistence:

  1. The permission audit log — queryable in PostgreSQL.
  2. The write path — what CYPEX captures automatically, and what it does not.
  3. Documentation approvals — recorded in product docs and release notes.

If you make a non-trivial decision about an Organization (split, merge, role remap, schema revoke), keep the audit query and its output with the change record, and add an ADR-style note under CYPEX internals.

Layer 1 — querying the audit log

Permission decisions are queryable directly in PostgreSQL, in cypex_log.t_permission_audit_log. Use it to answer “why was this user allowed or denied on this row?”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- The 50 most recent access decisions, newest first
SELECT created_at, action, entity_type, entity_name, user_id, user_name, organization_id
FROM   cypex_log.t_permission_audit_log
ORDER  BY created_at DESC
LIMIT  50;

-- Only the refusals
SELECT created_at, entity_type, entity_name, user_name, context
FROM   cypex_log.t_permission_audit_log
WHERE  action = 'ACCESS_DENIED'
ORDER  BY created_at DESC;

The table is protected by row-level security. A system admin sees every row. An organization admin sees rows for the organizations they are mapped to, and also every row whose organization_id is NULL.

Warning
The permission audit log is deployment-wide evidence, not per-tenant evidence. Only connector-execution rows carry an organization_id; every other kind of row leaves it NULL, and NULL rows are visible to every organization admin. Do not rely on this table to keep one tenant’s decisions hidden from another tenant’s administrator, and do not assume that filtering on organization_id returns a complete picture for that organization.

Layer 2 — the write path

Access-control decisions are recorded in cypex_log.t_permission_audit_log. The audit row carries:

ColumnTypeContents
idbigintSequence-generated primary key.
created_attimestamptzWhen the decision was recorded.
actiontextACCESS_GRANTED, ACCESS_DENIED, CREATE, UPDATE, DELETE, REVOKE, and the connector-execution outcomes EXECUTE_STARTED, EXECUTE_SUCCESS, EXECUTE_FAILED, EXECUTE_BLOCKED, EXECUTE_RETRIED, EXECUTE_TIMEOUT.
entity_typetextschema, organization, user, role, connector, connector_secret, connector_execution, connector_enablement, connector_allowlist.
entity_idtextIdentifier of the affected row.
entity_nametextHuman-readable name of the affected row.
user_idbigintThe acting user. NULL for actions taken by the platform itself.
user_nametextThe acting user’s name at the time of the decision.
user_roletextThe acting user’s role at the time of the decision.
before_statejsonbSnapshot before the change.
after_statejsonbSnapshot after the change.
organization_idbigintSet only for connector executions. Indexed, but NULL for every other kind of row.
ip_addressinetRequest IP.
user_agenttextRequest user agent.
contextjsonbCorrelation id, plus a per-domain payload — for connector executions the operation key, HTTP method, sanitised URL, status code, duration, error category and policy decision.

Both action and entity_type are plain text columns with no database constraint, so a value you have not seen before is possible after an upgrade.

When an audit row is written while handling a request, that request’s correlation id is stamped into context ->> 'correlationId' automatically. The id comes from the inbound x-request-id header when the caller supplies a well-formed one, and is generated otherwise. Rows written outside a request — platform-initiated actions such as connector runtime credential access — have no correlation id, and context may be NULL. Join on context ->> 'correlationId' to reconstruct everything a single request touched.

Organization management is logged separately

Creating, updating or deleting an Organization, and mapping roles to it, is recorded in cypex_log.t_log_api. It does not appear in t_permission_audit_log at all, so “who created this organization?” and “when was this role granted?” must be answered here:

1
2
3
4
SELECT ts, event, description, user_id, payload
FROM   cypex_log.t_log_api
WHERE  event LIKE 'organization.%'
ORDER  BY ts DESC;
OperationStored event value
Create organizationorganization.create
Update organizationorganization.update
Delete organizationorganization.delete
Add role to organizationorganization.role.add
Remove role from organizationorganization.role.remove

The payload column carries the organization id, name and domain. t_log_api also has an organization_id column, but nothing populates it — filter on event and payload, not on that column.

What triggers an audit entry

TriggerWhere it shows up
Schema access (internal-vs-client), org-scope validation, schema-access errorsPermission audit / Schema Access
Access refused on an organization, user or role operationPermission audit — action = 'ACCESS_DENIED', with the reason in context
Connector execution outcomesConnector Audit
Connector secret create/rotate/revoke/readCredentials
Connector CRUDConnectors admin — archiving a connector is recorded as REVOKE, not DELETE
Enablement toggleConnector Execution
Outbound host allowlist changesConnector Allowlist

Successful decisions are recorded as well as refusals, so an empty result for a user is meaningful: it means no decision was ever evaluated, not that access was granted silently.

Connector executions are the one domain with a dedicated admin screen. Open Connector Audit under Monitor & Audit in the sidebar — it is reserved for system admins — instead of querying the table by hand.

Layer 3 — documentation approvals

Some evidence lives outside the database, in product documentation and release artifacts:

EvidenceLocation
Organizations terminology referenceCYPEX Glossary
Default Organization seededThe Organizations-bootstrap migration (part of the v2.0.0 upgrade)
RLS helper functionsThe RLS-helpers migration (part of the v2.0.0 upgrade)
Per-table RLS policiesThe RLS-enablement migration (part of the v2.0.0 upgrade)
v2.0.0 release notes (introduces multi-tenancy)Release Notes v2.0.0
Mandatory operator upgrade entry pointUpgrade to CYPEX v2.0.0
Post-upgrade assignment procedureDetailed organization setup
Future ADR-style evidence (where to put new decisions)CYPEX internals

The three migrations run automatically as part of the upgrade. To confirm they took effect on a given deployment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- Default Organization was seeded
SELECT id, name, organization_domain FROM cypex.t_organization;

-- RLS helper functions are present
SELECT proname FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE  n.nspname = 'cypex'
AND    proname IN ('is_admin', 'is_organization_admin',
                   'current_organization_id', 'current_user_organization_ids');

-- Row-level security is enabled on the tenant-scoped tables
SELECT schemaname, tablename, rowsecurity FROM pg_tables
WHERE  schemaname IN ('cypex', 'cypex_log') AND rowsecurity
ORDER  BY tablename;
Tip

When you make a non-trivial decision about an Organization, leave evidence in at least two of the three layers:

  1. Run the relevant query on cypex_log.t_permission_audit_log and, for organization management, on cypex_log.t_log_api, and keep the output with the change record.
  2. Add an ADR-style note under CYPEX internals describing the decision, the alternatives considered, and the rollback plan.
  3. If the decision changes the migration sequence or the tenancy contract, update the relevant page in this section and the v2.0.0 release notes.

See also