User Role
Data Entity
Description
Assigns a named role to a user within a specific organization context, governing which products, screens, and API endpoints the user may access. A single user may hold multiple role rows — one per organization they belong to — enabling profile switching without token re-issuance.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Surrogate primary key. Generated server-side (UUID v4) at assignment time. | PKrequiredunique |
user_id |
uuid |
Foreign key to users.id. The user receiving this role assignment. | required |
organization_id |
uuid |
Foreign key to organizations.id. Scopes the role to a specific tenant. NULL for global_admin rows, which have no tenant context. | - |
role |
enum |
The role granted to the user. Drives access control across mobile app, admin portal, and API layer. | required |
is_active |
boolean |
Soft-active flag. False when the peer mentor has triggered the pause/deactivation flow or been deactivated by an admin without full deletion. Active sessions with this role are invalidated asynchronously. | required |
assigned_by |
uuid |
Foreign key to users.id. Records which admin or coordinator created this assignment. NULL for seed/bootstrap rows. | - |
assigned_at |
datetime |
UTC timestamp when the role was granted. Immutable after creation. | required |
expires_at |
datetime |
Optional UTC expiry for temporary role grants (e.g. a coordinator covering for another organization). NULL means the role is permanent until explicitly revoked. | - |
deactivated_at |
datetime |
UTC timestamp when is_active was set to false. NULL while active. Used to audit the pause/deactivation trail without relying on audit_logs for basic queries. | - |
deactivation_reason |
enum |
Machine-readable reason for deactivation. Drives UI messaging and determines whether the role can be self-reactivated. | - |
metadata |
json |
Extensible bag for per-role supplemental config (e.g. proxy_scope: [array of peer-mentor user_ids a coordinator may act on behalf of], or certification_id linking to a certificate row for roles gated by certification). Kept sparse; promoted to columns only when queried at scale. | - |
created_at |
datetime |
Row creation timestamp. Equals assigned_at for new grants; can differ if a row is logically recreated via upsert after a prior deletion. | required |
updated_at |
datetime |
Last mutation timestamp. Updated on every write via a database trigger or ORM hook. | required |
Database Indexes
idx_user_roles_user_id
Columns: user_id
idx_user_roles_organization_id
Columns: organization_id
idx_user_roles_user_org_role
Columns: user_id, organization_id, role
idx_user_roles_role_active
Columns: role, is_active
idx_user_roles_org_active
Columns: organization_id, is_active
idx_user_roles_expires_at
Columns: expires_at
Validation Rules
role_value_in_allowed_set
error
Validation failed
assigned_by_must_be_active_admin
error
Validation failed
expires_at_must_be_future
error
Validation failed
metadata_is_valid_json_object
error
Validation failed
deactivation_reason_required_when_inactive
error
Validation failed
bulk_role_update_org_scope_check
error
Validation failed
Business Rules
unique_role_per_user_per_org
A user may hold at most one active row with a given role within a given organization. Prevents duplicate access grants and ambiguous JWT claims. Enforced by the unique index idx_user_roles_user_org_role in combination with an upsert pattern at assignment time.
global_admin_has_no_organization
Rows where role = 'global_admin' MUST have organization_id = NULL. Global admins operate across all tenants and must not be scoped to any single organization. Attempted global_admin grants with a non-null organization_id are rejected.
non_global_role_requires_organization
All roles other than global_admin require a non-null organization_id. A peer_mentor, coordinator, or org_admin without an organization context is meaningless and would bypass tenant isolation.
peer_mentor_cannot_be_org_admin_same_org
Within a single organization a user may not simultaneously hold peer_mentor and org_admin roles. The role hierarchy is strict: peer_mentor < coordinator < org_admin. Holding both would create conflicting access boundaries. A user may hold peer_mentor in org A and coordinator in org B (multi-org scenario).
deactivation_invalidates_sessions
When is_active is set to false (pause, certificate expiry, admin deactivation), all refresh tokens for the affected user+organization combination must be revoked immediately. The session-revocation-service is called synchronously before the update is committed.
certificate_expiry_auto_deactivates_peer_mentor
For HLF: when a peer mentor's certification expires, their peer_mentor role row is automatically set to is_active = false with deactivation_reason = 'certificate_expired'. This removes them from coordinator-facing lists and triggers a push notification. The certificate-expiry-job drives this rule.
role_change_must_be_audited
Every INSERT, UPDATE, and DELETE on user_roles is written to audit_logs via the audit-service with actor_id = assigned_by (or the session user for updates), target_user_id, old_role, new_role, and organization_id. Audit rows are append-only and cannot be modified.
org_admin_cannot_assign_global_admin
Organization admins may only assign peer_mentor, coordinator, or org_admin roles within their own organization. Only global admins may create global_admin rows. Enforced at the API layer before the role-assignment-service is invoked.
expired_roles_treated_as_inactive
When expires_at < NOW(), the role is treated as is_active = false for authorization purposes even if the flag has not yet been flipped by a background job. The route-guard-service and jwt-role-claims-decoder check this condition at runtime to prevent relying solely on a scheduled job.