> ## 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.

# Permissions System Guide

> Version: 1.0.0 Last Updated: 2025-01-25 Status: Active

**Version:** 1.0.0\
**Last Updated:** 2025-01-25\
**Status:** Active

> **Quick Reference:** For quick lookup, see [permissions-quick-reference.md](permissions-quick-reference.md)\
> **Specification:** See [PF-30 Specification](../../specs/pf/specs/PF-30-permissions-system-v2.md)

***

## Table of Contents

1. [System Overview](#1-system-overview)
2. [Design Decisions](#2-design-decisions)
3. [Migration History (V1 → V2)](#3-migration-history-v1--v2)
4. [Review & Recommendations](#4-review--recommendations)
5. [Implementation Guidelines](#5-implementation-guidelines)
6. [References](#6-references)

***

## 1. System Overview

The Encore Health OS Platform uses a **granular permissions system (V2)** that replaced the legacy role-based system (V1). V2 provides flexible, module-specific permissions with support for custom roles, site-scoped assignments, and time-limited access.

### Current Status

**V2 (Active - Mandatory):** Granular permissions system

* ✅ Flexible: `{module}.{entity}.{action}` format (e.g., `hr.employees.view`)
* ✅ Custom roles per organization
* ✅ Site-scoped and time-limited assignments
* ✅ 165+ granular permissions
* ✅ **Mandatory**: All organizations use V2 exclusively (no feature flag)

**V1 (Deprecated):** Legacy `pf_user_roles` table

* ❌ **Deprecated**: No longer in use
* ❌ Table marked read-only (retained for audit/rollback purposes)
* ❌ All code migrated to V2

### Permission Key Format

```
{module}.{entity}.{action}
```

**Examples:**

* `hr.employees.view` - View HR employees
* `fa.bills.approve` - Approve FA bills
* `rh.residents.create` - Create RH residents
* `system.platform.admin` - Platform admin access

### Core Components

| Component            | Purpose                                   |
| -------------------- | ----------------------------------------- |
| `PermissionGate`     | Conditional rendering based on permission |
| `RequirePermission`  | Page-level protection                     |
| `useHasPermission()` | Hook for permission logic                 |

### Database Tables

| Table                      | Purpose                                      |
| -------------------------- | -------------------------------------------- |
| `pf_user_role_assignments` | User-to-role assignments with org/site scope |
| `pf_roles`                 | Role definitions (custom + system)           |
| `pf_role_permissions`      | Permission-to-role mappings                  |
| `pf_permissions`           | Permission definitions                       |
| `pf_user_roles`            | **DEPRECATED** - V1 table, read-only         |

***

## 2. Design Decisions

This section documents key architectural decisions for the PF-30 Permissions System V2.

### Decision 1: Site-Scope Interaction with Custom Roles

**Chosen Approach:** Custom roles are always org-scoped, site-scope is applied at assignment.

**Rationale:**

1. **Simplicity:** Custom roles define WHAT permissions a user has. Role assignments define WHERE those permissions apply.
2. **Flexibility:** Same custom role can be used across different sites without duplication.
3. **Maintainability:** Changing a role's permissions updates all assignments automatically.
4. **Precedent:** Mirrors how system roles already work with site assignments.

**Implementation:**

* Custom roles table (`pf_custom_roles`) does NOT have `site_id` column
* Role assignments table (`pf_user_role_assignments`) has optional `site_ids UUID[]` column
* Permission checks must consider site scope from assignment, not from role definition

### Decision 2: V2 is Mandatory (No Feature Flag)

**Chosen Approach:** V2 permissions system is mandatory for all organizations.

**Rationale:**

1. Migration complete: All organizations have been migrated to V2
2. Simplified codebase: No conditional logic based on feature flags
3. Consistent behavior: All users experience the same permission system
4. Reduced maintenance: Single code path to maintain

**Note:** The `permissions_v2_enabled` column still exists in the database for historical purposes but is no longer checked in code. All organizations are assumed to have V2 enabled.

**Note:** Feature flag has been removed post-migration. V2 is now mandatory.

### Decision 3: Base Role Inheritance Constraint

**Decision:** Add CHECK constraint to prevent inheriting from `platform_admin` or `org_admin`.

**Rationale:**

1. **Security:** Prevents accidental privilege escalation
2. **Clear hierarchy:** Custom roles should not exceed org\_admin permissions
3. **Explicit:** Database-level enforcement, not just application logic

**Allowed Base Roles:**

* `finance_admin`
* `site_admin`
* `finance_staff`
* `staff`
* `readonly`

**Disallowed Base Roles:**

* `platform_admin` (system-level only)
* `org_admin` (already has full org access)

### Decision 4: Foreign Key References

**Decision:** Use `pf_profiles(id)` for all user references.

**Rationale:**

1. **Consistency:** All other PF tables reference `pf_profiles`
2. **Query simplicity:** Can join directly without crossing schema boundaries
3. **RLS compatibility:** `pf_profiles` has RLS policies, `auth.users` doesn't
4. **Extensibility:** Additional user metadata available without joining

### Decision 5: Permission Caching Strategy

**Decision:** Use React Query with 5-minute staleTime, database indexes for query optimization.

**Rationale:**

1. **Consistency with platform:** Constitution §6.5 specifies 5-min staleTime, 10-min gcTime
2. **Balance:** Fresh enough for security, cached enough for performance
3. **Invalidation:** Role/permission changes invalidate queries immediately

***

## 3. Migration History (V1 → V2)

### Migration Overview

**Migration Period:** December 22-23, 2025\
**Status:** ✅ COMPLETE\
**Total Files Modified:** \~60

### Migration Phases

#### Phase 1: Foundation ✅

* Enabled V2 for all organizations
* Updated navigation system (removed `minRole`)
* Updated core hooks and utilities
* Updated module registry

#### Phase 2: Components ✅

* Migrated dashboard components
* Migrated navigation components
* Migrated core module components
* Migrated data manager components

#### Phase 3: RLS & Database ✅

* Updated all V2 helper functions to use `pf_user_role_assignments`
* Updated component user lookups
* Updated test infrastructure with dual V1/V2 seeding

#### Phase 4: Testing & Cleanup ✅

* Removed deprecated V1 code (`useUserRole.ts`, `minRole`, `hasMinimumRole()`, `getHighestRole()`)
* Updated documentation (AI guides, module docs)
* Marked `pf_user_roles` as read-only

### Key Changes

**Code Removed:**

* `useUserRole.ts` - Deleted - hook that queried V1 table
* `minRole` property - Removed from all navigation items and TypeScript types
* `ROLE_HIERARCHY` - Removed from module-registry.ts
* `getHighestRole()` - Removed from module-registry.ts
* `hasMinimumRole()` - Removed from module-registry.ts

**Database Changes:**

* Helper functions query `pf_user_role_assignments` first
* `pf_user_roles` has deprecation comment
* `pf_user_roles` has read-only trigger
* V2 enabled for all organizations

### Migration Patterns

#### Pattern 1: Navigation Items

```typescript theme={null}
// ❌ Before (V1)
{ label: 'Employees', path: '/hr/employees', minRole: 'staff' }

// ✅ After (V2)
{ label: 'Employees', path: '/hr/employees', permission: 'hr.employees.view' }
```

#### Pattern 2: Component Permission Checks

```typescript theme={null}
// ❌ Before (V1)
import { useUserRole } from '@/platform/modules/useUserRole';
const { role } = useUserRole();
if (role === 'org_admin') { ... }

// ✅ After (V2)
import { useHasPermission } from '@/platform/permissions';
const canAdmin = useHasPermission('system.platform.admin');
if (canAdmin) { ... }
```

#### Pattern 3: RLS Policies

```sql theme={null}
-- ❌ Before (V1)
CREATE POLICY "org_admins_can_edit" ON some_table
FOR UPDATE
USING (has_role(auth.uid(), 'org_admin'));

-- ✅ After (V2)
CREATE POLICY "org_admins_can_edit" ON some_table
FOR UPDATE
USING (pf_has_permission(auth.uid(), organization_id, 'module.entity.edit'));
```

### Role to Permission Mapping

| V1 Role          | V2 Permission Pattern                                  |
| ---------------- | ------------------------------------------------------ |
| `platform_admin` | `system.platform.admin`                                |
| `org_admin`      | `system.platform.admin` (or module-specific `*.admin`) |
| `site_admin`     | `*.view` + `*.create` + `*.edit` (no admin)            |
| `staff`          | `*.view` + `*.create`                                  |
| `readonly`       | `*.view` only                                          |
| `finance_admin`  | `fa.admin` + all FA permissions                        |
| `finance_staff`  | `fa.*.view` + `fa.*.create` (no approve/admin)         |

***

## 4. Review & Recommendations

### System Architecture Review

The permissions system overhaul is **architecturally sound** and aligns with constitution requirements, integrates with existing systems (PF-26 object permissions), and ensures proper security, testing, and documentation.

**Key Findings:**

1. ✅ Multi-tenant design correctly includes `organization_id` in all tables
2. ✅ Uses `pf_` prefix for Platform Foundation tables
3. ✅ Includes audit columns (`created_at`, `updated_at`, `created_by`, `updated_by`)
4. ✅ Permissions system is Platform Foundation (PF), not core-specific

### Database Schema Compliance

**Required Schema Elements:**

* ✅ `custom_fields JSONB` on business entity tables
* ✅ `created_by` and `updated_by` audit columns
* ✅ `site_id` where applicable (for site-scoped permissions)
* ✅ Descriptive comments on columns
* ✅ RLS enabled on all tables with SECURITY DEFINER functions

### Integration with Existing Systems

**PF-26 Object Permissions Integration:**

The system integrates with existing `pf_object_permissions` and `pf_field_permissions` (PF-26):

* **Module Permissions (New):** Control access to features/modules (e.g., "Can access HR module?")
* **Object Permissions (PF-26):** Control access to specific entities (e.g., "Can view Employee records?")
* **Field Permissions (PF-26):** Control field-level access (e.g., "Can edit salary field?")

**Permission Check Hierarchy:**

1. Module permission (e.g., `hr.employees.view`)
2. Object permission (e.g., `hr_employees.can_view` for role)
3. Field permission (e.g., `hr_employees.salary.permission_level`)

### RLS Policy Requirements

**CRITICAL:** All tables MUST have RLS enabled with SECURITY DEFINER functions to avoid recursion.

**Required RLS Policies:**

* Users can view org custom roles
* Org admins can manage custom roles
* All authenticated users can view permissions (system-wide, read-only)
* Users can view role permissions for their org
* Org admins can manage role permissions
* Users can view their own role assignments
* Org admins can manage role assignments

**Anti-Pattern (CRITICAL):**

```sql theme={null}
-- ❌ NEVER DO THIS (Causes infinite recursion)
CREATE POLICY "document_org_access" ON pf_documents
FOR SELECT USING (
 organization_id IN (
 SELECT organization_id FROM pf_user_role_assignments 
 WHERE user_id = auth.uid() -- This table also has RLS!
 )
);

-- ✅ USE SECURITY DEFINER FUNCTIONS WITH V2 PERMISSIONS
-- Option 1: Use built-in permission function (recommended)
CREATE POLICY "document_org_access" ON pf_documents
FOR SELECT USING (
  pf_has_permission(auth.uid(), organization_id, 'pf.documents.view')
);

-- Option 2: Custom SECURITY DEFINER function (if needed)
CREATE OR REPLACE FUNCTION has_org_access(org_id uuid, user_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
 RETURN EXISTS (
 SELECT 1 FROM pf_user_role_assignments
 WHERE pf_user_role_assignments.organization_id = org_id
 AND pf_user_role_assignments.user_id = user_id
 AND (pf_user_role_assignments.expires_at IS NULL 
      OR pf_user_role_assignments.expires_at > NOW())
 );
END;
$$;
```

### Testing Requirements

**Per Constitution §3.3:**

| Feature Type       | Unit | RLS        | Integration | E2E        |
| ------------------ | ---- | ---------- | ----------- | ---------- |
| Database schema    | -    | ✅ Required | -           | -          |
| Permission checks  | 80%  | ✅ Required | ✅ Required  | -          |
| Role management UI | 80%  | ✅ Required | ✅ Required  | -          |
| Critical flows     | 80%  | ✅ Required | ✅ Required  | ✅ Required |

**Critical Flows Requiring E2E:**

* User role assignment workflow
* Custom role creation and permission assignment
* Permission inheritance from system roles
* Cross-organization permission isolation

**RLS Test Requirements:**

* ✅ No cross-organization data leakage
* ✅ Users can only view/manage roles in their organizations
* ✅ Permission checks respect tenant boundaries
* ✅ System roles have appropriate access

### Security Considerations

**Critical Security Requirements:**

1. **Tenant Isolation:** ✅ RLS policies MUST prevent cross-org access
2. **Permission Escalation:** ✅ Users cannot grant themselves permissions
3. **Role Hijacking:** ✅ Custom roles cannot inherit from higher system roles
4. **Audit Trail:** ✅ All role/permission changes logged to `pf_audit_logs`

**Security Testing:**

* ✅ User cannot access another org's custom roles
* ✅ User cannot assign themselves higher permissions
* ✅ Permission checks respect site scope
* ✅ Expired role assignments are not honored

***

## 5. Implementation Guidelines

### Using Permissions in Code

**ALWAYS use permission service, not direct queries:**

```typescript theme={null}
// ✅ CORRECT
import { useHasPermission, PermissionGate } from '@/platform/permissions';

const canViewEmployees = useHasPermission('hr.employees.view');

<PermissionGate permission="hr.employees.edit">
  <EditButton />
</PermissionGate>

// ❌ WRONG - Direct database queries
const { data } = await supabase
  .from('pf_role_permissions')
  .select('*')
  .eq('permission_key', 'hr.employees.view');
```

### Creating Custom Roles

**Pattern:**

1. Create role in `pf_custom_roles`
2. Assign permissions via `pf_role_permissions`
3. Assign role to users via `pf_user_role_assignments`

### Navigation Filtering

```typescript theme={null}
{
 label: 'Employees',
 path: '/hr/employees',
 permission: 'hr.employees.view', // Permission-based access control
}
```

### Anti-Patterns

* ❌ Hardcode role checks (`user.role === 'admin'`)
* ❌ Create custom roles inheriting from `org_admin` or `platform_admin`
* ❌ Query permission tables directly in RLS policies
* ❌ Use V1 patterns (`minRole`, `useUserRole()`, `hasMinimumRole()`)

***

## 6. References

### Documentation

* [Quick Reference](permissions-quick-reference.md) - Quick lookup tables and patterns
* [PF-30 Specification](../../specs/pf/plans/PF-30-permissions-system-v2-PLAN.md) - Complete system specification
* [Module Permissions Matrix](module-permissions-matrix.md) - All available permissions

### Implementation

* [Permissions Component README](../architecture/integrations/index.md) - Component documentation
* [AI Guide Permissions Patterns](../../AI_GUIDE.md#permissions-system-patterns) - AI agent patterns
* [AGENTS.md Permissions Patterns](../../AGENTS.md#permissions-system-patterns-pf-30) - Quick reference patterns

### Historical (Archived)

* Archived permissions documentation available in `docs/archive/permissions/` for historical reference

***

**Last Updated:** 2025-01-25\
**Status:** Active Documentation
