PACS AI Logo
|
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.

FieldTypeExampleNotes
idstringacme-pacsSame as the document ID
namestringAcme Cardiology HospitalDisplay name shown in the viewer
addressstring123 Main St, Montreal, QCFree-form
onboarding_questionnairesstringemptyOptional JSON blob
onboarding_enable_registrationboolfalseSelf-serve public registration toggle
onboarding_enable_consentboolfalseConsent-flow toggle
onboarding_consent_linkstringemptyURL shown during the consent step
created_at, updated_atint1715000000Unix 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

RoleScope
OWNERFull control over the tenant; created at install through POST /v1/user/owner/add; can add or remove ADMIN and USER members. Enforced by RBACOwnerGuard.
ADMINManages users, ingestion jobs and inference models; cannot transfer or delete the tenant. Enforced together with owners by RBACOwnerOrAdminGuard.
USERClinician 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)

EndpointPurpose
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/updateToggle onboardingEnableRegistration for self-serve sign-up
PUT /tenant/onboarding-consent/config/updateConsent 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/resetDeletes the user general and model-specific questionnaire answers so onboarding replays (idempotent)
POST /user/addCreate a tenant user (role, name, email, licenseNo, specialty)
POST /user/invite, POST /user/invite/resend, GET /user/invites, DELETE /user/invite/{id}/removeEmail invitations; a registration that carries the invite code is created already verified
GET /user/all, PUT /user/update, DELETE /user/{id}/removeList, update and remove members
POST /user/{id}/suspend, POST /user/{id}/reactivateAccount suspension with immediate session revocation, and reactivation
GET /user/me, GET /user/metadata, PUT /user/metadata/update, PUT /user/password/updateUser self-service
POST /iam/login, POST /iam/verify-email, POST /iam/forgot-passwordSession login, verification-email resend, password reset
GET /user/policies/registration, POST /user/policies/accept, GET /user/policies/status, GET /user/{ID}/policies/statusVersioned Terms and Privacy acceptance: registration metadata, acceptance, own status, and administrator audit status
GET /user/specialtiesDoctor specialties offered at registration
GET /ecs/logs?index&query&startDate&endDate&exportSearch or export audit indices without opening Kibana
PATCH /ecs/kibana/indices/syncSuperuser-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/subscribeThe 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)

EventIndexNotable fields
User loginloginssession_id, role, specialty
Admin invite sentadmin_invitesinvitee email
User managementadmin_membersaction (CREATE, UPDATE, DELETE), target user, role, licence
Remote-PACS query (C-FIND)find_modality_studiesmodality_id, query_id
Study retrieved (C-MOVE)retrieved_studiesstudy_instance_uid, modality_id
Inference runpredict_inference_modelsinference_model_id, docker_image, model, study_instance_uid, series_instance_uids, additional_metadata
Consent signedsigned_consentstenant and user
Custom series storedstored_custom_seriesstudy_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.