CLIP LearnDocs 1.0
Reference

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)
RoleWho they areWhere they land after login
CANDIDATEA learner. The default role for every new account./app (the learner home)
FACULTYInternal trainer — authors content, manages clients and trainees, reviews questions./faculty
ADMINInternal programme admin — everything FACULTY can do, plus user/role management and the audit log./admin (Programme admin)
CORPORATEExternal 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.

CapabilityWhat it allowsCANDIDATEFACULTYADMINCORPORATE
content.readEnrolledRead the content of courses you're actively enrolled inYes
practiceUse drills, mock exams and the MOEgent AI trainerYes
content.authorCreate/edit courses, lessons and the media libraryYesYes
content.readAnyPreview any course's content (internal staff only)YesYes
questions.manageAuthor and review question banksYesYes
learners.viewAllSee every learner in the tenantYesYes
learners.viewOrgSee only your own organisation's learners and analyticsYesYesYes
learners.manageCreate, suspend, reset and enrol learnersYesYes
clients.manageCreate clients (organisations) and grant org-level accessYesYes
knowledge.viewTenantBrowse all of the tenant's knowledge sourcesYesYes
knowledge.viewOrgBrowse only your org's entitled knowledge sourcesYes
users.manageManage staff accounts and change rolesYes
audit.viewView the governance audit logYes
billing.viewView seats and ROI reportingYesYes

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 expiresAt in 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.

GuardUsed byBehaviour when denied
requireUser()Every signed-in pageNo 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 actionsRole 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 null and 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:

Content is denied at the gate. The 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.
Data is scoped to their own org. The admin surface calls 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 / controlWhat it doesNotes
Role selectorSets the account's role to one of ADMIN, FACULTY, CORPORATE, CANDIDATEOnly ADMIN; an invalid role value is ignored
Status toggleFlips Active / Suspended (User.active)Suspended users are pushed to /suspended and blocked from API routes
Edit detailsName, email, WhatsApp/phoneEmail must be unique — a clash with another account is rejected
Reset passwordIssues 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.

On this page