Skip to main content
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: 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

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

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

Object Permissions UI Updated

The ObjectPermissionsTab component was migrated from hardcoded ORDERED_ROLES (5 roles) to dynamic useAvailableRoles() hook (all 18 system roles):

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.

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

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

5b. Fix Last V1 References

Migration: 20260319194753 — Fix 2 functions still referencing dropped table

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)

Trigger Consolidation


Application Code Changes Summary

Hooks & Services

Components

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

Dropped Tables

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)


References