# 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`
- `chapters`: `authenticated-safe-lookup`
- `listSummaries`, `listSummariesDetail`, `listStaff`, `resolveResponsiblePersonName`: `resource-overview`
- `getSkillsAnalytics`, `searchBySkills`, `listWithUtilization`, `getChargeabilityStats`, `getSkillMarketplace`: `controller-finance`
- `importSkillMatrix`: `self-service`
- 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/comment.ts`

- `list`, `listMentionCandidates`, `count`, `create`, `resolve`, `delete`: `entity-scoped`

Reasoning:

- comments must inherit the audience of the backing entity, not the comment row itself
- supported entity types are currently `estimate` and `resource`
- estimate comments inherit the estimate workspace audience: controller, manager, or admin
- resource comments inherit the resource detail audience: self-service for the caller's own linked resource, plus broad access for users who already have resource overview visibility
- mention autocomplete uses the same entity-scoped access check instead of reusing assignment-oriented user directory routes
- the registry keeps router policy, assistant metadata, and web comment targets on the same supported-entity definition
- future entity types must be added through an explicit registry with per-entity access checks, assistant parity, and router tests in the same slice

### `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/settings.ts`

- `getSystemSettings`, `updateSystemSettings`, connection tests, `getAiConfigured`: `admin-only`

Reasoning:

- even the boolean AI readiness check leaks whether admin-managed infrastructure is wired and available
- the route has no current web consumer outside admin operations, so narrowing it does not block normal user workflows

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

- `resolveByIdentifier`: `authenticated-safe-lookup`
- `list`, `getTree`, `getByIdentifier`, `getById`: `resource-overview`
- create, update, deactivate: `admin-only`

Reasoning:

- `resolveByIdentifier` stays narrow enough for low-risk lookup flows
- `list` and especially `getTree` expose the internal org hierarchy, parent links, sort order, and structure metadata, so they should not remain broad authenticated reads
- detailed org-unit reads also expose `_count.resources` and parent/child context that maps the staffing structure

### `packages/api/src/router/client.ts`

- `resolveByIdentifier`: `authenticated-safe-lookup`
- `list`, `getTree`, `getByIdentifier`, `getById`: `planning-read`
- create and update: `manager-write`
- delete: `admin-only`

Reasoning:

- `resolveByIdentifier` returns a deliberately narrow lookup shape for code/name resolution
- `list` already exposes `_count.children` and `_count.projects`, and `getTree` reveals the full client hierarchy used in planning and reporting flows
- detailed client reads add parent/child structure plus project counts, so they should align with the explicit planning audience instead of broad authenticated access

### `packages/api/src/router/utilization-category.ts`

- `list`, `getById`: `planning-read`
- create and update: `admin-only`

Reasoning:

- the categories feed project configuration and planning/reporting workflows instead of broad self-service screens
- `getById` includes `_count.projects`, so the detailed read should not remain a generic authenticated route

### `packages/api/src/router/management-level.ts`

- `listGroups`, `getGroupById`: `planning-read`
- create, update, delete: `admin-only`

Reasoning:

- management-level groups carry chargeability targets and resource-linked counts that feed planning and reporting workflows, so they should not stay on broad authenticated reads
- the list is consumed by resource editing, reporting filters, and admin configuration, which all fit the explicit planning audience better than generic `protectedProcedure`

### `packages/api/src/router/blueprint.ts`

- `listSummaries`, `list`, `getById`, `getByIdentifier`, `getGlobalFieldDefs`: `planning-read`
- `resolveByIdentifier`: `authenticated-safe-lookup`
- create, update, delete, global-flag writes: `admin-only`

Reasoning:

- `listSummaries` exposes `_count.projects`, so the assistant-facing summary list should not remain a broad authenticated read
- `resolveByIdentifier` already returns a narrow lookup shape suitable for low-risk name/id resolution
- the broader blueprint reads expose full template configuration such as field definitions, defaults, and validation rules that belong to planning workflows rather than generic authenticated access
- `getGlobalFieldDefs` aggregates active global field definitions across blueprints, so it belongs with the same planning configuration audience rather than a broad authenticated read

### `packages/api/src/router/holiday-calendar.ts`

- `listCalendars`, `listCalendarsDetail`, `getCalendarByIdentifier`, `getCalendarByIdentifierDetail`, `getCalendarById`: `admin-only`
- create, update, delete calendar and entry mutations: `admin-only`
- `previewResolvedHolidays`, `previewResolvedHolidaysDetail`, `resolveHolidays`, `resolveHolidaysDetail`: `authenticated-safe-lookup`
- `resolveResourceHolidays`, `resolveResourceHolidaysDetail`: `self-service` for the caller's own resource, with elevated cross-resource reads for manager and admin roles

Reasoning:

- the calendar catalog is currently consumed in the web app only by the admin vacation editor, so broad authenticated reads expose internal configuration without a product need
- region-based resolution helpers remain a narrow lookup surface because callers provide the location context directly instead of enumerating internal resource data
- resource-based holiday resolution derives sensitive location context from a specific employee record, so it must follow the same self-service ownership model as other resource-scoped absence reads

### `packages/api/src/router/entitlement.ts`

- `getBalance`, `getBalanceDetail`: `self-service` for the caller's own resource, with elevated cross-resource reads for controller, manager, and admin roles
- `get`, `set`, `getYearSummary`, `getYearSummaryDetail`: `manager-write`
- `bulkSet`: `admin-only`

Reasoning:

- regular users can inspect only their own holiday-aware balance, and the route enforces that by checking resource ownership before loading entitlement data
- cross-resource balance reads and year summaries are operational planning and approval workflows, so they stay with controller/manager/admin audiences rather than broad authenticated access
- bulk entitlement changes affect many users at once and should remain restricted to the smallest administrative audience

### `packages/api/src/router/vacation.ts`

- `previewRequest`, `list`, `getById`, `getForResource`, `getTeamOverlap`, `getTeamOverlapDetail`, `cancel`: `self-service` for the caller's own resource, with elevated cross-resource reads for manager and admin roles
- `create`: `self-service` for the caller's own resource, with elevated creation for manager and admin roles
- `approve`, `reject`, `getPendingApprovals`, `updateStatus` approval paths: `manager-write`
- `batchCreatePublicHolidays`: `admin-only`

Reasoning:

- the employee-facing vacation flows are valid self-service features, but they must not reveal holiday context, overlap data, or request details for arbitrary resources
- manager and admin roles already handle approval and operational cross-resource workflows, so they retain broader access where the route logic explicitly allows it
- bulk public-holiday generation changes organization-wide absence data and therefore belongs to the smallest administrative audience

### `packages/api/src/router/notification.ts`

- `list`, `unreadCount`, `markRead`, task detail/status routes, reminder routes, and `delete`: `self-service`
- `create`, `createBroadcast`, `listBroadcasts`, `getBroadcastById`, `createTask`, `assignTask`: `manager-write`

Reasoning:

- the self-service surface is already constrained to the caller's own notifications, reminders, tasks, or assignee visibility
- broadcast and task-assignment flows can affect other users and organization-wide messaging, so they must stay on explicit manager-or-admin procedures

### `packages/api/src/router/user.ts`

- `me`, dashboard layout/preferences, favorites, MFA setup/status: `self-service`
- `listAssignable`: `manager-write`
- `list`, `activeCount`, create/update role and permissions, resource linking, `getEffectivePermissions`, `disableTotp`: `admin-only`
- `verifyTotp`: `public` for the login flow

Reasoning:

- self-service user routes only expose or mutate the authenticated account's own preferences and MFA state
- `listAssignable` is an operational lookup for delegation and assignment flows, which fits manager and admin audiences
- user administration and effective-permission inspection expose high-sensitivity identity and authorization state and therefore should remain admin-only

### `packages/api/src/router/assistant.ts`

- `listPendingApprovals`: `self-service`
- `chat`: authenticated shell with tool-level audience enforcement

Reasoning:

- `listPendingApprovals` reads pending approvals by `ctx.dbUser.id`, so it is a self-service view of the caller's own approval queue
- `chat` requires authentication, but the effective data audience is enforced by assistant tool selection and backing router permissions rather than by a single broad router audience on the chat endpoint itself

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