User
Data Entity
Description
Core identity record for every human actor in the Meander platform. Stores authentication credentials, profile data, account status, and external identity provider links. Acts as the root anchor for all user-owned data across activities, contacts, expenses, notifications, sessions, and assignments. Supports four roles (peer_mentor, coordinator, org_admin, global_admin) within a multi-tenant PostgreSQL schema where each user belongs to exactly one primary organization.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Globally unique surrogate primary key. Generated server-side (UUIDv4) at creation. Never reused after soft-delete. | PKrequiredunique |
email |
string |
Primary login identifier and communication address. Normalized to lowercase on write. Used for invite flows, password reset, and transactional email dispatch. | requiredunique |
password_hash |
string |
Bcrypt hash (cost 12) of the user's password. NULL for users who authenticated exclusively via BankID, Vipps, or passkey and have never set a password. | - |
first_name |
string |
User's given name. Required for display and for Bufdir report attribution. Set at invite time by admin or populated from BankID/Vipps identity assertion. | required |
last_name |
string |
User's family name. Combined with first_name for display throughout mobile and admin surfaces. | required |
phone_number |
string |
E.164-formatted phone number. Used for SMS notification dispatch and Vipps login association. Populated from Vipps identity response when available. | - |
national_id_encrypted |
string |
AES-256-GCM encrypted Norwegian national identity number (11-digit fødselsnummer). Populated from BankID or Vipps identity assertion. Used to reconcile against member system records. NULL for users who have never authenticated via a national ID provider. | - |
organization_id |
uuid |
Foreign key to the organizations table identifying the user's primary tenant. Determines data isolation scope, label overrides, and enabled module set. Set at invite time and immutable after activation without a global admin action. | required |
primary_role |
enum |
The user's highest-privilege role within their organization. Governs which products (mobile app vs admin portal) and which feature areas are accessible. Detailed per-area authorization is enforced by route guards and API middleware using this field plus organization membership context. | required |
status |
enum |
Lifecycle state of the account. 'invited' means invite sent but not accepted. 'active' is normal operation. 'paused' is a voluntary temporary suspension (peer mentor requests pause; coordinator is notified). 'inactive' is admin-initiated deactivation. 'suspended' is a security-initiated hold pending review. | required |
email_verified |
boolean |
True once the user has clicked the verification link in their invite or email-change email. Email/password login is blocked until this is true. | required |
email_verified_at |
datetime |
UTC timestamp when email_verified was set to true. Audit trail for compliance. | - |
bankid_subject |
string |
Opaque subject identifier returned by the BankID OIDC provider after a successful BankID authentication. Used to match returning BankID sessions to existing user records without re-entering email. Unique per provider; NULL until first BankID login. | - |
vipps_subject |
string |
Opaque subject identifier from Vipps login. Mirrors bankid_subject semantics. NULL until first Vipps login. | - |
avatar_url |
string |
URL to the user's profile image stored in object storage. Relative path resolved through the object storage adapter. NULL if the user has not uploaded a photo. | - |
preferred_language |
enum |
User's preferred display language for the app UI and notification content. Drives locale selection in the mobile app at startup. | required |
invited_by_user_id |
uuid |
Foreign key to users(id) of the admin or coordinator who sent the invite. Used for audit trail and referral attribution. NULL for the first global admin bootstrapped via CLI. | - |
invite_token_hash |
string |
SHA-256 hash of the one-time invite token sent in the invitation email. Cleared (set to NULL) after the user completes registration. Prevents token reuse. | - |
invite_expires_at |
datetime |
UTC expiry for the invite token. Invitations expire after 7 days. After expiry the admin must resend. Cleared alongside invite_token_hash on acceptance. | - |
pause_started_at |
datetime |
UTC timestamp when the user last transitioned to 'paused' status. NULL when status is not 'paused'. Used to calculate pause duration and to surface the paused list to coordinators. | - |
deactivated_at |
datetime |
UTC timestamp of admin-initiated deactivation. NULL when active. Retained permanently for audit compliance; the record itself is never hard-deleted. | - |
last_login_at |
datetime |
UTC timestamp of the user's most recent successful authentication (any method). Used by the security dashboard for anomaly detection and inactive account reporting. | - |
created_at |
datetime |
UTC timestamp of record creation (invite sent or admin-created). Immutable after insert. | required |
updated_at |
datetime |
UTC timestamp of the last modification to any field in this row. Updated automatically via a trigger or ORM hook on every write. | required |
Database Indexes
idx_users_email
Columns: email
idx_users_organization_id
Columns: organization_id
idx_users_status
Columns: status
idx_users_primary_role
Columns: primary_role
idx_users_org_role
Columns: organization_id, primary_role
idx_users_bankid_subject
Columns: bankid_subject
idx_users_vipps_subject
Columns: vipps_subject
idx_users_invite_token_hash
Columns: invite_token_hash
idx_users_created_at
Columns: created_at
idx_users_last_login_at
Columns: last_login_at
Validation Rules
email_format_valid
error
Validation failed
email_globally_unique
error
Validation failed
password_strength
error
Validation failed
phone_e164_format
error
Validation failed
national_id_luhn_check
error
Validation failed
role_assignment_permission
error
Validation failed
name_not_blank
error
Validation failed
status_transition_valid
error
Validation failed
avatar_url_safe_domain
error
Validation failed
global_admin_no_organization
error
Validation failed
Business Rules
invite_only_registration
New users cannot self-register. All accounts must be created by an org_admin or global_admin via the invite flow. The invite_token_hash field is cleared on first login, making the link single-use.
organization_data_isolation
Every query on user-owned data (activities, contacts, expenses, etc.) must include an organization_id scope. The org-scoped-auth-guard middleware injects this scope from the JWT; requests without a matching organization context are rejected with 403.
global_admin_no_org_data
Global admins authenticate without an organization context. They can manage system-level configuration and user accounts but receive no access to any organization's operational data (activities, contacts, expenses) by default. Access must be explicitly escalated and logged.
peer_mentor_mobile_only
Users with primary_role = 'peer_mentor' are denied access to the admin web portal. The route guard redirects them to the no-access screen if they attempt to authenticate against the admin portal.
pause_coordinator_notification
When a peer mentor transitions status to 'paused', the system must notify their coordinator via push notification and/or email before the status write completes. If notification dispatch fails, the pause is still committed but the failure is logged for retry.
soft_delete_only
Users are never hard-deleted from the database. Deactivation sets status = 'inactive' and stamps deactivated_at. All associated sessions are immediately revoked. Deactivated users appear in audit logs and Bufdir reports for the periods when they were active.
national_id_encrypted_storage
The national_id_encrypted field must never be stored in plaintext. Encryption (AES-256-GCM) and decryption occur in the auth-service layer only. The field is excluded from all logging, analytics queries, and non-identity-verification read paths.
role_downgrade_session_invalidation
When primary_role is changed to a less-privileged value (e.g., coordinator → peer_mentor), all active sessions for that user must be revoked immediately so the new role claims take effect at next login.
invite_expiry_enforcement
An invite with invite_expires_at in the past cannot be used to activate an account. The auth-service checks this before accepting the registration form submission. The admin must resend a fresh invite.
single_primary_organization
Each user row is pinned to exactly one organization_id (their primary tenant). Membership in additional organizations is represented in the organization_memberships table. The organization_id on the users row is immutable after account activation without a global_admin action plus an audit log entry.