Pathfinder Docs

Documentation Preview

Access Control Reference

Source: `docs/security/ACCESS_CONTROL.md`View on GitHub

Access Control Reference

Last Updated: September 20, 2026

Last verified against code: 2026-09-20 (role model refactor)

Overview

Pathfinder authorizes on two axes:

  1. Base roleprofiles.role. Authority, and therefore tenant scope: admin/staff are the Range Lab network; the four organization roles are partner-org users.
  2. Organizationprofiles.org_id. Required for organization roles. Optional for network roles, and when set it must be the single organizations.is_network row (Range Lab) — their home org, which keys their resources-module roles and the records they create.

The pairing is bound by the database constraint chk_profiles_role_org_scope (migration 20260920120002) and the guard_profile_authority_columns trigger (20260920120003), and interpreted only through lib/access-control.ts. Anything that is not base authority lives elsewhere: resources tiers in user_module_roles, Corps personas in volunteer_profiles.corps_role, demo tenancy in organizations.is_demo. Full rationale: ROLE_MODEL_REFACTOR.md.

Roles

RoleTenantUI labelWhat it may do
adminNetworkRange Lab AdminEverything. Manages every organization and user.
staffNetworkRange Lab StaffReads across organizations; writes the shared catalogs (deals, income, education, grants, tags, form templates). No user administration.
org_adminOrganizationOrganization AdminOrganization settings; creates/edits supervisor, caseworker, viewer users in their own org.
supervisorOrganizationSupervisorEverything a caseworker can, plus household archive/reopen and inventory configuration (categories, items, locations, adjustments).
caseworkerOrganizationCaseworkerClients, intake, referrals, tasks, inventory movements, resource providers.
viewerOrganizationViewerRead-only.
volunteerPortalVolunteer (Corps)Corps portal only. No CRM access. Created by Corps signup.

Retired: demo (→ organizations.is_demo + org_admin), landlord (Pathfinder's old portal; rows deactivated). RST shares the Supabase project but does not read profiles — its identity lives in portal.landlord_members and public.rst_staff.

Central helpers

Use these instead of role arrays. All take a profile-shaped object ({ role, org_id, is_active? }).

HelperTrue for
getUserScope(p){ tenantScope, orgId, baseRole }, or null for non-CRM / invalid rows
canAccessCrm(p)any active CRM base role — middleware and login gate
isNetworkScope(p) / isCrossOrgReader(p)admin, staff — skip the org filter
canManageNetworkCatalog(p)admin, staff
isTenantAdmin(role)admin, org_admin
canManageOrgUsers(p)admin, org_admin
canManageOrgRecords(p)admin, org_admin, supervisor
canWriteOrgData(p)everyone except viewer
canAccessOrgRecord(p, recordOrgId)network scope, or same organization
assignableRolesFor(actor)roles the actor may assign
isValidRoleOrgPair(role, orgId)mirrors the DB constraint

Org isolation in queries must key on isCrossOrgReader(profile), never on a role-name list:

if (!isCrossOrgReader(profile)) query = query.eq('org_id', profile.org_id)

Route Access Matrix

RouteGate
/crm, /clients, /resources, /referrals, /jobs, /events, /resource-navigator, /chatbotcanAccessCrm (middleware) + module check for organization users
/intake/* (except /intake/self-service)canAccessCrm (middleware) + canWriteOrgData (page)
/admin/*admin; org_admin for /admin/settings and /admin/users only
/intake/self-servicePublic
/corps/*Corps login; corps_role

Protection Layers

  1. Middleware (middleware.ts) — session refresh; /admin/* gate; CRM list gate via canAccessCrm; module gate for organization users (isNetworkScope bypasses).
  2. Page / APIlib/access-control.ts helpers.
  3. Database
    • RLS keyed on current_user_role() and current_user_org_id(). IN ('admin','staff') means network because the trigger guarantees staff can never point at a partner org and org admins cannot mint it.
    • chk_profiles_role_org_scope — role/org shape.
    • guard_profile_authority_columns trigger (migration 20260920120003) — network roles may only be homed at the is_network org; only admins, org admins (own org, manageable roles) and the service role may write profiles.role, org_id, is_active.
    • guard_organization_is_network trigger — is_network is admin-only.

User management

  • POST /api/admin/create-user validates (target_role, target_org_id) with canActorAssignRole() before creating the auth user.
  • lib/admin-user-permissions.tscanActorManageTarget() / canActorAssignRole(); ORG_ADMIN_MANAGEABLE_ROLES = ['supervisor','caseworker','viewer'].
  • The provisioning control plane (/api/internal/provisioning/*) may only assign organization roles; a newly provisioned organization's primary admin is org_admin.

Module-Based Access Control

In addition to roles, the CRM uses module-based access by organization tier (Basic, Standard, Premium, Enterprise). See Organization Modules. Network users bypass module checks.

Migrations

FilePurpose
20260920120000_role_model_00_audit.sqlRead-only report + audit gate
20260920120001_role_model_01_remap.sqlorganizations.is_network (Range Lab) + is_demo; staff in a non-network org → caseworker; demoorg_admin in a demo org; landlord deactivated; remap log
20260920120002_role_model_02_constraint.sqlchk_profiles_role_org_scope
20260920120003_role_model_03_profile_write_guard.sqlnetwork-org invariant + authority-column trigger (portal self-signup allowed); is_network admin-only trigger

There is deliberately no landlord table migration: public.properties (read by RST) has an FK to landlords, so retiring landlords / landlord_users / profiles.landlord_id is a coordinated shared-schema change under RST's docs/SHARED_SCHEMA_COORDINATION.md (rule §6.10).


Use links in each imported doc to open its source.