# Route Access Matrix **Date:** 2026-03-30 **Purpose:** Make high-sensitivity API audiences explicit and reduce ambiguous `protectedProcedure` usage on broad read routes. ## Audience Classes - `self-service`: authenticated users can only read or mutate data that belongs to their linked resource or account - `authenticated-safe-lookup`: authenticated users can access a deliberately narrow, identity-safe lookup surface - `resource-overview`: users with `viewAllResources` or `manageResources` - `planning-read`: users with `viewPlanning` - `controller-finance`: controller, manager, or admin through `controllerProcedure` - `manager-write`: manager or admin through `managerProcedure` - `admin-only`: admin through `adminProcedure` ## Current Classification ### `packages/api/src/router/resource.ts` - `getMyResource`: `self-service` - `getById`, `getByEid`, `getHoverCard`, `getByIdentifier`, `getByIdentifierDetail`, `resolveByIdentifier`, `getChargeabilitySummary`: `self-service` unless the caller also has `resource-overview` - `directory`: `authenticated-safe-lookup` - `listSummaries`, `listSummariesDetail`, `listStaff`, `resolveResponsiblePersonName`: `resource-overview` - `getSkillsAnalytics`, `searchBySkills`, `listWithUtilization`, `getChargeabilityStats`, `getSkillMarketplace`: `controller-finance` - create, update, deactivate, batch update, imports for other users: `manager-write` or `admin-only` ### `packages/api/src/router/project.ts` - `resolveByIdentifier`, `searchSummaries`, `getByIdentifier`: `planning-read` - `searchSummariesDetail`, `list`, `getById`, `getByIdentifierDetail`, `getShoringRatio`, `listWithCosts`: `controller-finance` - create, update, status changes, cover mutations: `manager-write` - delete and batch delete: `admin-only` - `isImageGenConfigured`, `isDalleConfigured`: authenticated low-risk configuration checks ### `packages/api/src/router/timeline.ts` - `getMyEntriesView`, `getMyHolidayOverlays`: `self-service` - timeline-wide planning reads and shift previews: `controller-finance` - allocation updates, quick-assign, project shifts: `manager-write` ### `packages/api/src/router/allocation.ts` - `list`, `listView`, `listDemands`, `listAssignments`, `getAssignmentById`, `resolveAssignment`, `getDemandRequirementById`, `checkResourceAvailability`, `getResourceAvailabilityView`, `getResourceAvailabilitySummary`: `planning-read` - mutations already sit behind `manager-write` ### `packages/api/src/router/dashboard.ts` - all current routes are `controller-finance` ### `packages/api/src/router/role.ts` - `resolveByIdentifier`: `authenticated-safe-lookup` - `list`, `getByIdentifier`, `getById`: `planning-read` - create, update, delete: `manager-write` Reasoning: - `resolveByIdentifier` returns a narrow lookup shape without planning counts - `list`, `getByIdentifier`, and `getById` attach planning-linked usage counts, so they must not remain broad `protectedProcedure` reads ### `packages/api/src/router/scenario.ts` - `getProjectBaseline`: `planning-read` plus explicit `viewCosts` Reasoning: - the route combines staffing baseline data with commercial totals, so both planning and cost audiences are required ### `packages/api/src/router/estimate.ts` - `list`: `controller-finance` - drafting, versioning, export generation, and approval writes: `manager-write` ### `packages/api/src/router/system-role-config.ts` - all reads and writes: `admin-only` Reasoning: - system role defaults define the effective permission model and therefore belong to the smallest operational audience ### `packages/api/src/router/country.ts` - `list`, `resolveByIdentifier`, `getCityById`: `authenticated-safe-lookup` - `getByIdentifier`, `getById`: `resource-overview` - create, update, metro-city writes: `admin-only` Reasoning: - minimal country lookups are needed broadly for forms, filters, and location resolution - detailed country reads include metro-city detail plus `_count.resources`, so they should align with broad people-directory visibility ### `packages/api/src/router/org-unit.ts` - `list`, `getTree`, `resolveByIdentifier`: `authenticated-safe-lookup` - `getByIdentifier`, `getById`: `resource-overview` - create, update, deactivate: `admin-only` Reasoning: - minimal org-unit lookups are low-risk master data - detailed org-unit reads expose `_count.resources` and parent/child context that maps the staffing structure ## Assistant Parity Rule - assistant tool visibility must never widen the audience of the backing router - router audience is the source of truth; assistant gating mirrors it - when a route becomes narrower, update assistant visibility in the same hardening slice - `search_resources` must follow `resourceOverviewProcedure`, not broad authenticated access - `search_by_skill` must follow `controllerProcedure`, not broad authenticated or planning-only access - if `assistant-tools.ts` already has unrelated local edits, prefer updating `packages/api/src/router/assistant.ts` and parity tests first instead of mixing concerns into the tool implementation file ## Rollout Discipline For audience-scoping changes, use this order: 1. narrow the backing router procedure first 2. add or tighten authorization tests on the router 3. align assistant visibility in `packages/api/src/router/assistant.ts` 4. update assistant parity tests 5. ship in small isolated commits so regressions can be reverted without undoing unrelated hardening ## Immediate Follow-Ups - monitor whether `viewPlanning` should later split into narrower project-read vs allocation-read audiences - split `allocation` further into narrower future audiences where resource-capacity and staffing-demand reads diverge - add authorization tests for every route listed above so the matrix is CI-enforced, not just documented