User
Data Entity
Description
Core identity record for every human actor in the Meander platform. Represents Peer Mentors, Coordinators, Organization Administrators, and Global Administrators across all three products. Stores authentication credentials, profile data, and account lifecycle state. Organization membership and role assignment are managed through the user_roles join table; this entity holds only identity and auth data.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Surrogate primary key, generated server-side on creation | PKrequiredunique |
email |
string |
User's email address. Used as login identifier for email/password flow and as delivery address for notifications. Must be globally unique across the platform. | requiredunique |
password_hash |
string |
Bcrypt hash of the user's password (cost factor ≥12). NULL when the user's only auth method is BankID or Vipps. | - |
first_name |
string |
User's given name. Displayed throughout the UI and included in notification messages. | required |
last_name |
string |
User's family name. | required |
phone_number |
string |
E.164-formatted mobile phone number. Required for SMS notifications and BankID/Vipps verification flows. Optional at account creation but required before first BankID/Vipps login attempt. | - |
national_id_encrypted |
string |
AES-256-GCM encrypted Norwegian national identity number (fødselsnummer / personnummer) returned by Vipps or BankID during initial login. Stored encrypted; decryption key held in KMS. Used to backfill missing national IDs in member systems. | - |
profile_photo_url |
string |
Public URL to the user's avatar image stored in cloud object storage. Set via avatar upload flow in the mobile app. | - |
preferred_language |
enum |
User's preferred UI language. Drives localization in both the mobile app and admin portal. | required |
status |
enum |
Lifecycle state of the user account. 'paused' means voluntarily inactive (peer mentor can resume); 'inactive' means deactivated by an admin; 'pending_verification' means invited but not yet onboarded. | required |
primary_auth_provider |
enum |
The authentication provider used to first create this account. Determines which credential set is canonical. | required |
bankid_subject |
string |
Unique subject identifier returned by the BankID OIDC provider. Used to match returning BankID logins to existing user records. NULL until the user completes a BankID login. | unique |
vipps_subject |
string |
Unique subject identifier returned by the Vipps OIDC provider. NULL until the user completes a Vipps login. | unique |
email_verified |
boolean |
Whether the user has confirmed ownership of their email address via the verification link sent on invitation or email change. | required |
biometric_enabled |
boolean |
Whether the user has enabled biometric (Face ID / fingerprint) session authentication on their device. Device-level biometric state is managed by the mobile OS; this flag reflects user intent. | required |
passkey_enabled |
boolean |
Whether the user has registered at least one passkey credential (WebAuthn). Passkey credentials are stored in passkey_credentials_repository; this flag is a denormalized fast-check. | required |
is_global_admin |
boolean |
Indicates Norse Digital Products staff with cross-organization system access. Global admins do not appear in any organization's user list and cannot access org operational data by default. | required |
invitation_token_hash |
string |
Bcrypt hash of the one-time invitation token sent to the user's email. Cleared to NULL after the user completes onboarding. Used only during pending_verification state. | - |
invitation_expires_at |
datetime |
Timestamp when the invitation token expires. Invitations are valid for 7 days. NULL after onboarding is complete. | - |
onboarded_at |
datetime |
Timestamp when the user successfully completed onboarding (set password / linked BankID / Vipps and accepted terms). NULL for pending_verification accounts. | - |
last_login_at |
datetime |
Timestamp of the most recent successful authentication event. Updated on every login regardless of provider. | - |
pause_reason |
text |
Optional free-text reason provided by the peer mentor when entering the paused state. Visible to their coordinator. | - |
paused_at |
datetime |
Timestamp when the account entered paused state. NULL if not currently paused. | - |
deleted_at |
datetime |
Soft-delete timestamp. NULL for active records. When set, the user is excluded from all queries via application-layer filtering. Hard deletion is never performed. | - |
created_at |
datetime |
Record creation timestamp, set server-side on INSERT. | required |
updated_at |
datetime |
Timestamp of the most recent UPDATE to this row. Maintained by a database trigger. | required |
Database Indexes
idx_users_email
Columns: email
idx_users_bankid_subject
Columns: bankid_subject
idx_users_vipps_subject
Columns: vipps_subject
idx_users_status
Columns: status
idx_users_deleted_at
Columns: deleted_at
idx_users_is_global_admin
Columns: is_global_admin
idx_users_last_login_at
Columns: last_login_at
Validation Rules
email_format
error
Validation failed
email_uniqueness
error
Validation failed
password_strength
error
Validation failed
phone_e164_format
error
Validation failed
name_non_empty
error
Validation failed
status_transition_guard
error
Validation failed
profile_photo_https_url
error
Validation failed
preferred_language_allowed_values
error
Validation failed
global_admin_no_org_roles
error
Validation failed
Business Rules
invite_only_registration
Users cannot self-register. Every user account is created by an Org Admin or Global Admin via invitation. The user-invitation-service generates a time-limited token and sends the onboarding email; the account starts in pending_verification status.
organization_context_required
Every non-global-admin user must have at least one row in user_roles linking them to an organization before the account is considered fully active. The auth-service rejects login from accounts with status 'pending_verification' or with no active role assignment.
paused_user_activity_block
A user with status 'paused' may log in and view their data but cannot create new activities, events, or register expenses. The mobile app enforces this via rbac-service permission checks on write operations.
pause_coordinator_notification
When a user transitions to 'paused' status, the scenario-engine-service triggers a notification to the user's coordinator(s) within the same organization.
soft_delete_only
Users are never hard-deleted from the database. Deactivation sets deleted_at and status='inactive'. All queries filter WHERE deleted_at IS NULL. This preserves referential integrity with activities, expenses, audit_logs, and other child records.
global_admin_tenant_isolation
Users with is_global_admin=true have no user_roles rows and cannot read or write any organization's operational data (activities, contacts, expenses) through normal API paths. Global admin operations are limited to system management endpoints.
national_id_encryption
The national_id_encrypted field must never be stored or logged in plaintext. All writes encrypt with AES-256-GCM before persistence; reads decrypt in-memory only for the duration of the API request. The field is never returned to client applications.
invitation_expiry
An invitation token is valid for 7 days. If onboarding is not completed within this window, the account remains in pending_verification and the token is rejected. Org Admins can re-send the invitation, which generates a new token and resets invitation_expires_at.
credential_consistency
At least one valid authentication method must exist for every active user: either password_hash is non-null, bankid_subject is non-null, or vipps_subject is non-null. Removing the last auth method is blocked.
audit_all_mutations
Every INSERT, UPDATE, or soft-delete on the users table must produce an audit_logs entry recording the actor (user or admin), action type, changed fields, and timestamp. Enforced by the audit-service which is called by all write-path services.