User Management
Tenancy and User Management
Health Assistant is designed to be "Home-First, Clinic-Ready." This document explains how the system handles multiple users, organizational hierarchies, and identity linking.
1. The Data Model
Health Assistant separates isolation boundaries (Tenant), organizational structure (Organization, recursive), and clinical identities (Patient, Doctor) into distinct peer entities rather than a strict containment hierarchy.
Tenant — The Isolation Boundary
The Tenant is the absolute boundary for data isolation.
- Home Use: A Tenant represents the entire family or a single installation.
- Clinic Use: A Tenant represents a Medical Group or a SaaS customer.
- Isolation: Users in Tenant A can never see data in Tenant B.
Organization — Recursive Grouping
An Organization represents a physical or logical grouping within a Tenant. Organizations are recursive via a self-referential part_of_id foreign key (FHIR Organization.partOf), so a tree of arbitrary depth can be modeled — there is no fixed number of tiers.
- Household: In home setups, the system automatically creates a "Default Household" organization. This serves as the primary container for family members.
- Clinic/Hospital: Represents a specific branch or facility.
- Department: A Department is simply an Organization whose
part_of_idpoints to a parent Organization (typically of typeCLINICorHOSPITAL). It is not a separate model or table. - Org Types:
HOUSEHOLD,CLINIC,HOSPITAL,DEPARTMENT,PROVIDER_GROUP,OTHER.
Clinical Identities — Peer Entities, Not Contained
Users, Patients, and Doctors are tenant-scoped peers; they are not nested inside Organizations or Departments:
- A User (login account) belongs to exactly one Tenant and has no direct link to any Organization or Department.
- A Patient belongs to a Tenant and may optionally link to one User via
user_id. - A Doctor belongs to a Tenant, may optionally link to one User via
user_id, and is associated to Organizations (including Departments) many-to-many via theorganization_doctorsassociation table. - Examinations carry an optional
organization_id, so they can be grouped by Department (e.g., "Cardiology", "Dental").
Cardinalities
| Relationship | Cardinality | Mechanism |
|---|---|---|
| User → Patient | 1 : N | Patient.user_id (soft, nullable, not unique) |
| User → Doctor | 1 : N | Doctor.user_id (soft, nullable, not unique) |
| Organization → child Organization | 1 : N | Organization.part_of_id (self-ref) |
| Department ↔ Doctor | N : N | organization_doctors association |
| Organization → Examination | 1 : N | Examination.organization_id |
2. Roles and Permissions
Health Assistant implements a dual-scope Role-Based Access Control (RBAC) system.
| Role | Scope | Description |
|---|---|---|
| SYSTEM_ADMIN | Global | The system owner. Can manage all tenants, global AI configurations, and system-wide settings. The first user registered in a new installation is automatically promoted to this role. |
| ADMIN | Tenant | The Tenant owner (e.g., Head of Household or Clinic Manager). Can manage users and organizations within their own tenant. |
| MANAGER | Tenant | Administrative staff or senior caregivers. Can view all data in the tenant and manage clinical records. |
| USER | Tenant | Standard user. Access is typically restricted to their own records unless granted wider permissions. |
3. Identity Linking
A unique feature of Health Assistant is the separation and linking of Identity (who logs in) and Clinical Records (the data).
Why Link?
In a household, the person who manages the app (the User) is often also a patient (the Patient record) or a caregiver (the Doctor record). In a clinic, a doctor's login maps to their professional Doctor profile. Linking allows the system to:
- Automatically show "My Dashboard" when you log in (when a single Patient is linked).
- Associate login accounts with professional licenses for doctors.
How it Works — One-to-Many
Linking is one-to-many, not one-to-one:
DoctorModelandPatientboth contain an optionaluser_idforeign key. Neither column is unique-constrained, so a single User can be linked to multiple Patient records and multiple Doctor records simultaneously (e.g., a parent who is both a patient and a caregiver for several family members).- When a
user_idis present on a clinical record, that record is "owned" by the login account. - Home Setup: Usually, you will create a Patient record for yourself and link it to your User account. A caregiver may additionally create Doctor records for themselves and link them.
Unlinking:
- Links can be removed at any time from the User Detail page.
- Unlinking removes the connection but preserves the clinical data. The Patient or Doctor record remains in the database, just without an owner.
4. Smart Patient Context
Because one User may be linked to multiple Patient records (see §3), the system needs a way to decide which patient's data to display. Health Assistant uses a "Smart Context" engine:
- Automatic Context: If a User is linked to exactly one Patient record, that patient is automatically selected as the "active context" upon login.
- Single-Patient Default: In new home installations with only one patient, that patient is selected by default.
- Secure Switching: If a User is linked to two or more patients (or an Admin manages multiple patients), they must manually select the context. The system will never show data from a random patient.
- Session Cleanup: Logging out explicitly wipes the patient context from the browser to prevent data residue for the next user.
5. Workflows
Home Setup (Zero-Config)
- Register: The first user registers via
/auth/registerwithout supplying atenant_id. - Auto-Provision: The system detects this is the first user and:
- Assigns them the
SYSTEM_ADMINrole. - Creates a new
Tenantautomatically. - Creates a "Default Household"
Organizationautomatically.
- Assigns them the
- Ready: The user can immediately start adding family members as Patients.
Subsequent self-onboarding users (e.g. another household setting up their
own installation) follow the same path: they omit tenant_id, get their
own new tenant, and become ADMIN of it. Only the very first registration
in the entire database becomes SYSTEM_ADMIN — this check is
race-protected by a Postgres advisory lock so two concurrent bootstrap
registrations cannot both promote.
Joining an Existing Tenant (Invite Token Required)
Anyone who knows a tenant_id cannot register inside that tenant — that
would be a tenant-impersonation hole. Joining an existing tenant requires
a short-lived invite token minted by that tenant's admin.
- Admin mints invite —
POST /api/v1/auth/invite(ADMIN, MANAGER, or SYSTEM_ADMIN only). The admin can optionally bind the token to a specific email, choose a role (USER/ADMIN/MANAGER), and set an expiry (default 7 days).SYSTEM_ADMINis never grantable via invite — that role is bootstrap-only by design.curl -X POST https://your-host/api/v1/auth/invite \ -H "Authorization: Bearer $ADMIN_JWT" \ -d "email=newmember@family.com" -d "role=USER" # → { "invite_token": "...", "tenant_id": "...", "role": "USER", "expires_in_days": 7 } - Invitee registers —
POST /api/v1/auth/registerwith bothtenant_idandinvite_token. The server verifies the token's signature, expiry, tenant binding, and (if set) email binding.curl -X POST https://your-host/api/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"newmember@family.com", "password":"...", '\ '"tenant_id":"...", "invite_token":"..."}' - Failure modes (all return 403):
- Missing
invite_tokenfield. - Token signature invalid (tampered) or expired.
- Token's
tenant_id≠ request'stenant_id. - Token bound to a different email than the request.
- Missing
Clinical Setup
- Tenant Admin: Registers via the bootstrap path (becomes
ADMINof their new tenant) OR is invited by aSYSTEM_ADMINcross-tenant. - Create Facilities & Departments: The Admin creates Organizations of
type
CLINICorHOSPITAL, then creates child Organizations of typeDEPARTMENTwithpart_of_idpointing to the parent facility. - Invite Staff: Admin mints invite tokens (see above) for doctors and staff, who join as tenant-level Users. Note: Users themselves are not assigned to Departments — see step 4.
- Link Doctors & Assign to Departments: Clinical
Doctorrecords are created, linked to the staff'sUseraccounts viauser_id, and associated to one or more Departments via theorganization_doctorsassociation table (many-to-many).
6. CLI Administration
For system administrators, a command-line script is provided to manage the initial setup and promote users.
Create or Promote System Admin
To create a new system administrator or promote an existing user:
python backend/scripts/create_system_admin.py --email admin@example.com --password "password"Arguments:
--email: The email address for the account.--password: The password (only used if creating a new user).--tenant: Optional name for the initial tenant if one needs to be created.
7. Data Deletion Semantics
The schema enforces cascading deletes so data cannot be orphaned.
Tenant Deletion
Deleting a Tenant row CASCADEs to all tenant-owned tables via
tenant_id foreign keys (ON DELETE CASCADE). This includes users,
patients, observations, medications, examinations, documents, alerts,
notifications, AI config, chat sessions, etc.
Exception: telemetry_data is a TimescaleDB hypertable where FK
constraints aren't reliably supported. The tenant_id column has no FK;
a periodic cleanup job is responsible for purging telemetry rows after
their tenant is deleted.
Patient Deletion
Deleting a Patient CASCADEs to their entire clinical record:
medications, allergies, clinical events, examinations, documents,
devices, chat sessions, alerts, notifications, layouts, and
user integrations. No patient-owned row is orphaned.
Soft-Delete (FHIR Facade)
The 9 FHIR-exposed models (Patient, Observation, DiagnosticReport,
Medication, AllergyIntolerance, Organization, Examination,
ClinicalEvent, Document) mix in SoftDeleteMixin. The FHIR facade
sets deleted_at = now() instead of hard-deleting; subsequent reads
return 410 Gone (not 404 Not Found) so callers can distinguish
"never existed" from "was deleted".