core PK: id 13 required 4 unique

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.

26
Attributes
7
Indexes
9
Validation Rules
24
CRUD Operations

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
btree unique

Columns: email

idx_users_bankid_subject
btree unique

Columns: bankid_subject

idx_users_vipps_subject
btree unique

Columns: vipps_subject

idx_users_status
btree

Columns: status

idx_users_deleted_at
btree

Columns: deleted_at

idx_users_is_global_admin
btree

Columns: is_global_admin

idx_users_last_login_at
btree

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
on_create

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
always

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
always

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
on_update

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
on_delete

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
always

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
always

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
on_create

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
on_update

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
on_create

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.

Storage Configuration

Storage Type
primary_table
Location
main_db
Partitioning
No Partitioning
Retention
Permanent Storage