Account & Access Model

Account & Access Model

How accounts, organisations, teams, users, roles, and patients relate. The full vision (with branches and multi-org) and the MVP subset. Designed so the MVP schema doesn't lock us out of multi-tenant growth.

1. Account Hierarchy · Full Vision

The PM has documented four use cases with progressively richer hierarchies. The MVP only ships the simplest two; the schema accommodates all of them from day one to avoid a rewrite when growth comes.

Diagram · Account hierarchy across use cases
graph TB subgraph UC1["Use Case 1 · Single User (HCP) · MVP"] direction TB A1[Account · 1 user = admin = contract owner] P1A[Patient] P1B[Patient] A1 --> P1A A1 --> P1B end subgraph UC2["Use Case 2 · Group Practice / Facility · MVP if simple"] direction TB A2[Account · Facility] Admin2[Facility Admin · contract owner] Lead2A[Team Lead · Cardiology] Lead2B[Team Lead · Internal Med] U2A[User · Dr. Bauer] U2B[User · Dr. Hoffmann] U2C[User · Dr. Meier] A2 --> Admin2 A2 --> Lead2A A2 --> Lead2B Lead2A --> U2A Lead2A --> U2B Lead2B --> U2C end subgraph UC3["Use Case 3 · Org with Branches · Out of MVP"] direction TB A3[Account · Legal Entity] BAdmin1[Branch Admin · Munich] BAdmin2[Branch Admin · Berlin] Team3A[Team] Team3B[Team] A3 --> BAdmin1 A3 --> BAdmin2 BAdmin1 --> Team3A BAdmin2 --> Team3B end subgraph UC4["Use Case 4 · Hilo Internal · MVP"] HAdmin[Hilo Admin] Sales[Sales / Demo] Support[Customer Support] end style A1 fill:#1F3020,stroke:#5FA377,color:#dee2e6 style A2 fill:#1F3020,stroke:#5FA377,color:#dee2e6 style A3 fill:#1a1d22,stroke:#374151,color:#6b7280 style HAdmin fill:#2B1539,stroke:#9066B8,color:#dee2e6

2. Schema Sketch

Postgres-flavored. Multi-tenant from day one via org_id; team scoping via team_id. Even when MVP runs single-org / single-team, every row has the column populated. Adding multi-tenancy later becomes an admin tool, not a schema migration.

-- Tenant root
orgs (
  id UUID PK,
  name TEXT,
  type TEXT CHECK (type IN ('single_user', 'group_practice', 'org_with_branches')),
  contract_owner_user_id UUID,    -- nullable for hilo-internal accounts
  created_at TIMESTAMPTZ,
  -- billing/contract metadata, region, etc.
)

-- Optional team grouping inside an org (used by group_practice + org_with_branches)
teams (
  id UUID PK,
  org_id UUID FK orgs(id),
  name TEXT,
  parent_team_id UUID FK teams(id),  -- supports branches later
  created_at TIMESTAMPTZ,
)

-- Identity provider issues the JWT; we mirror the user record for joins
users (
  id UUID PK,
  org_id UUID FK orgs(id),
  team_id UUID FK teams(id),         -- nullable
  external_id TEXT UNIQUE,           -- Auth0 / Entra ID user id
  email TEXT,
  full_name TEXT,
  created_at TIMESTAMPTZ,
)

-- Roles are global + scoped via the assignment
roles (
  id TEXT PK,                        -- 'org_admin', 'team_lead', 'user', 'hilo_admin', 'support'
  description TEXT,
)

user_roles (
  user_id UUID FK users(id),
  role_id TEXT FK roles(id),
  scope_org_id UUID,                 -- for org-scoped roles
  scope_team_id UUID,                -- for team-scoped roles
  PRIMARY KEY (user_id, role_id, scope_org_id, scope_team_id),
)

-- Patient is the subject of monitoring; assignment is the link
patients (
  id UUID PK,
  org_id UUID FK orgs(id),           -- which org "owns" this patient record
  external_b2c_id TEXT,              -- link to canonical B2C record
  full_name TEXT,
  email TEXT,
  date_of_birth DATE,
  created_at TIMESTAMPTZ,
)

device_assignments (
  id UUID PK,
  org_id UUID FK orgs(id),
  patient_id UUID FK patients(id),
  device_id TEXT,                    -- references B2C device
  assigned_to_user_id UUID FK users(id),
  assigned_at TIMESTAMPTZ,
  unassigned_at TIMESTAMPTZ,         -- nullable
  status TEXT CHECK (status IN ('active', 'paused', 'inactive')),
)

-- Consent record (legal anchor for patient-managed flow)
patient_consent (
  id UUID PK,
  patient_id UUID FK patients(id),
  org_id UUID FK orgs(id),
  scope JSONB,                       -- what the consent covers
  granted_at TIMESTAMPTZ,
  revoked_at TIMESTAMPTZ,
  evidence JSONB,                    -- token, IP, user agent at grant time
)

-- Audit log · append-only · no UPDATE/DELETE grants
audit_log (
  id BIGSERIAL PK,
  ts TIMESTAMPTZ NOT NULL DEFAULT now(),
  actor_user_id UUID,
  actor_org_id UUID,
  event_type TEXT,                   -- 'patient.read', 'export.csv', 'role.change', ...
  subject_id UUID,
  payload JSONB,
  request_id TEXT,
  ip TEXT,
  user_agent TEXT,
)
CREATE INDEX audit_log_actor_idx ON audit_log (actor_user_id, ts DESC);
CREATE INDEX audit_log_subject_idx ON audit_log (subject_id, ts DESC);

3. Role Matrix · MVP Subset

The PM's full role matrix has many TBD cells; the MVP subset below is what we plan to actually ship.

Role Scope Sees patient data of Can assign devices Can manage users Can export MVP?
Single User / Admin Org (own) All patients in org Yes N/A (only user) Yes In
Org Admin (group practice) Org All patients in org Yes Yes Yes Stretch
Team Lead Team Patients assigned to team users Within team Within team Within team Stretch
User (clinician) Self Patients assigned to self Self only No Self only In
Branch Admin Branch (sub-org) Branch patients Within branch Within branch Within branch Out
Hilo Admin Global (internal tooling — not in dashboard MVP) Phase 2
Hilo Support Per-org (impersonation) Read-only on assigned org with audit trail No No No Phase 2

4. RBAC Enforcement Pattern

Every API handler is wrapped in middleware that:

  1. Validates the JWT against the IdP (Auth0 / Azure AD B2C JWKS)
  2. Loads the user record and their roles
  3. Checks the requested resource against role scope (org_id, team_id)
  4. Records an audit log entry with actor + subject
  5. Calls the handler with a scoped DB query helper
// pseudo-code
app.get('/api/patients/:id', requireAuth, requireScope('patient:read'), async (req, res) => {
  const patient = await db.scoped(req.user).patients.findById(req.params.id);
  if (!patient) return res.status(404).end();
  await audit.log(req, 'patient.read', { patient_id: patient.id });
  res.json(patient);
});

Critical property: db.scoped(req.user) automatically filters by org_id (and team_id when role is team-scoped). Cannot accidentally leak across orgs.

5. Patient Consent Flow (Patient-Managed mode — Phase 2)

Architecturally specced for Phase 2; not built for May 28th. Documented now to lock the schema decisions.

  1. Doctor adds patient (name + email) in the dashboard
  2. System creates a patient_consent row in pending state
  3. Email with signed link sent to patient (signed = HMAC of consent_id + expiry)
  4. Patient clicks link → consent page rendered → terms displayed → patient grants or declines
  5. System records granted_at with evidence JSONB (IP, UA, token used)
  6. Audit log entry written
  7. From this point, B2B service can read patient data via the consent gate
  8. Patient can revisit a self-service revocation page at any time → revoked_at set → access removed
Legal blocker. The consent terms wording, retention period for consent records, evidence fields, and revocation cascade behavior all need Hilo legal sign-off. Cannot ship to real patients without that. See Risk R-03.

6. Open Questions from PM's Doc

QuestionWhy it blocksOwner
Is full account management approach in MVP scope?Determines if we ship UI for org / team managementPM
Who is allowed to see private (PII) data?Affects field-level access in API serializerLegal
How do we manage online order bundles (devices + subscription + dashboard fee)?Determines whether Shopify or backoffice owns thisPM + tech
When is the customer account created? (after contract / order / payment / approval)Determines when DB rows are inserted, when emails firePM
How is the first admin created? (Hilo / checkout / activation link / SSO later)Determines auth bootstrap flowPM + tech
Edit rights / view rights / data view rights splitDetermines fine-grained permissions vs role-only accessLegal + PM

7. The Account-Based vs Device-Based Decision (resolved)

The PM doc explicitly leans account-based access — access rights and duration live on the user account, the bracelet is just hardware. This is the right call.

Pros: cleaner subscription model, clearer ownership, stronger control over transfer/abuse, closer to WHOOP model, simpler revoke semantics, easier to support multi-patient bands later (just re-assign the device — access stays with whoever the patient assigned it to).

Cons (and our mitigations):