|
Documentation: Admin access, tenants and users

Operate

Admin access, tenants and users

How the first owner is created, what each role can do, and how registration, invites and policies work.

Create the tenant (Firestore)

There is no REST endpoint to create a tenant. In the Firebase Console open Firestore, create (or reuse) the tenants collection, and add a document whose ID is the tenant ID: a stable, lowercase, kebab-case slug referenced by every user, study and inference record. Repeat per tenant for multi-tenant installs.

Field Type Example Notes
id string acme-pacs Same as the document ID
name string Acme Cardiology Hospital Display name shown in the viewer
address string 123 Main St, Montreal, QC Free-form
onboarding_questionnaires string empty Optional JSON blob
onboarding_enable_registration bool false Self-serve public registration toggle
onboarding_enable_consent bool false Consent-flow toggle
onboarding_consent_link string empty URL shown during the consent step
created_at, updated_at int 1715000000 Unix epoch seconds

Create the first owner (superuser endpoint)

Create the admin user in Firebase Console (Authentication, Users, Add user) with a temporary password communicated out of band, and note the UID Firebase generates for later reconciliation. Then bind that email to the tenant as its owner with the privileged endpoint. It is gated by the FirebaseSuperUserGuard middleware and authenticated with the FIREBASE_SUPERUSER_KEY value sent in the X-FB-SUDO-KEY header (OpenAPI security scheme firebaseSudoKey; all other routes use bearerAuth, a session JWT). The role must be exactly OWNER in upper case: the handler compares against the OWNER constant and returns 403 with "Role must be owner." for anything else. The handler creates the api-pacs record bound to that Firebase email and returns the generated initial password, or 409 if the user already exists. The admin then completes a first login, which creates the user record on the api-pacs side.

curl -X POST https://api.<customer-domain>/v1/user/owner/add \
  -H "X-FB-SUDO-KEY: $FIREBASE_SUPERUSER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId":  "acme-pacs",
    "role":      "OWNER",
    "name":      "Alice Admin",
    "email":     "[email protected]",
    "licenseNo": "MD12345",
    "specialty": "Cardiology"
  }'

Roles

Role Scope
OWNER Full control over the tenant; created at install through POST /v1/user/owner/add; can add or remove ADMIN and USER members. Enforced by RBACOwnerGuard.
ADMIN Manages users, ingestion jobs and inference models; cannot transfer or delete the tenant. Enforced together with owners by RBACOwnerOrAdminGuard.
USER Clinician or regular user: views studies, inference results and the worklist; cannot change tenant configuration or membership. Manual reprocessing stays admin-only.

Every request carries a Firebase ID token verified by api-pacs; sessions are tracked in Redis; every record is scoped to tenant_id and cross-tenant reads are blocked at the application layer (a cross-tenant lookup by study UID returns 404; no role or key exempts a caller from tenant scoping on study lookups). Public registrations are always created as USER; the role is server-owned and never client-chosen.

Tenant, user and account endpoints (/v1)

Endpoint Purpose
GET /tenant, GET /tenant/public?tenantId= Tenant record for the caller; public tenant metadata used by the login and registration pages
PUT /tenant/onboarding-registration/config/update Toggle onboardingEnableRegistration for self-serve sign-up
PUT /tenant/onboarding-consent/config/update Consent flow configuration
POST /tenant/onboarding-questionnaire-answers/add, GET /tenant/onboarding-questionnaire-answers?questionnaireType= General onboarding questionnaires (PRE_SURVEY, POST_SURVEY)
POST /inference/onboarding-model-questionnaire-answers/add, GET /inference/onboarding-model-questionnaire-answers?modelId= Answers to the scored per-model bias and fairness quizzes
POST /user/tutorial/reset Deletes the user general and model-specific questionnaire answers so onboarding replays (idempotent)
POST /user/add Create a tenant user (role, name, email, licenseNo, specialty)
POST /user/invite, POST /user/invite/resend, GET /user/invites, DELETE /user/invite/{id}/remove Email invitations; a registration that carries the invite code is created already verified
GET /user/all, PUT /user/update, DELETE /user/{id}/remove List, update and remove members
POST /user/{id}/suspend, POST /user/{id}/reactivate Account suspension with immediate session revocation, and reactivation
GET /user/me, GET /user/metadata, PUT /user/metadata/update, PUT /user/password/update User self-service
POST /iam/login, POST /iam/verify-email, POST /iam/forgot-password Session login, verification-email resend, password reset
GET /user/policies/registration, POST /user/policies/accept, GET /user/policies/status, GET /user/{ID}/policies/status Versioned Terms and Privacy acceptance: registration metadata, acceptance, own status, and administrator audit status
GET /user/specialties Doctor specialties offered at registration
GET /ecs/logs?index&query&startDate&endDate&export Search or export audit indices without opening Kibana
PATCH /ecs/kibana/indices/sync Superuser-only (X-FB-SUDO-KEY): sync Kibana index patterns
GET /debug/vars (server root, not under /v1) Go expvar metrics as JSON, behind the same basic-auth gate as the OpenAPI docs (user sudo, password OPENAPI_DOCS_PASSWORD)
POST /lead/contact-form, POST /lead/subscribe The contact form and newsletter endpoints used by pacsai.co

Public registration and login

  1. POST /v1/user/register accepts email, a single name (1 to 100 characters), licenseNo, specialty, password, the exact current policy versions and a single-use turnstileToken; unknown fields such as a password confirmation are rejected with 400. Input is normalized and strictly validated (field lengths, password strength, body size, unknown fields, trailing JSON). Registration is only open when the tenant has onboarding_enable_registration set.
  2. Turnstile is verified with Cloudflare Siteverify before any tenant lookup or account creation; invalid, expired or replayed tokens return HTTP 400 TURNSTILE_INVALID, Cloudflare failures return HTTP 503 CLOUDFLARE_API_ERROR.
  3. Attempts are throttled per tenant, per email and per trusted IP (HTTP 429). If the downstream Firestore profile write fails, the newly created Firebase identity is deleted.
  4. The account is created unverified and a verification email is sent through Mailgun; login enforces email verification (FIREBASE_AUTH_EMAIL_NOT_VERIFIED) and resend requests have a 60-second cooldown (POST /v1/iam/verify-email).
  5. POST /v1/iam/login takes tenantId, email, password and an optional turnstileToken. api-pacs calls Firebase Identity Toolkit signInWithPassword, verifies the ID token with the tenant Admin SDK, discards Firebase tokens and creates only the PACS Redis session. Passwords and tokens are never logged.
  6. This contract replaced the earlier {tenantId, idToken} login. It is a breaking change: deploy the paired viewer and backend together (or in a maintenance window) and do not keep the old ID-token route, because it bypasses adaptive enforcement.
  7. Failures use hashed tenant, account and IP counters. Account failures require Turnstile but never hard-lock an email; IP and tenant counters first challenge and later return HTTP 429 with Retry-After. Redis failure returns LOGIN_PROTECTION_UNAVAILABLE (503); Siteverify transport or configuration failures return CLOUDFLARE_API_ERROR (503) without poisoning the counters.
  8. Current policy acceptance is enforced on inference, orchestrator, Orthanc and DICOMweb routes, with an optional grace deadline for existing users.

Audit events (Elasticsearch)

Event Index Notable fields
User login logins session_id, role, specialty
Admin invite sent admin_invites invitee email
User management admin_members action (CREATE, UPDATE, DELETE), target user, role, licence
Remote-PACS query (C-FIND) find_modality_studies modality_id, query_id
Study retrieved (C-MOVE) retrieved_studies study_instance_uid, modality_id
Inference run predict_inference_models inference_model_id, docker_image, model, study_instance_uid, series_instance_uids, additional_metadata
Consent signed signed_consents tenant and user
Custom series stored stored_custom_series study_instance_uid, series_instance_uids, custom_series_instance_uid, custom_sop_instance_uid, model_name, model_version, patient_id

Every event carries tenant_id, tenant_name, user_id, email, name and a Unix timestamp. Customers run audit queries in Kibana Discover and export CSV or JSON, or script direct _search queries against Elasticsearch; the predict_inference_models and stored_custom_series events are the canonical record of which model touched which study, when and on whose behalf. No immutable archive ships by default; replicate the indices to write-once storage if tamper resistance is required.

PACS AI Logo status Status Terms and Conditions Privacy Policy Privacy Impact Assessment Document (EFVP)

© 2026 HeartWise AI Lab, Montreal Heart Institute. All rights reserved.