Roles & permissions
The four CLIP Learn roles, the capability matrix, tenant and org isolation, and how corporate sponsors are kept out of learner content.
Every account in CLIP Learn holds exactly one role, and that role decides what the person can see and do. There are four roles — CANDIDATE, FACULTY, ADMIN and CORPORATE — and they fall into two lines of trust that share one learner base: internal staff (FACULTY, ADMIN) who build and run the programme, and external people (CANDIDATE learners, CORPORATE sponsors) who consume it.
This page documents each role, the full capability matrix, and the two separate things that control access: capabilities (what action a role may take) and scope (which rows of data that action reaches). A capability grants the action; the scope decides over which learners, courses or tenant it applies.
The role model at a glance
ADMIN (internal · full platform control)
│
FACULTY (internal · authors + client success)
│
CORPORATE ──────── CANDIDATE ─────── (learner base)
(external sponsor) (learner)| Role | Who they are | Where they land after login |
|---|---|---|
CANDIDATE | A learner. The default role for every new account. | /app (the learner home) |
FACULTY | Internal trainer — authors content, manages clients and trainees, reviews questions. | /faculty |
ADMIN | Internal programme admin — everything FACULTY can do, plus user/role management and the audit log. | /admin (Programme admin) |
CORPORATE | External client sponsor (a company's L&D contact). Sees only their own organisation's people and ROI. | /admin (Team overview) |
One role per account
The role lives on the User.role field and defaults to CANDIDATE. A user's tenant/organisation membership also carries its own Membership.role, but access control reads User.role. Only an ADMIN can change a role (see Managing roles).
Capability matrix
Capabilities are the named actions the system recognises. The table below is the complete matrix — a tick means the role holds that capability.
| Capability | What it allows | CANDIDATE | FACULTY | ADMIN | CORPORATE |
|---|---|---|---|---|---|
content.readEnrolled | Read the content of courses you're actively enrolled in | Yes | — | — | — |
practice | Use drills, mock exams and the MOEgent AI trainer | Yes | — | — | — |
content.author | Create/edit courses, lessons and the media library | — | Yes | Yes | — |
content.readAny | Preview any course's content (internal staff only) | — | Yes | Yes | — |
questions.manage | Author and review question banks | — | Yes | Yes | — |
learners.viewAll | See every learner in the tenant | — | Yes | Yes | — |
learners.viewOrg | See only your own organisation's learners and analytics | — | Yes | Yes | Yes |
learners.manage | Create, suspend, reset and enrol learners | — | Yes | Yes | — |
clients.manage | Create clients (organisations) and grant org-level access | — | Yes | Yes | — |
knowledge.viewTenant | Browse all of the tenant's knowledge sources | — | Yes | Yes | — |
knowledge.viewOrg | Browse only your org's entitled knowledge sources | — | — | — | Yes |
users.manage | Manage staff accounts and change roles | — | — | Yes | — |
audit.view | View the governance audit log | — | — | Yes | — |
billing.view | View seats and ROI reporting | — | — | Yes | Yes |
Read the gaps, not just the ticks
FACULTY deliberately lacks users.manage, audit.view and billing.view — a trainer can build and teach but cannot change roles, read the audit log, or see billing/ROI. CORPORATE holds only three capabilities and none of them touch course content or answer keys.
What each role can and cannot do
CANDIDATE (learner)
The base role, and the default for any new sign-up.
- Can: read the content of courses they hold an active, non-expired enrolment in; use Practice, Flashcards, Assignments, Written exams, Exams, the discussion forum, Evaluation and the MOEgent AI trainer; manage their own profile and settings.
- Cannot: see other learners, author or preview courses they're not enrolled in, touch any admin or faculty surface, or read another course's / another tenant's question bank.
- Content gate: access to a course requires a live enrolment. When the enrolment has an
expiresAtin the past, access ends — the "My course" nav link disappears rather than pointing at a stale slug.
FACULTY (internal trainer)
Internal staff who build and run the teaching. Everything under /faculty is gated to FACULTY and ADMIN.
- Can: author courses, lessons and media; author and review question banks; manage clients (organisations) and grant them access; create, enrol, suspend and reset learners; preview any course in their tenant; run assignments, written exams, the gradebook, SCORM and API/webhooks.
- Cannot: change anyone's role, manage staff accounts, view the audit log, or see billing/ROI reporting. Those are ADMIN-only.
- FACULTY is tenant-scoped — a faculty member never reaches another tenant's data.
ADMIN (internal programme admin)
Everything FACULTY can do, plus the governance surface at /admin.
- Can, on top of FACULTY: manage users and change roles (
/admin/users); read the audit log (/admin/audit); view billing/ROI and the benchmark; post to the learner noticeboard; answer escalated learner questions ("Learner questions for the team"); browse conversations. ADMIN sees platform-wide stats (all learners, all tickets, all activity). - Cannot: cross tenant boundaries. ADMIN is "full platform control" within its own tenant only — it is never cross-tenant.
CORPORATE (external client sponsor)
A paying client's L&D contact — not staff. They share the /admin route with ADMIN but see a completely different, tightly scoped surface titled "Team overview".
- Can: see their own organisation's sponsored candidates at a glance; view a per-employee readiness/ROI table (progress, accuracy, best mock, target sitting, projected exam-readiness); export a board-ready CSV; browse only the knowledge sources their org is entitled to; see seats-used-vs-purchased.
- Cannot — and this is enforced, not just hidden:
- Read any lesson body, past paper or exam answer (see Keeping sponsors out of learner content).
- See learners outside their own organisation.
- Manage the platform: no Users tab, no Conversations, no Audit, no Benchmark, no noticeboard, no ticket answering, and no MOEgent copilot panel — all of those are rendered only when the role is ADMIN.
- Post announcements or reply to escalations.
How access is enforced
Access is checked on the server for every page and API route. The building blocks live in src/lib/session.ts and src/lib/rbac.ts.
| Guard | Used by | Behaviour when denied |
|---|---|---|
requireUser() | Every signed-in page | No session → redirect to /login; suspended account → redirect to /suspended |
requireRole(...roles) | Role-gated pages (e.g. /faculty, /admin) | Role not in the list → redirect to /app |
requireCapability(cap) | Capability-gated actions | Role lacks the capability → redirect to /app |
apiUser() | API routes (can't redirect) | Returns null for no-session or suspended → the route replies 401 |
The database is the source of truth
On every request the session helper re-reads role and active straight from the database — it does not trust the value baked into the login cookie. That is why a role change or a suspension takes effect immediately, even though the user's Better Auth cookie is still technically valid. Suspension flips User.active to false; it does not revoke the cookie, so apiUser() is what stops a suspended user's live cookie from reaching the trainer, practice or mock endpoints.
Tenant isolation
CLIP Learn is multi-tenant. A tenant is a whole training organisation; within it there can be many client organisations (Orgs).
- The tenant for a request is derived from the signed-in user's Membership, never hard-coded. This stays correct the moment a second tenant exists.
- Fail-closed rule: an authenticated user with no membership has no tenant context — callers scope to
nulland reject rather than leaking the oldest tenant's data. - The only time the system falls back to "the sole tenant" is when there is no session at all (server-to-server or pre-auth branding paths), which is safe while the platform is single-tenant.
- No role crosses tenants — not even ADMIN. "Full platform control" means full control of that admin's own tenant.
Question banks are isolated the same way. A learner's practice/mock questions are limited to their tenant, and within it to the courses they are actively enrolled in, plus one shared general practice bank (sqe-sample). This stops another course's — or another tenant's — questions, and the answer keys attached to them, from being pulled into practice. Staff question scope is tenant-wide but never cross-tenant.
Keeping sponsors out of learner content
CORPORATE is the one external role that gets an "admin-shaped" dashboard, so it is deliberately fenced off from anything a learner reads. Two mechanisms do this:
hasCourseAccess / hasProgramAccess checks return true for FACULTY and ADMIN, require a live enrolment for a CANDIDATE, and return false for CORPORATE (and any other role). These gate the course, lesson and past-paper pages, progress writes and certificate issuance — so a sponsor can never open a lesson body or an exam answer.adminScope. For ADMIN it returns "no restriction" (sees everyone). For CORPORATE it returns the explicit list of candidate user IDs in the sponsor's own organisation — and every learner list, statistic and ticket query is filtered to that list. If the sponsor's org has no members, the list is empty and they see nothing, rather than everything.A sponsor's knowledge browsing is scoped the same way: they see only the sources their organisation is entitled to (its direct course grants plus every phase-course of any programme it's been granted, non-expired only), never the tenant-wide catalogue that staff see.
Why this split matters
The Team overview a CORPORATE user renews on is intentionally ROI-only — seats, cohort completion, projected readiness, a per-employee table and a CSV export. No escalations, no platform-wide data, and no content. The action a role holds and the rows it may touch are two separate axes, and for CORPORATE both are locked down.
Managing roles and suspension
Changing roles and suspending accounts is ADMIN-only, under Admin → Users (/admin/users). FACULTY and CORPORATE cannot reach this page.
| Field / control | What it does | Notes |
|---|---|---|
| Role selector | Sets the account's role to one of ADMIN, FACULTY, CORPORATE, CANDIDATE | Only ADMIN; an invalid role value is ignored |
| Status toggle | Flips Active / Suspended (User.active) | Suspended users are pushed to /suspended and blocked from API routes |
| Edit details | Name, email, WhatsApp/phone | Email must be unique — a clash with another account is rejected |
| Reset password | Issues a fresh temporary password, shown to the admin once | — |
Two lockout guards protect role changes so an organisation can never be locked out of its own admin surface:
- No self-demotion — an admin cannot change their own role to anything other than ADMIN.
- No demoting the last admin — if the target is currently an ADMIN and they are the only active admin, the change is refused.
Every one of these actions is written to the governance audit log (ROLE_CHANGE, SUSPEND_USER / RESTORE_USER, EDIT_USER, RESET_PASSWORD, ANNOUNCE, ASSIGN_COHORT, ANSWER_TICKET), which only ADMIN can read via Admin → Audit.