core PK: id 11 required 2 unique

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.

24
Attributes
10
Indexes
10
Validation Rules
34
CRUD Operations

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

Columns: email

idx_users_organization_id
btree

Columns: organization_id

idx_users_status
btree

Columns: status

idx_users_primary_role
btree

Columns: primary_role

idx_users_org_role
btree

Columns: organization_id, primary_role

idx_users_bankid_subject
btree unique

Columns: bankid_subject

idx_users_vipps_subject
btree unique

Columns: vipps_subject

idx_users_invite_token_hash
btree

Columns: invite_token_hash

idx_users_created_at
btree

Columns: created_at

idx_users_last_login_at
btree

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
on_create

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
always

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
always

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
always

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
on_update

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
on_delete

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
always

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
on_update

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
on_create

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
on_update

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.

Storage Configuration

Storage Type
primary_table
Location
main_db
Partitioning
No Partitioning
Retention
Permanent Storage