Skip to content

SQL Views

Identity Atlas creates SQL views automatically via the migration system. These views handle the heavy lifting — recursive membership resolution and permission assignment aggregation — so your queries stay simple.

Views are created by migration files in app/api/src/db/migrations/ and applied automatically when the web container starts.


Resource Permission Views

Created by migration 005_views.sql; the two matrix views were promoted to materialized views in 013_matrix_matviews_and_indexes.sql and last rebuilt by 049_governed_intent_rows.sql.

View Kind Output columns Purpose
vw_ResourceMembersRecursive Standard view resourceId, principalId, principalType, membershipType, depth, path All memberships (direct + indirect via nested groups) using a recursive CTE. Cycle-safe, max 10 levels deep. membershipType is Direct at depth 1 and Indirect deeper; path is the resolved chain of ids.
vw_ResourceUserPermissionAssignments Materialized view resourceId, principalId, principalType, membershipType, managedByAccessPackage The matrix surface — one row per effective (subject, resource, membershipType) cell. managedByAccessPackage flags cells covered by a governance resource, for IST vs SOLL analysis.
vw_UserPermissionAssignments Standard view (compat alias) groupId, memberId, principalType, membershipType, managedByAccessPackage Backward-compatible alias over vw_ResourceUserPermissionAssignments with the older groupId/memberId column names.

vw_ResourceUserPermissionAssignments is materialized — refresh it

This view holds a stored snapshot. After ingesting or changing assignment data you must run REFRESH MATERIALIZED VIEW "vw_ResourceUserPermissionAssignments"; (or the concurrent variant, see Materialized Views) before it reflects the new data. vw_UserPermissionAssignments reads from it, so it follows the same refresh.

membershipType is one of Direct, Indirect, Eligible — the three universal "how does the subject have this" values. (Owner was retired in 046_owner_as_resource.sql: ownership is now a Direct assignment on a separate GroupOwnership resource.)

-- Who has access to a specific resource, including indirect memberships?
SELECT rmr."principalId", p."displayName", rmr."membershipType", rmr."depth", rmr."path"
FROM "vw_ResourceMembersRecursive" rmr
JOIN "Principals" p ON p."id" = rmr."principalId"
WHERE rmr."resourceId" = 'your-resource-guid'
ORDER BY rmr."depth";

-- A user's complete permission picture across all resources
SELECT v."resourceId", r."displayName", v."membershipType", v."managedByAccessPackage"
FROM "vw_ResourceUserPermissionAssignments" v
JOIN "Resources" r ON r."id" = v."resourceId"
WHERE v."principalId" = 'user-guid-here';

-- How many permissions does each user hold?
SELECT v."principalId", p."displayName", COUNT(*) AS "permissionCount"
FROM "vw_ResourceUserPermissionAssignments" v
JOIN "Principals" p ON p."id" = v."principalId"
GROUP BY v."principalId", p."displayName"
ORDER BY "permissionCount" DESC;

Governance View

Created by migration 005_views.sql; promoted to a materialized view in 013_matrix_matviews_and_indexes.sql and last rebuilt by 049_governed_intent_rows.sql.

View Kind Output columns Purpose
vw_UserPermissionAssignmentViaBusinessRole Materialized view userId, groupId, resourceId, businessRoleId Maps users through governance resources (business roles / access packages) to the resources those roles Contains. groupId and resourceId are the same contained-resource id, exposed under both names.

Materialized — refresh required

Like the matrix view, this is a materialized view. Run REFRESH MATERIALIZED VIEW "vw_UserPermissionAssignmentViaBusinessRole"; after changing governance data before querying it.

-- Which resources does a user reach via business role governance?
SELECT v."userId", v."resourceId", r."displayName" AS "resourceName",
       v."businessRoleId", br."displayName" AS "businessRoleName"
FROM "vw_UserPermissionAssignmentViaBusinessRole" v
JOIN "Resources" r  ON r."id"  = v."resourceId"
JOIN "Resources" br ON br."id" = v."businessRoleId"
WHERE v."userId" = 'user-guid-here';

Materialized Views

Two of the views above are PostgreSQL materialized views — vw_ResourceUserPermissionAssignments and vw_UserPermissionAssignmentViaBusinessRole. They were promoted from standard views in 013_matrix_matviews_and_indexes.sql because recomputing the matrix from scratch on every /api/permissions request was taking 100+ seconds on large (2M+ row) datasets.

Because they store a snapshot, they must be refreshed after the underlying ResourceAssignments / ResourceRelationships data changes:

-- Standard refresh (locks the matview for the duration)
REFRESH MATERIALIZED VIEW "vw_ResourceUserPermissionAssignments";
REFRESH MATERIALIZED VIEW "vw_UserPermissionAssignmentViaBusinessRole";

-- Concurrent refresh (no read lock; requires the unique index the migrations create)
REFRESH MATERIALIZED VIEW CONCURRENTLY "vw_ResourceUserPermissionAssignments";

You normally don't run these by hand: the migrations create the matviews empty (WITH NO DATA) and the web container refreshes them at the end of bootstrap, and the ingest endpoint /api/ingest/refresh-views runs REFRESH MATERIALIZED VIEW CONCURRENTLY so the crawlers refresh automatically at end-of-sync. Reach for a manual refresh only when querying the matviews directly (e.g. from psql or a contract test) after loading data.


Historical Queries

All core tables (Principals, Resources, ResourceAssignments, etc.) are tracked by the _history audit table via PostgreSQL triggers. Every insert, update, and delete is recorded as a JSONB snapshot, enabling full change history queries.

-- Current data (standard query, no change needed)
SELECT * FROM "Principals" WHERE department = 'Finance';

-- Full change history for a specific principal
SELECT "changedAt", operation, "rowData", "prevData"
FROM "_history"
WHERE "tableName" = 'Principals'
  AND "rowId" = 'principal-guid-here'
ORDER BY "changedAt" DESC;

-- All assignment changes in the last 30 days
SELECT "rowId", operation, "changedAt", "rowData"
FROM "_history"
WHERE "tableName" = 'ResourceAssignments'
  AND "changedAt" >= now() - interval '30 days'
ORDER BY "changedAt" DESC;

-- Deleted resources (no longer in the current table)
SELECT "rowId", "changedAt", "rowData"->>'displayName' AS name
FROM "_history"
WHERE "tableName" = 'Resources'
  AND operation = 'D'
ORDER BY "changedAt" DESC;

For more on audit history usage and query patterns, see Audit History.