Auth and scope
Clerk supplies identity. Iris checks which program, reviews, and operations that staff member may access.
Session to Actor
getServerSession reads Clerk currentUser, validates public
metadata with actorSchema from lib/auth/actor.ts, and returns
{ actor, userId }. Review records save only actorRef(actor):
name, role and staff ID. Preferences and scope fields stay in the session. Required staff
fields are role, staffId, and program. Reporting links are optional metadata. Preferences
come from unsafe metadata; they do not grant authority. Missing/invalid staff metadata
raises AccountSetupError. A signed-out user raises
AccessError with 401.
Visibility and capability
| Role | Review visibility | Additional capability |
|---|---|---|
| examiner | Own assigned reviews in the same program. | Edit facts only in Active / Needs Changes; schedule. |
| supervisor | Own and subordinate examiners’ reviews in the same program. | Approval queue, EPP export, schedule; examiner facts stay locked. |
| program_manager | Whole program. | Approval queue, imports, reports, policy publication, EPP export. |
| admin | Whole program. | Imports, reports, policy publication, apply_rules; no export. |
| statistics | Whole program. | Imports, reports, policy publication, apply_rules, FNS export. |
| pec | Whole program; PEC inbox narrows to awaiting PEC. | Approval queue and policy decisions; no fact editing. |
Visibility is canAccessReview in memory and reviewScope in SQL.
Both read examinerScope, so a list and a page cannot disagree. Role-only
permissions live in one capability table in scope.ts and are read with
can(actor, capability). Operation gates in commands.ts also
depend on review state. applyRules means updating calculation/check versions,
not publishing an amount table; publication uses report.
Trust boundary
A client-supplied Actor can pass shape validation without proving identity. The review
POST replaces it and the timestamp with server values. Use session actors for access
checks. Store load and findBusiness do not accept an Actor;
their callers must authorize returned records.
Development and tests
View as requires a Clerk test secret, a roster caller and target, and a matching Clerk
user. The API creates a 60-second sign-in ticket. Playwright’s separate bypass requires
localhost/127.0.0.1, a configured QC_E2E_SECRET, no
VERCEL variable, and a matching request cookie/header. Each Playwright run
creates a random secret in e2e/test-secret.ts. Keep that variable unset on
normal deployments. The 14-person roster is fixture data, not a live organization
directory.