Roles and access

Start

Roles and access

Four roles form a strict hierarchy of responsibility: one system tier and three human tiers. Access is decided by the server on every request. It does not depend on a hidden menu item or a client-side check.

The role hierarchy

Authority narrows as you go down: a system tier that keeps the platform running, an admin who governs, a trainer who teaches, and a trainee who learns. Each arrow says what one tier does for the next. The right-hand column is the rule the database applies to that tier.

Role hierarchy

AUTHORITYHOW THE DATABASE ENFORCES ITSUPER ADMINSystem tier · service identity, no screens✓ Runs the background embedding workers✓ Writes the vector data the AI features rely on✓ Never a login: no menu, no dashboardRLS ruleapp.current_role = super_adminkeeps embeddings and jobs runningADMINGoverning tier · person, web admin console✓ Approves accounts, assigns and manages roles✓ Owns dropdowns, notices and dashboards✓ Sees competency suggestions for every courseRLS ruleadmin sees all trainer profiles and resourcesapproves accounts, grants the roleTRAINERTeaching tier · person, portal and mobile✓ Creates courses, uploads the resource library✓ Drafts, reviews and publishes MCQ assessments✓ Monitors trainee results and feedbackRLS ruleown profile and own uploadspublishes courses, quizzes, resourcesTRAINEELearning tier · person, portal and mobile✓ Enrolls, studies, attempts each quiz once✓ Gives feedback, sees skill gaps✓ Downloads a verifiable certificateRLS ruleonly rows from courses they joined
super_admin is a service identity that has no screens. Admin, trainer and trainee are the people who use the platform.

Meet each role

Select a role to see who holds it, how it is granted, what it can do, what its menu looks like and what data it can reach.

Who holds it

Programme owners and coordinators at the department.

How it is granted

Signs up, then waits in the approval queue until an existing admin sets approved = true.

What it can do

  • Approve or reject accounts and manage roles in bulk
  • Manage dropdown taxonomies (subjects, skills, categories)
  • Post notices to chosen roles, with an expiry
  • Watch enrolment, completion and pass-rate dashboards
  • Open a course and see the five closest trainers by competency

Example menu

  • Approvals
  • Courses
  • Assessments overview
  • Competency mapping
  • Dropdown manager
  • Notices

Data it can reach

Everything, through RLS admin rules. The only cross-trainer vector view is a separate job, not the admin session itself.

Typical API calls

  • GET /api/admin/users?status=pending
  • PATCH /api/admin/users/:id/approve
  • POST /api/notices
  • GET /api/admin/competency/suggest?course_id=

Permission matrix

Who can do what. The system tier appears only where it does something no person can.

CapabilitySuper adminAdminTrainerTrainee
Sign up and log in–✓✓✓
Waits for admin approval–✓✓✓
Approve accounts, assign roles–✓––
Manage dropdowns and notices–✓––
Create and publish courses–✓✓–
Upload resources––✓–
Generate and publish assessments––✓–
Enroll in a course–––✓
Attempt an assessment, once–––✓
Give feedback on a course–––✓
Download own certificate–––✓
Issue certificates–✓✓–
See competency suggestions–✓––
Write embeddings and chunks✓–––
Read every resource–✓––

How a role becomes an access decision

Roles are stored as rows, not as a column on the user. That is what lets one person hold more than one role later without a schema change. On every request the same four questions are asked, in the same order, on web and mobile.

From role to rows

noyesnoyes

Signed-in user
Bearer JWT

attachRole
reads user_roles

approved = true?

403, account waits in admin queue

authorizeRoute
route allowed for this role?
(nav_item_roles)

403

injectRLSContext
app.current_role = super_admin,
admin, trainer or trainee

RLS policies
filter every row and vector

Handler sees only what
this role may see

Failing any step stops the request. The final step is the database itself, so a bug in a handler cannot widen access.
packages/db/sql/roles.sql
CREATE TYPE user_role AS ENUM ('super_admin', 'admin', 'trainer', 'trainee');

CREATE TABLE user_roles (
  user_id   UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
  role      user_role NOT NULL,
  approved  BOOLEAN NOT NULL DEFAULT false,      -- the admin approval gate
  PRIMARY KEY (user_id, role)
);

Approval lifecycle

Signing up is easy. Getting in is not automatic: every role row starts unapproved, and the attachRole stage turns the account away until an admin approves it.

Account approval states

sign up (Better Auth)user_roles row created,approved = falseadmin approvesadmin rejectsattachRole passes on everyrequest

Registered

Pending

Approved

Rejected

Active

Never trust the menu

The sidebar is generated from nav_items and nav_item_roles, but hiding an item is only presentation. authorizeRoute checks the same table on the server.

Least privilege in data

Row-level security filters rows and vectors by role, so even a handler bug cannot return another trainer's profile.

One privileged path

The single feature that needs to read every trainer's vector, competency suggestions, runs under its own BYPASSRLS database role that no request handler can import.

Capacity Connect · Team Syntax Squad · SIH 2026 · PS 26075Code samples are implementation sketches.