Manages cycles, creates other admins, full analytics
admin_l2
Posts jobs, manages verifiers, scoped to assigned cycles
verifier
Reviews and approves student content for assigned students only
student
Own data only
Custom roles can be created in the roles table with a custom permissions array. The system uses permission-based checks, not role-name checks, so custom roles work everywhere.
Permissions matrix
Student management
Permission
super_admin
admin_l1
admin_l2
verifier
student
students:read (all students)
✓
✓
✓
—
—
students:read_assigned (own assigned students)
✓
✓
✓
✓
—
students:create
✓
✓
✓
—
—
students:update
✓
✓
✓
—
—
students:delete
✓
—
—
—
—
students:bulk_import
✓
✓
✓
—
—
students:export
✓
✓
✓
—
—
profile:read_own
✓
✓
✓
✓
✓
profile:update_own
✓
—
—
—
✓
Placement cycles
Permission
super_admin
admin_l1
admin_l2
verifier
student
cycles:create
✓
✓
—
—
—
cycles:read
✓
✓
✓ (assigned only)
—
✓ (eligible)
cycles:update
✓
✓
—
—
—
cycles:delete
✓
—
—
—
—
cycles:manage_enrollment
✓
✓
—
—
—
cycles:bulk_enroll
✓
✓
—
—
—
Jobs
Permission
super_admin
admin_l1
admin_l2
verifier
student
jobs:create
✓
✓
✓
—
—
jobs:read
✓
✓
✓
—
✓ (eligible)
jobs:update
✓
✓
✓
—
—
jobs:delete
✓
✓
—
—
—
jobs:toggle_applications
✓
✓
✓
—
—
Applications
Permission
super_admin
admin_l1
admin_l2
verifier
student
applications:create_own
✓
—
—
—
✓
applications:read (all)
✓
✓
✓ (assigned cycles)
—
—
applications:read_own
✓
—
—
—
✓
applications:update_status
✓
✓
✓
—
—
applications:bulk_download
✓
✓
✓
—
—
applications:send_to_recruiter
✓
✓
✓
—
—
Verifications
Permission
super_admin
admin_l1
admin_l2
verifier
student
verifications:read (assigned)
✓
✓
✓
✓
—
verifications:approve
✓
✓
✓
✓
—
verifications:reject
✓
✓
✓
✓
—
verifications:assign
✓
✓
✓
—
—
verifications:request
—
—
—
—
✓
Events
Permission
super_admin
admin_l1
admin_l2
verifier
student
events:create
✓
✓
✓
—
—
events:read
✓
✓
✓
—
✓
events:update
✓
✓
✓
—
—
events:delete
✓
✓
—
—
—
events:mark_attendance
✓
✓
✓
—
—
events:exceptions:review
✓
✓
—
—
—
events:exceptions:request
—
—
—
—
✓
Analytics + Intel
Permission
super_admin
admin_l1
admin_l2
verifier
student
analytics:read
✓
✓
✓
—
—
intel:query
✓
✓
✓
—
—
intel:export
✓
✓
✓
—
—
Tenant configuration
Permission
super_admin
admin_l1
admin_l2
verifier
student
tenant:config:read
✓
✓
—
—
—
tenant:config:update
✓
—
—
—
—
tenant:domain_rules:update
✓
—
—
—
—
tenant:allowlist:manage
✓
—
—
—
—
tenant:feature_flags:update
✓
—
—
—
—
User / role management
Permission
super_admin
admin_l1
admin_l2
verifier
student
roles:assign:super_admin
✓
—
—
—
—
roles:assign:admin_l1
✓
—
—
—
—
roles:assign:admin_l2
✓
✓
—
—
—
roles:assign:verifier
✓
✓
✓
—
—
roles:assign:student
✓
✓
✓
—
—
users:deactivate
✓
✓
—
—
—
API keys
Permission
super_admin
admin_l1
admin_l2
verifier
student
api_keys:create
✓
—
—
—
—
api_keys:revoke
✓
—
—
—
—
api_keys:list
✓
—
—
—
—
API key scopes
When a super admin creates an API key, they select which permission scopes it carries. API keys can never have more permissions than admin_l2 by default. Suggested key scopes:
Scope name
Permissions included
student_data_readonly
students:read, profile:read_own
jobs_readonly
jobs:read, cycles:read
applications_readonly
applications:read
full_readonly
All :read permissions
student_write
students:read, students:create, students:update
webhooks
Receive webhook events (no DB reads — outbound only)
Role hierarchy enforcement
When assigning a role, the assigner can only assign roles at their level or below:
// middleware/require-permission.tsexport function requirePermission(permission: string) { return async (req: FastifyRequest, reply: FastifyReply) => { const userPermissions = req.user.permissions; // Wildcard check for super_admin if (userPermissions.includes('*')) return; // Exact match if (userPermissions.includes(permission)) return; // Partial match: "students:read_assigned" satisfies "students:read_assigned" // but NOT "students:read" reply.code(403).send({ error: 'forbidden', message: `Missing required permission: ${permission}`, }); };}// Usage on a route:app.get('/tenants/:slug/students', { preHandler: [requirePermission('students:read')],}, handler);
For verifier accessing students: the verifier has students:read_assigned. The handler additionally filters to WHERE id IN (SELECT student_id FROM verifier_assignments WHERE verifier_id = req.user.id).