Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Version: 1.0.0
Created: 2026-03-19
Status: ✅ Complete (Phase 3 cleanup deferred to Q3 2026)
Owner: Platform Foundation (PF-30)
Executive Summary
The Permissions V2 migration replaced the legacy pf_user_roles table with a granular, permission-based system built on pf_user_role_assignments, pf_role_permissions, and pf_module_permissions. Work spanned December 2025 through March 2026 across five phases:
| Phase | Period | Scope |
|---|
| 1. Initial V2 Migration | Dec 2025 | ~60 files: navigation, components, hooks, V1 code removal |
| 2. Hard Deprecation | Jan 2026 | Feature flag removed, V2 mandatory, Object Permissions UI updated |
| 3. RLS Policy Hardening | Mar 2026 | ALL→CRUD policy split, 12 helper functions rewritten, expires_at enforcement |
| 4. Trigger Consolidation | Mar 2026 | Rewired all triggers to update_updated_at_column(), dropped 22 deprecated functions |
| 5. V1 Table Drop & Final Cleanup | Mar 2026 | Dropped pf_user_roles, fixed last V1 references, dropped legacy overloads |
Current state: V2 is the sole permission system. All RLS policies use granular CRUD. All _has_org_access helpers delegate to pf_has_org_access with expires_at validation. The pf_user_roles table no longer exists.
Phase 1: Initial V2 Migration (Dec 2025)
Migration period: December 22–23, 2025
Files modified: ~60
What Changed
| Category | Count | Details |
|---|
| Navigation files | 8 | Removed minRole property, added permission |
| Module registry | 3 | Removed ROLE_HIERARCHY, getHighestRole(), hasMinimumRole() |
| Dashboard components | 6 | Switched to useHasPermission() / PermissionGate |
| Core module components | 10 | Migrated HR, FA, RH, FW, etc. |
| Platform components | 7 | Auth, settings, admin panels |
| Edge functions | 1 | Updated to verifyOrgAccess() pattern |
| Database helpers | 11 | Updated to query pf_user_role_assignments first |
| RLS test files | 8 | Dual V1/V2 seeding |
| Documentation | 6 | AI guides, module docs |
Code Removed
useUserRole.ts — hook that queried V1 table
minRole property — from all navigation items and TypeScript types
ROLE_HIERARCHY — from module-registry.ts
getHighestRole() — from module-registry.ts
hasMinimumRole() — from module-registry.ts
V2 Components Introduced
| Component | Purpose |
|---|
PermissionGate | Conditional rendering based on permission key |
RequirePermission | Page-level protection wrapper |
useHasPermission() | Hook for permission logic in components |
Permission key format: {module}.{entity}.{action} (e.g., hr.employees.edit, fa.bills.approve)
Phase 2: Hard Deprecation (Jan 2026)
Completed: January 26, 2026
What Changed
| Item | Status |
|---|
permissions_v2_enabled feature flag removed from all code paths | ✅ |
usePermissionsV2Enabled hook deleted | ✅ |
isV2Enabled() removed from PermissionService | ✅ |
| V2 mandatory for all organizations | ✅ |
pf_user_roles marked read-only (trigger) | ✅ |
Object Permissions UI Updated
The ObjectPermissionsTab component was migrated from hardcoded ORDERED_ROLES (5 roles) to dynamic useAvailableRoles() hook (all 18 system roles):
| File | Change |
|---|
src/platform/data-manager/hooks/useAvailableRoles.ts | New hook — fetches all system roles dynamically |
src/platform/data-manager/components/ObjectPermissionsTab.tsx | Uses useAvailableRoles() instead of ORDERED_ROLES |
src/platform/data-manager/components/ObjectPermissionMatrix.tsx | Uses roleLabel from props, removed ROLE_CONFIG |
src/platform/data-manager/components/FieldPermissionsSection.tsx | Uses useAvailableRoles() and getRoleLabel() |
src/platform/data-manager/types/permissions.ts | ORDERED_ROLES and ROLE_CONFIG marked deprecated |
Phase 3: RLS Policy Hardening (Mar 2026)
3a. Split ALL Policies to Granular CRUD
Migration: 20260318040458 — Split ALL policies to CRUD (PF tables)
Replaced every FOR ALL policy with individual SELECT, INSERT, UPDATE, DELETE policies across all business tables. Every UPDATE and INSERT policy includes a WITH CHECK clause. Tables affected include pf_, hr_, fa_, fw_, fm_, ce_, gr_, it_, lo_, rh_, cl_, pm_ prefixed tables.
Standard enforced:
- No
ALL policies permitted
WITH CHECK (false) on deny-all mutation policies
FORCE ROW LEVEL SECURITY on all business tables
SECURITY DEFINER functions set search_path = public (unquoted)
3b. Rewired 12 _has_org_access Helpers
Migration: 20260319041820 — Rewrite 7 core helpers to delegate to pf_has_org_access
All core-specific _has_org_access functions now delegate to pf_has_org_access, which validates expires_at on role assignments. This ensures expired assignments are rejected uniformly across all RLS policies.
| Helper | Signature Preserved | Notes |
|---|
fw_has_org_access(org_id, user_id) | ✅ | Delegates to pf_has_org_access |
fa_has_org_access(p_org_id, p_user_id) | ✅ | Delegates to pf_has_org_access |
fm_has_org_access(org_id, user_id) | ✅ | Delegates to pf_has_org_access |
hr_has_org_access(org_id, user_id) | ✅ | Delegates to pf_has_org_access |
it_has_org_access(org_id, user_id) | ✅ | Delegates to pf_has_org_access |
lo_has_org_access(org_id, user_id) | ✅ | Delegates to pf_has_org_access |
gr_has_org_access(org_id, user_id) | ✅ | Reversed arg mapping preserved |
cl_has_org_access | ✅ | Already delegating (prior migration) |
pm_has_org_access | ✅ | Already delegating (prior migration) |
ce_has_org_access | ✅ | Fixed separately |
it_dashboard_has_org_access | ✅ | Fixed separately |
pf_settings_audit_has_org_access | ✅ | Fixed separately |
3c. Additional Security Fixes
Migration: 20260319181618 — Security audit: expires_at checks + deprecated references
has_rh_admin_role — rewired to pf_user_role_assignments with expires_at filter
has_rh_admin_role — search_path fixed (unquoted public)
- RH table grants added for
authenticated role
Migration: 20260214153000 — ce_unmatched_calls service-role-only access
Migration: 20260214154500 — gr_has_org_access legacy arg order fix
Migration: 20260214162000 — fa_is_finance_admin permission key join fix
Migration: 20260214170000 — pf_can_admin_wizards permission function fix
Migration: 20260305220611 — Enable RLS on 6 previously unprotected tables
Migration: 20260306120000 — Go-live permissions fix
Phase 4: Trigger Consolidation (Mar 2026)
Migrations: 20260319184533 through 20260319184926 (4 files)
All updated_at triggers across every core were rewired to the canonical update_updated_at_column() function, then 22 redundant per-core trigger functions were dropped.
Workstreams
| Migration | Workstream | Cores |
|---|
20260319184533 | 1A: Rewire triggers | PF, FA, FM, CE |
20260319184716 | 1B: Rewire triggers | HR, IT, FW, GR, CL |
20260319184818 | 1C: Rewire triggers | PM |
20260319184926 | 2: Drop deprecated functions | All cores |
Functions Dropped (22)
Per-core set_updated_at(), update_*_timestamp(), and similar functions that duplicated the canonical update_updated_at_column().
Phase 5: V1 Table Drop & Final Cleanup (Mar 2026)
5a. Drop pf_user_roles Table
Migration: 20260319182644
| Object Dropped | Type |
|---|
pf_user_roles_readonly_trigger | Trigger |
pf_prevent_user_roles_writes() | Function |
RLS policies on pf_user_roles | Policies |
pf_user_roles | Table |
Orphaned functions referencing pf_user_roles | Functions |
5b. Fix Last V1 References
Migration: 20260319194753 — Fix 2 functions still referencing dropped table
| Function | Fix |
|---|
hr_has_analytics_access(org_id, user_id) | Rewired to delegate to pf_has_org_access |
pf_get_slow_queries(p_threshold, p_limit) | Rewired to query pf_user_role_assignments |
5c. Drop Legacy Overload
Migration: 20260319194921
Dropped pf_get_slow_queries(numeric, integer) — the old overload that still referenced pf_user_roles.
Migration File Index
Core V2 Migrations (Permissions & Security)
| Timestamp | Description |
|---|
20260214153000 | Fix ce_unmatched_calls service-role-only access |
20260214154500 | Fix gr_has_org_access legacy argument order |
20260214162000 | Fix fa_is_finance_admin permission key join |
20260214170000 | Fix pf_can_admin_wizards permission function |
20260305220611 | Enable RLS on 6 previously unprotected tables |
20260306120000 | Go-live permissions fix |
20260318040458 | Split ALL policies to granular CRUD (PF tables) |
20260318192816 | Seed all missing platform_admin permissions |
20260319041820 | Rewrite 7 _has_org_access helpers to delegate to pf_has_org_access |
20260319125214 | Seed missing permission keys (rh.schedule-templates.view, etc.) |
20260319125324 | Security hardening — fw_get_sla_status org_id enforcement |
20260319161137 | Fix has_rh_admin_role to query pf_user_role_assignments |
20260319162949 | Fix has_rh_admin_role search_path (unquoted public) |
20260319164220 | Grant authenticated role access to all rh_* tables |
20260319164431 | Fix rh_residences INSERT policy |
20260319181618 | Security audit: expires_at checks + deprecated references |
20260319182644 | Drop pf_user_roles table and related objects |
20260319194753 | Fix hr_has_analytics_access and pf_get_slow_queries |
20260319194921 | Drop legacy pf_get_slow_queries(numeric, integer) overload |
Trigger Consolidation
| Timestamp | Description |
|---|
20260319184533 | Workstream 1A: Rewire PF, FA, FM, CE triggers |
20260319184716 | Workstream 1B: Rewire HR, IT, FW, GR, CL triggers |
20260319184818 | Workstream 1C: Rewire PM triggers |
20260319184926 | Workstream 2: Drop 22 deprecated trigger functions |
Application Code Changes Summary
Hooks & Services
| Item | Action | Phase |
|---|
useUserRole.ts | Deleted | 1 |
usePermissionsV2Enabled.ts | Deleted | 2 |
useHasPermission() | Active (V2) | 1 |
useAvailableRoles() | Created | 2 |
PermissionService.isV2Enabled() | Removed | 2 |
Components
| Component | Change | Phase |
|---|
PermissionGate | Introduced | 1 |
RequirePermission | Introduced | 1 |
ObjectPermissionsTab | Migrated to dynamic roles | 2 |
ObjectPermissionMatrix | Removed ROLE_CONFIG dependency | 2 |
FieldPermissionsSection | Migrated to useAvailableRoles() | 2 |
| Navigation items (~8 files) | minRole → permission | 1 |
| Dashboard components (~6) | hasMinimumRole() → useHasPermission() | 1 |
Edge Functions
All edge functions use verifyOrgAccess() / verifyOrgRole() from supabase/functions/_shared/auth.ts. No inline pf_user_role_assignments queries permitted.
Database Schema (Current State)
Active Tables
| Table | Purpose |
|---|
pf_user_role_assignments | User-to-role assignments with org/site scope and expires_at |
pf_roles | Role definitions (system + custom) |
pf_role_permissions | Permission-to-role mappings |
pf_module_permissions | Permission definitions ({module}.{entity}.{action}) |
pf_custom_roles | Organization-defined custom roles |
Dropped Tables
| Table | Dropped In | Replacement |
|---|
pf_user_roles | 20260319182644 | pf_user_role_assignments |
Helper Functions (Current)
All _has_org_access helpers delegate to pf_has_org_access, which enforces:
- User has an active role assignment for the organization
expires_at IS NULL OR expires_at > now()
Remaining Work (Phase 3 — Q3 2026)
| Item | Status | Notes |
|---|
Archive pf_user_roles data verification | ⏳ | Data preserved in migration history |
Remove minRole property from TypeScript types | ⏳ | Still exists in some type definitions |
Remove deprecated ORDERED_ROLES / ROLE_CONFIG constants | ⏳ | Sunset date: 2026-03-31 |
| V1 fallback logic in PermissionService | ⏳ | Dead code after Phase 2 |
permissions_v2_enabled column on pf_organizations | ⏳ | No longer checked |
References
| Document | Path |
|---|
| Deprecation Plan | docs/architecture/deprecation/pf_user_roles.md |
| V2 Migration Completion Report (Archived) | docs/archive/permissions/permissions-v2-migration-complete.md |
| Object Permissions V2 Migration | docs/platform/data-manager/OBJECT_PERMISSIONS_V2_MIGRATION.md |
| Permissions Quick Reference | docs/pf/permissions-quick-reference.md |
| Permission Patterns (Cursor Rule) | .cursor/rules/permission-patterns.md |
| PF-30 Spec | specs/pf/specs/PF-30-permissions-system-v2.md |
| Deprecation Notice (Specs) | specs/_DEPRECATION_NOTICE.md |
| RLS & Isolation Standards | Memory: architecture/database/rls-and-isolation-standards |