Users
Data Entity
Description
Central identity entity representing all authenticated individuals on the Meander platform — peer mentors, coordinators, organization administrators, and global administrators. Stores personal profile data, authentication metadata, organization membership, and account lifecycle state. Every authenticated action across all three products (mobile app, admin portal, sales website) resolves back to a users record.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Unique identifier for the user, generated server-side on account creation | PKrequiredunique |
email |
string |
User email address, used as the primary login credential for email/password authentication and for system notifications | requiredunique |
password_hash |
string |
Bcrypt-hashed password for email/password authentication. Null when user authenticates exclusively via BankID/Vipps/passkey | - |
first_name |
string |
User first name, displayed in profile and used in notifications and reports | required |
last_name |
string |
User last name, displayed in profile and used in reports | required |
phone |
string |
Phone number in E.164 format, used for SMS notifications and Vipps identity matching | - |
personnummer |
string |
Norwegian national identity number (11 digits), obtained via BankID/Vipps login. Encrypted at rest. Used for identity verification and member system synchronization | unique |
organization_id |
uuid |
Foreign key to organizations table. Determines tenant context. Set during onboarding by admin — users do not choose their organization at login | required |
status |
enum |
Account lifecycle state. Controls login access and visibility in member lists | required |
auth_provider |
enum |
Primary authentication method used to create the account | required |
bankid_subject_id |
string |
BankID unique subject identifier from OAuth identity token, used for account linking on subsequent BankID logins | unique |
vipps_subject_id |
string |
Vipps Login unique subject identifier from ID token, used for account linking on subsequent Vipps logins | unique |
biometric_enabled |
boolean |
Whether the user has opted into biometric authentication (Face ID/fingerprint) on at least one device | required |
passkey_enabled |
boolean |
Whether the user has registered at least one FIDO2 passkey credential | required |
language |
string |
Preferred UI language locale code (e.g., nb-NO, se-NO for Northern Sami) | required |
invitation_token |
string |
Time-limited token sent via invitation email. Null after account activation | unique |
invitation_expires_at |
datetime |
Expiry timestamp for the invitation token | - |
password_reset_token |
string |
Time-limited token for password reset flow. Null when not in use | unique |
password_reset_expires_at |
datetime |
Expiry timestamp for the password reset token | - |
roles_updated_at |
datetime |
Timestamp of the most recent role assignment change. Used by JWT validation to detect stale role claims and force token refresh | required |
last_login_at |
datetime |
Timestamp of the user's most recent successful authentication | - |
paused_at |
datetime |
Timestamp when a peer mentor set themselves to paused status. Null if not paused. Coordinator is notified on pause | - |
deactivated_at |
datetime |
Timestamp when the account was deactivated by an admin. Null if active | - |
avatar_url |
string |
URL to the user's profile photo stored in cloud storage | - |
created_at |
datetime |
Timestamp of account creation (invitation sent) | required |
updated_at |
datetime |
Timestamp of last profile or status update | required |
Database Indexes
idx_users_email
Columns: email
idx_users_organization_id
Columns: organization_id
idx_users_organization_status
Columns: organization_id, status
idx_users_personnummer
Columns: personnummer
idx_users_bankid_subject_id
Columns: bankid_subject_id
idx_users_vipps_subject_id
Columns: vipps_subject_id
idx_users_invitation_token
Columns: invitation_token
idx_users_status
Columns: status
idx_users_last_login_at
Columns: last_login_at
idx_users_created_at
Columns: created_at
Validation Rules
email_format_and_uniqueness
error
Validation failed
phone_e164_format
error
Validation failed
personnummer_checksum
error
Validation failed
name_fields_non_empty
error
Validation failed
valid_status_transitions
error
Validation failed
organization_must_exist
error
Validation failed
invitation_token_expiry
error
Validation failed
password_policy
error
Validation failed
language_locale_valid
error
Validation failed
avatar_url_format
warning
Validation failed
Business Rules
organization_context_from_invitation
Users do not choose their organization at login. Organization context is set during onboarding when an admin invites the user, and cannot be changed after account creation
tenant_isolation
All queries for user data must be scoped by organization_id. Users in one organization cannot see or modify users in another organization. Global admins manage the system but do not have default access to org operational data
pause_does_not_deactivate
Peer mentors can pause their status (temporarily deactivate) without leaving the program. Paused users retain their account but are removed from active member lists and coordinator dashboards. Coordinator must be notified on pause
invitation_flow_required
New users are created exclusively via admin invitation (status=invited). Self-registration is not supported. Users must complete the invitation flow (accept token, set password or link BankID/Vipps) before status transitions to active
roles_updated_at_invalidates_jwt
When a user's role assignments change, roles_updated_at is bumped. JWT validation middleware compares this timestamp against the token's issued-at claim to force token refresh, ensuring stale role claims are never honoured
deactivated_users_cannot_login
Users with status=deactivated are blocked from all authentication flows. Active sessions and refresh tokens are revoked on deactivation
single_organization_membership
Each user belongs to exactly one organization. Multi-organization access is handled via the organization hierarchy (parent-child), not via multiple user records
personnummer_encrypted_at_rest
The personnummer field contains sensitive PII (Norwegian national ID) and must be encrypted at the database column level. Only authentication and identity verification services may decrypt it
account_linking_prevents_duplicates
When a user authenticates via BankID or Vipps and the returned personnummer or subject ID matches an existing user, the identity is linked to the existing account rather than creating a duplicate
bulk_operations_require_audit
All bulk user operations (deactivation, role reassignment, invitation resend) must create individual audit log entries per affected user within the same transaction