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
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
The embedding workers in the Python ai-service. Never a person.
How it is granted
Set by the worker when it opens its database transaction: app.current_role = super_admin. No sign-up, no password.
What it can do
- Claim embedding jobs from the queue
- Write chunks and vectors into resource_chunks
- Refresh competency embeddings for trainers and courses
- Keep the AI features current without any user waiting
Example menu
None. This identity has no interface.
Data it can reach
The tables the embedding pipeline writes. One extra RLS policy per table allows it, and nothing else changes for anyone.
Typical API calls
- None. It talks to Postgres and Ollama directly, not to the public API.
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=
Who holds it
Subject-matter trainers who deliver courses.
How it is granted
Signs up as a trainer and is approved by an admin. Approval is per role row.
What it can do
- Create and publish courses
- Upload lectures, presentations and study material
- Generate an AI draft, edit it, set a deadline, publish
- Monitor trainee performance and read feedback
- Keep a profile whose skills feed competency matching
Example menu
- Profile
- Courses
- Resources
- Assessments (builder and review)
- Trainee monitoring
Data it can reach
Their own profile and uploads, plus the courses they teach. RLS hides other trainers' profiles and vectors.
Typical API calls
- POST /api/courses
- POST /api/resources
- POST /api/questionnaires/:id/generate
- PUT /api/trainers/me/profile
Who holds it
Officers and staff taking training.
How it is granted
Signs up as a trainee and is approved by an admin.
What it can do
- Enroll in a course, once
- Study the resources of courses they joined
- Attempt each questionnaire once, before its deadline
- Give a 1 to 5 rating and a comment
- See skill gaps and download a verifiable PDF certificate
Example menu
- Profile
- Courses
- Resources
- Assessments
- Feedback
Data it can reach
Only rows from courses they joined, and only their own attempts and certificates. Answer keys never reach the client.
Typical API calls
- POST /api/courses/:id/enroll
- GET /api/courses/:id/resources
- POST /api/questionnaires/:id/attempts
- POST /api/courses/:id/feedback
Permission matrix
Who can do what. The system tier appears only where it does something no person can.
| Capability | Super admin | Admin | Trainer | Trainee |
|---|---|---|---|---|
| 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
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
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.
Row-level security filters rows and vectors by role, so even a handler bug cannot return another trainer's profile.
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.