core PK: id 7 required 1 unique

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.

13
Attributes
6
Indexes
6
Validation Rules
20
CRUD Operations

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
btree

Columns: user_id

idx_user_roles_organization_id
btree

Columns: organization_id

idx_user_roles_user_org_role
btree unique

Columns: user_id, organization_id, role

idx_user_roles_role_active
btree

Columns: role, is_active

idx_user_roles_org_active
btree

Columns: organization_id, is_active

idx_user_roles_expires_at
btree

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
on_create

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
on_create

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
on_create

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
on_create

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
on_update

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
always

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
on_create

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
on_create

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
always

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.

Storage Configuration

Storage Type
primary_table
Location
main_db
Partitioning
No Partitioning
Retention
Permanent Storage