Skip to main content

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:
PhasePeriodScope
1. Initial V2 MigrationDec 2025~60 files: navigation, components, hooks, V1 code removal
2. Hard DeprecationJan 2026Feature flag removed, V2 mandatory, Object Permissions UI updated
3. RLS Policy HardeningMar 2026ALL→CRUD policy split, 12 helper functions rewritten, expires_at enforcement
4. Trigger ConsolidationMar 2026Rewired all triggers to update_updated_at_column(), dropped 22 deprecated functions
5. V1 Table Drop & Final CleanupMar 2026Dropped 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

CategoryCountDetails
Navigation files8Removed minRole property, added permission
Module registry3Removed ROLE_HIERARCHY, getHighestRole(), hasMinimumRole()
Dashboard components6Switched to useHasPermission() / PermissionGate
Core module components10Migrated HR, FA, RH, FW, etc.
Platform components7Auth, settings, admin panels
Edge functions1Updated to verifyOrgAccess() pattern
Database helpers11Updated to query pf_user_role_assignments first
RLS test files8Dual V1/V2 seeding
Documentation6AI 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

ComponentPurpose
PermissionGateConditional rendering based on permission key
RequirePermissionPage-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

ItemStatus
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):
FileChange
src/platform/data-manager/hooks/useAvailableRoles.tsNew hook — fetches all system roles dynamically
src/platform/data-manager/components/ObjectPermissionsTab.tsxUses useAvailableRoles() instead of ORDERED_ROLES
src/platform/data-manager/components/ObjectPermissionMatrix.tsxUses roleLabel from props, removed ROLE_CONFIG
src/platform/data-manager/components/FieldPermissionsSection.tsxUses useAvailableRoles() and getRoleLabel()
src/platform/data-manager/types/permissions.tsORDERED_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.
HelperSignature PreservedNotes
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_accessAlready delegating (prior migration)
pm_has_org_accessAlready delegating (prior migration)
ce_has_org_accessFixed separately
it_dashboard_has_org_accessFixed separately
pf_settings_audit_has_org_accessFixed 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_rolesearch_path fixed (unquoted public)
  • RH table grants added for authenticated role
Migration: 20260214153000ce_unmatched_calls service-role-only access
Migration: 20260214154500gr_has_org_access legacy arg order fix
Migration: 20260214162000fa_is_finance_admin permission key join fix
Migration: 20260214170000pf_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

MigrationWorkstreamCores
202603191845331A: Rewire triggersPF, FA, FM, CE
202603191847161B: Rewire triggersHR, IT, FW, GR, CL
202603191848181C: Rewire triggersPM
202603191849262: Drop deprecated functionsAll 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 DroppedType
pf_user_roles_readonly_triggerTrigger
pf_prevent_user_roles_writes()Function
RLS policies on pf_user_rolesPolicies
pf_user_rolesTable
Orphaned functions referencing pf_user_rolesFunctions

5b. Fix Last V1 References

Migration: 20260319194753 — Fix 2 functions still referencing dropped table
FunctionFix
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)

TimestampDescription
20260214153000Fix ce_unmatched_calls service-role-only access
20260214154500Fix gr_has_org_access legacy argument order
20260214162000Fix fa_is_finance_admin permission key join
20260214170000Fix pf_can_admin_wizards permission function
20260305220611Enable RLS on 6 previously unprotected tables
20260306120000Go-live permissions fix
20260318040458Split ALL policies to granular CRUD (PF tables)
20260318192816Seed all missing platform_admin permissions
20260319041820Rewrite 7 _has_org_access helpers to delegate to pf_has_org_access
20260319125214Seed missing permission keys (rh.schedule-templates.view, etc.)
20260319125324Security hardening — fw_get_sla_status org_id enforcement
20260319161137Fix has_rh_admin_role to query pf_user_role_assignments
20260319162949Fix has_rh_admin_role search_path (unquoted public)
20260319164220Grant authenticated role access to all rh_* tables
20260319164431Fix rh_residences INSERT policy
20260319181618Security audit: expires_at checks + deprecated references
20260319182644Drop pf_user_roles table and related objects
20260319194753Fix hr_has_analytics_access and pf_get_slow_queries
20260319194921Drop legacy pf_get_slow_queries(numeric, integer) overload

Trigger Consolidation

TimestampDescription
20260319184533Workstream 1A: Rewire PF, FA, FM, CE triggers
20260319184716Workstream 1B: Rewire HR, IT, FW, GR, CL triggers
20260319184818Workstream 1C: Rewire PM triggers
20260319184926Workstream 2: Drop 22 deprecated trigger functions

Application Code Changes Summary

Hooks & Services

ItemActionPhase
useUserRole.tsDeleted1
usePermissionsV2Enabled.tsDeleted2
useHasPermission()Active (V2)1
useAvailableRoles()Created2
PermissionService.isV2Enabled()Removed2

Components

ComponentChangePhase
PermissionGateIntroduced1
RequirePermissionIntroduced1
ObjectPermissionsTabMigrated to dynamic roles2
ObjectPermissionMatrixRemoved ROLE_CONFIG dependency2
FieldPermissionsSectionMigrated to useAvailableRoles()2
Navigation items (~8 files)minRolepermission1
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

TablePurpose
pf_user_role_assignmentsUser-to-role assignments with org/site scope and expires_at
pf_rolesRole definitions (system + custom)
pf_role_permissionsPermission-to-role mappings
pf_module_permissionsPermission definitions ({module}.{entity}.{action})
pf_custom_rolesOrganization-defined custom roles

Dropped Tables

TableDropped InReplacement
pf_user_roles20260319182644pf_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)

ItemStatusNotes
Archive pf_user_roles data verificationData preserved in migration history
Remove minRole property from TypeScript typesStill exists in some type definitions
Remove deprecated ORDERED_ROLES / ROLE_CONFIG constantsSunset date: 2026-03-31
V1 fallback logic in PermissionServiceDead code after Phase 2
permissions_v2_enabled column on pf_organizationsNo longer checked

References

DocumentPath
Deprecation Plandocs/architecture/deprecation/pf_user_roles.md
V2 Migration Completion Report (Archived)docs/archive/permissions/permissions-v2-migration-complete.md
Object Permissions V2 Migrationdocs/platform/data-manager/OBJECT_PERMISSIONS_V2_MIGRATION.md
Permissions Quick Referencedocs/pf/permissions-quick-reference.md
Permission Patterns (Cursor Rule).cursor/rules/permission-patterns.md
PF-30 Specspecs/pf/specs/PF-30-permissions-system-v2.md
Deprecation Notice (Specs)specs/_DEPRECATION_NOTICE.md
RLS & Isolation StandardsMemory: architecture/database/rls-and-isolation-standards