Evalium — Roles, Access Control, & Identity Specification (Execution Ledger Edition v3.1)
1. Purpose
This document defines Evalium’s:
- Authentication model (identity + step-up)
- Authorization model (roles/capabilities)
- Multi-tenant + multi-org (silo) enforcement via PostgreSQL RLS
- Access patterns for Admin Console, Operator Execution, and Client Portal (Glass Box)
- External sharing via magic links (reporting and ledger visibility)
- Future-proof custom roles
- SMB-friendly UX rules (simple by default; defensibility underneath)
This is the authoritative spec for backend permission enforcement and UI access behaviour.
For engineering invariants (WORM ledger, amendments/voids, contextual binding, verification levels), see:
docs/architecture/FOUNDATION.mdFor idempotency scope and retry semantics, see:
docs/architecture/IDEMPOTENCY-AND-RETRY-POLICY.mdFor system structure (Engagement/Project wrapper, Client Portal, hashing/ratification components), see:
docs/architecture/architecture
2. Core concepts (Operational Defensibility Context)
Evalium is an Execution Ledger:
- authoring defines what should be done (templates, rubrics, gates) via immutable versioning.
- Execution records what happened (append-only ledger events and immutable ledger entries).
- Reporting / Portals are projections over immutable history (with controlled disclosure).
UX principle:
- Users expect “Edit / Save”
- Backend performs “Amend” (append-only) and can show history when needed
3. Isolation Model (Tenant + Org Unit Silos)
Evalium enforces strict data separation using:
- Tenants: customer-level isolation
- Org Units: silos inside a tenant (e.g., UK, DE, Safety Team, Region A)
- Roles & Capabilities: feature control
- TxManager + PostgreSQL RLS: final enforcement per request
Org hierarchy note: org units may be arranged in a user-defined hierarchy (future), but Evalium enforcement remains tenant + org unit only.
3.1 RLS GUCs
TxManager sets these per transaction:
SET LOCAL app.tenant_id = '<uuid>';
SET LOCAL app.org_unit_id = '<uuid>';
SET LOCAL app.org_scope_all = 'true' | 'false';
3.2 Combined RLS Policy Pattern
Every siloed table must use a combined tenant + org policy:
USING (
tenant_id = current_setting('app.tenant_id')::uuid
AND (
current_setting('app.org_scope_all') = 'true'
OR org_unit_id = current_setting('app.org_unit_id')::uuid
)
)
This guarantees:
- Tenant isolation
- Org-unit siloing
- Optional global visibility only for privileged users
3.3 Connection Safety
Evalium uses SET LOCAL only (never SET SESSION) to prevent scope leakage across pooled connections.
3.4 SMB UI Rule
If a tenant has exactly one org unit:
- Hide org selectors
- Hide org context in navigation
- Default to a single-organisation UX
4. Authentication (Identity)
Evalium optimizes for SMB adoption (low friction) while supporting non-repudiation-ready workflows for high-stakes execution.
4.1 JWT Claims (Lean + Stable)
JWT stores:
user_id
tenant_id
org_unit_id
role_ids[]
org_scope_all: true|false (optional, privileged)
Rules:
- Capabilities are not stored in the JWT
- Capabilities are resolved server-side per request from role definitions (cacheable)
Dev/Test Tokens
In dev/test, helpers mint JWTs based on stored user_roles bindings (roles must exist + be assigned) so tests reflect real RBAC state.
4.2 Identity Modes (MVP → Enterprise)
A. Passwordless Magic Link Login (Recommended MVP)
- No passwords stored
- Low friction for SMB users
- Secure, expiring, single-use tokens
B. Email + Password (Phase 2+)
- Hashing (Argon2id recommended)
- Reset flows
- MFA optional
C. SSO/OIDC (Enterprise)
- Optional, later
4.3 Identity Tiers (Access Strength Model)
Evalium distinguishes identity strength for different actions:
- Tier 0 — Anonymous: no access beyond
/healthand auth endpoints - Tier 1 — Link Principal (Scoped Viewer): access via scoped magic link (read-only or narrowly permitted actions), revocable, expiring
- Tier 2 — Authenticated User: normal JWT user session (operators/admins)
- Tier 3 — Step-Up Verified: stronger proof required for high-stakes actions (e.g., WebAuthn/MFA)
Important framing: Tier 1 is not “a user”; it is a Link Principal with explicit scope + permissions + expiry.
4.4 Step-Up Authentication (Contract Requirement)
Some actions require step-up authentication (Tier 3), especially when they:
- Create or ratify high-trust ledger state
- Void records after ratification
- Perform Level 4 Context-Verified execution events
- Change verification requirements/policies
Backend contract (mandatory):
- Requests that require step-up MUST include a step-up proof token (mechanism implementation-defined)
- Backend validates the proof and records the method used in the ledger context metadata
5. Authorization Model (Capabilities First)
5.1 Capability = Atomic Permission
Capabilities are stable identifiers that represent actions. Capability checks answer:
“Are you allowed to attempt this action?”
They do not replace verification policy (which answers: “what proof is required to write?”).
Suggested capability groups (stable, expandable)
authoring
questions.read,questions.writeevaluations.read,evaluations.writetaxonomy.read,taxonomy.manage,taxonomy.assignprogrammes.read,programmes.manage(if used)
Engagement / Project (Golden Thread)
engagements.readengagements.manage
Assignments / Execution
assignments.issuesessions.launchsessions.monitor(future)
Execution ledger write surface (split by intent)
execution.write(record execution answers/events)evidence.attach(upload/attach evidence + metadata binding)review.sign(peer verification / sign-off)ledger.amend(append an amendment event)ledger.void(append a void event)ledger.view(read ledger timelines / history views)
Ratification
ledger.ratify(stakeholder ratification)
Reporting
reporting.view(aggregated)reporting.view_named(PII-bearing views)reporting.export
Identity & Admin
users.read,users.manageroles.read,roles.manageorg.managebilling.read_invoice,billing.manage_subscription
System (dev/build gated)
debug.access(dev builds only)
Capability names may expand; existing ones should remain backwards compatible.
5.2 Roles = Sets of Capabilities
Roles are tenant-scoped collections of capabilities.
Rules:
-
No hard-coded role checks in handlers
-
Permission checks are capability-based only:
- ✅
if user.can("ledger.void") - ❌
if role == "Owner"
- ✅
5.3 Policy vs Capability (must be explicit)
Even when a capability allows an action, policy may still reject it.
Examples:
- User has
ledger.voidbut policy forbids voiding after ratification without step-up. - User has
reporting.view_namedbut disclosure policy forbids named views for a given portal link scope. - User has
taxonomy.assignbut may only change tags on content they can already read; content edits still require the relevant*.writecapability.
5.4 Frontend contract: actions, not raw capability strings
The frontend should not branch directly on raw capability names in page code.
Instead, pages and shared components should consume stable action predicates such as:
questions:view_listquestions:createquestions:editquestions:assign_taxonomyevaluations:bulk_mutate
This keeps the UI stable when coarse capabilities later split into finer enterprise permissions.
Current mapping guidance:
questions.readgrants question/passage list/detail viewingquestions.writegrants question/passage content mutationevaluations.readgrants evaluation list/detail viewingevaluations.writegrants evaluation mutationtaxonomy.assigngrants tag mutation on readable content without implying content edit rights
5.5 Capability checks do not replace idempotency policy
Capability answers who may attempt an action. Idempotency answers how retries are safely handled for mutating actions.
For in-scope mutating endpoints, both are required:
- capability check (authorization)
- keyed idempotency (
Idempotency-Key) per policy
Missing either control must reject the request.
6. Route → Capability Matrix (Current HTTP Surface)
This section documents the current mapping pattern. The identifiers may evolve as endpoints are reorganized, but enforcement remains capability-based.
Public
POST /auth/loginGET /auth/verify/health
Auth only (no capability check)
GET /auth/me(introspection)
Debug (dev-only)
/debug/**requiresdebug.accessAND must be disabled/blocked in production builds.
| Surface | Routes (/api/v1…) | Capability |
|---|---|---|
| Questions | GET /questions, GET /questions/\{id\} | questions.read |
POST/PATCH/DELETE /questions/**, /versions/**, /publish, /archive | questions.write | |
PUT /taxonomy/questions/\{id\}/terms, taxonomy-only PATCH /questions/\{id\}, POST /questions/bulk with taxonomyReplace | questions.read + taxonomy.read + (questions.write or taxonomy.assign) | |
| Passages | GET /passages, GET /passages/\{id\} | questions.read |
POST/PATCH/DELETE /passages/**, /questions/** | questions.write | |
PATCH /passages/\{id\} for taxonomy-only updates, POST /passages/bulk with taxonomyReplace | questions.read + taxonomy.read + (questions.write or taxonomy.assign) | |
| Evaluations | GET /evaluations/**, /versions/**, /preview, /validate, /usage | evaluations.read |
POST/PATCH/DELETE /evaluations/**, /versions, /publish, /feedback, /sections/** | evaluations.write | |
PUT /taxonomy/evaluations/\{id\}/terms, POST /evaluations/bulk with taxonomyReplace | evaluations.read + taxonomy.read + (evaluations.write or taxonomy.assign) | |
| Buckets | GET /buckets/** | evaluations.read |
POST/PATCH/DELETE /buckets/**, /preview | evaluations.write | |
| Programmes | GET /programmes/**, /programmes/\{id\}/requirements | programmes.read |
POST/PATCH/DELETE /programmes/**, /requirements/** | programmes.manage | |
| Sessions | POST /evaluations/\{id\}/sessions/** (create/answers/submit) | sessions.launch + execution.write |
| Evidence / Media | /media/** | evidence.attach (and/or media.manage if retained) |
| Evidence review | POST /submissions/\{id\}/evidence/approve | submissions.approve |
POST /submissions/\{id\}/evidence/reject | submissions.review | |
| Ledger views | GET /submissions/\{id\} | ledger.view + (if named) reporting.view_named |
| Evaluation submissions | GET /evaluations/\{id\}/submissions | reporting.view |
| Users | GET /users, GET /users/\{id\} | users.read |
POST/PATCH/DELETE /users/** | users.manage | |
| Roles | GET /roles, /roles/capabilities | roles.read |
POST/PATCH/DELETE /roles/**, role bindings routes | roles.manage | |
| Debug (dev) | /debug/** | debug.access |
Note: If you keep
media.manage, keep it as an internal/admin capability. For execution flows preferevidence.attach.Alignment: Evidence review uses capabilities only. Avoid hard-coded role names (e.g., "proctor"); use capability aliases where a domain term is needed.
Idempotency alignment: For in-scope mutating routes in this matrix,
Idempotency-Keyis required perdocs/architecture/IDEMPOTENCY-AND-RETRY-POLICY.md(explicit exemptions are policy-defined).
7. Default Roles (Operational Defensibility Baseline)
These roles ship as defaults. Tenants may later customize roles (Phase 2+).
7.1 Tenant Owner
Full tenant control.
- All permissions
- Billing management
- Org management
- User management
- Reporting across all orgs
- Ledger admin actions (amend/void/verify/ratify) when permitted by policy
7.2 Tenant Admin (Global)
Controls all orgs within a tenant (but not billing).
- Manage users and roles
- Manage authoring
- Issue assignments
- Reporting across all orgs
- Manage engagements/projects (if enabled)
- Configure verification policies (where allowed)
7.3 Org Admin
Controls one org unit.
- Manage users in org
- Manage content in org
- Issue assignments in org
- Reporting in org
- Manage engagements/projects in org (if enabled)
7.4 Author
Creates and maintains versioned templates/checklists/gates.
questions.read/writeevaluations.read/write- Publish versions
- View reports for authored content (org-scoped)
7.5 Operator (Practitioner)
The person doing the work / executing tasks.
- Launch assigned tasks
execution.writeevidence.attach(where allowed)- View own outcomes/results (where policy allows)
7.6 Verifier (Supervisor)
Peer reviewer/sign-off for Level 2 flows.
ledger.viewreview.sign- Read evidence needed to verify
- No authoring/admin access
Verifier actions may require step-up auth depending on policy.
7.7 Client / Stakeholder (Glass Box User)
Read-only ledger visibility + (optional) ratification.
ledger.view(typically via portal projection)ledger.ratify(if enabled)- No authoring/admin access; no execution write access
Notes:
- Often provisioned via magic link (Tier 1) or dedicated accounts (Tier 2)
- Ratification typically requires step-up auth
7.8 Reviewer (External SME)
Read-only access to authoring content (org-scoped), optional expiry.
questions.read,evaluations.read- No publish/edit
7.9 Auditor (Internal/External)
Inspection of records and reporting.
reporting.view- Optional
reporting.view_named(strictly controlled) ledger.view- No authoring/admin edits
7.10 Billing Admin
Finance-only access.
billing.read_invoicebilling.manage_subscriptionNo access to authoring/execution/reporting/ledger data.
8. External Sharing (Magic Links) — First-Class
Evalium supports secure, scoped, revocable links via a Link Principal.
8.1 Link Types
- Engagement/Project summary (glass box)
- Evaluation summary
- Assignment/Cohort summary
- Single ledger entry / submission view
- Aggregated-only reporting
- Aggregated + Named (PII-bearing) where explicitly enabled
8.2 Link Record Shape (conceptual)
tenant_id
org_unit_id
scope_type (engagement|evaluation|assignment|submission)
scope_id
permissions[] (e.g., ledger.view, reporting.view, ledger.ratify)
expires_at
can_view_named: true|false
display_name (optional)
revoked_at (optional)
created_by_user_id
8.3 Rules
- Links are revocable
- Links are RLS-enforced
- Links should be short-lived by default
- PII-bearing views require explicit enablement (
can_view_named=true) AND the link must include the right permission(s)
9. Guest & Temporary Access Patterns
9.1 Temporary Reviewers
- Guest accounts with expiry timestamps
- Org-scoped and/or evaluation-scoped
- Read-only authoring review
9.2 Guest Reporting / Client Visibility
- Magic link viewer (Tier 1 / Link Principal)
- Or dedicated stakeholder account (Tier 2+)
- Always RLS enforced
10. Custom Roles & RBAC Engine (Non-Negotiable)
The RBAC engine is final. UI may only expose a subset of roles initially, but the backend must support unlimited custom roles via capability lookups.
Rules:
- No hardcoded role checks
- JWT contains
role_ids, never capability lists - Capability lookup is server-side and cacheable
- Role changes must take effect immediately without reissuing JWTs (server-side lookup ensures this)
10.1 Custom Role Management (Roadmap)
-
Phase 1: default roles managed via API
-
Phase 2: UI Role Editor:
- create/clone/edit roles
- capability selection UI
- org-scoped role assignment rules
11. UX Principles (SMB-Friendly, Defensibility Underneath)
-
Hide complexity when unnecessary (single org = no org UI)
-
Prefer “show/hide” over permission errors (but backend still blocks)
-
Low friction access:
- magic links for viewers/clients/auditors
- passwordless login for teams
-
Avoid heavy legal jargon in UI labels:
- “History”, “Amended”, “Voided”, “Verified”, “Approved”
-
Preserve the “Save button lie”:
- workflow feels editable
- history remains immutable and visible when needed
12. Implementation State Summary
Already Implemented (per current architecture direction)
- Multi-tenant + multi-org silo architecture
- RLS + TxManager using SET LOCAL
- Combined tenant+org RLS policies
- Lean JWT approach (role_ids only)
- Version freeze model on submission (snapshot-driven reporting)
- Core RBAC scaffolding endpoints (roles, bindings) where present
To Implement / Align Next
- Role/terminology updates (Candidate→Operator, Proctor→Verifier, add Client/Stakeholder)
- Ledger-specific capability split (execution.write vs review.sign vs amend/void/ratify)
- Step-up auth contract enforcement for high-stakes actions
- Magic link expansion to support Glass Box ledger views (engagement-scoped)
- Verification policy enforcement (required context metadata + tier requirements) at write boundaries
13. Future-Proof Guarantees
This access model ensures:
- Safe scaling to many org silos
- Custom roles without redesign
- Secure external sharing with revocation and expiry
- Controlled PII exposure (named vs aggregated)
- Evolution of auth methods without re-architecting
- A clear path to client ratification and milestone state proofs