Dentolize · X-Ray Integration Walkthrough
On this pageTermsData model reference

Glossary & Data Model

Terms

Access Key — A revocable credential (format xak_<prefix>_<secret>) a clinic generates to let the desktop app authenticate to Dentolize's REST/GraphQL APIs without a user login. Stored server-side only as an HMAC-SHA256 hash, never in plaintext. Scoped to specific capabilities. Model: AccessKey.

Scope — A specific capability an Access Key is allowed to use. Three exist today: branches.read (list branches and their rooms), files.create (upload files), xray.acquire (drive the acquisition workflow — connect, heartbeat, transitions, upload). Stored as a plain string array (AccessKey.scopes) validated against a fixed server-side allow-list.

Lease token — A short-lived (90-second, heartbeat-renewable) credential minted from an Access Key when a desktop app calls the connect endpoint. Bound to one (company, branch, room, desktop instance) at a time. Used for every subsequent REST/WebSocket call instead of re-presenting the Access Key itself, and immediately invalidated if the underlying Access Key is revoked. Tracked in Redis, not the database.

Room — An arbitrary string identifying a physical location or scanner within a branch (e.g. an operatory). Not a formal Dentolize entity with its own table — it's a string configured on the desktop app that must match a branch's configured room list. A lease guarantees only one desktop instance can hold a given room at a time.

Desktop app / Dentolize Bridge — The companion Electron + native-TWAIN Windows application (repository xolize-core, PR #6) that clinics install on the workstation connected to their scanner. Holds an Access Key, receives capture requests over a GraphQL subscription, drives the real scanner hardware, and uploads the result. A real, working application as of this PR — not external/future scope.

Acquisition (X-Ray Acquisition Request) — One request/response cycle: a staff member asks for a specific X-ray slot to be filled by a connected desktop app. Model: XrayAcquisitionRequest.

Slot / slotOrder — Which position in a multi-image X-ray layout (e.g. a full-mouth series) the captured image should be saved into. Validated against a per-chart-type allow-list (VALID_SLOTS) so a slot number invalid for a given layout is rejected before a request is even created. Corresponds to the existing File.order field used elsewhere in the X-ray gallery.

Upload intent — A single, time-boxed (10-minute) attempt to upload one image for one acquisition, tracked so the server can reserve storage quota up front, hand out a presigned upload URL, and independently re-verify the uploaded object afterward rather than trusting the desktop app's own description of what it sent. Model: XrayUploadIntent.

Feature flag (FEATURE_XRAY_ACQUISITION) — Gates the entire feature to companies flagged isBeta, checked both client-side (hides the Acquire button) and server-side (rejects the mutation and desktop connect/lease calls). Has an independent kill switch separate from the beta-targeting rule.

Data model reference

Company
  ├─ AccessKey[]                     (one company can issue many keys)
  ├─ XrayAcquisitionRequest[]
  └─ XrayUploadIntent[]

AccessKey
  ├─ keyHash          String  @unique   (HMAC-SHA256 of the raw key; raw key is never stored)
  ├─ keyPrefix        String             (first 14 chars of the raw key, shown in the UI)
  ├─ label            String
  ├─ scopes           String[]           (branches.read | files.create | xray.acquire)
  ├─ companyId        String  → Company
  ├─ createdById      String  → User
  ├─ lastUsedAt        DateTime?          (touched on every successful auth, race-safe)
  ├─ revokedAt         DateTime?          (set on revoke; null = active)
  └─ XrayAcquisitionRequest[]     (via respondedByKeyId — which key fulfilled a request)

XrayAcquisitionRequest
  ├─ status                  XrayAcquisitionStatus   (WAITING | ACCEPTED | REJECTED | IN_PROGRESS |
  │                                                    PREVIEW | CAPTURED | WAITING_UPLOAD | UPLOADING |
  │                                                    COMPLETED | FAILED | TIMEOUT | CANCELLED)
  ├─ statusUpdatedAt, stateDeadlineAt    (drives the timeout cron — see Real-Time Status page)
  ├─ requestIdempotencyKey   String      (unique per requester, prevents duplicate web requests)
  ├─ xrayType, slotOrder
  ├─ patientName, patientDOB, patientPhoto     (denormalized snapshot at request time)
  ├─ patient        → Patient
  ├─ branch         → Branch
  ├─ company        → Company
  ├─ xray           → Xray                    (the target X-ray record the file attaches to)
  ├─ requestedBy    → User                     (the staff member who made the request)
  ├─ respondedByKey → AccessKey?                (which key/workstation accepted it, if any)
  ├─ respondedByInstanceId  String?             (which specific desktop app instance claimed it)
  ├─ auditLogs      → XrayAcquisitionLog[]
  ├─ uploadIntents  → XrayUploadIntent[]
  └─ file           → File?                     (set once completed)

XrayAcquisitionLog
  ├─ event           String    (REQUESTED, ACCEPTED, CAPTURED, UPLOADING, COMPLETED, CANCELLED, TIMEOUT, ...)
  ├─ details         Json?
  ├─ userId          String?   (set when a staff action caused the event)
  ├─ accessKeyId     String?   (set when the desktop app caused the event)
  ├─ idempotencyKey  String?   (unique per acquisition request — enables safe retries)
  └─ acquisitionRequest → XrayAcquisitionRequest

XrayUploadIntent
  ├─ status                XrayUploadIntentStatus  (PENDING | UPLOADED | COMPLETED | EXPIRED | CANCELLED)
  ├─ objectKey             String  @unique          (the S3 key the presigned form targets)
  ├─ expectedSize, expectedContentType, checksumSha256   (declared up front, verified after upload)
  ├─ reservedBytes         Int                       (storage quota reserved against Company.sizeLeft)
  ├─ clientRequestId       String                     (idempotency key, unique per acquisition request)
  ├─ expiresAt             DateTime                   (10 minutes from creation)
  ├─ objectDeletedAt       DateTime?                  (set once the cron confirms cleanup)
  └─ acquisitionRequest    → XrayAcquisitionRequest

See packages/prisma/schema.prisma:279 (AccessKey), :7239-:7360 (XrayAcquisitionStatus, XrayAcquisitionRequest, XrayAcquisitionLog, XrayUploadIntentStatus, XrayUploadIntent) for the authoritative definitions, and packages/prisma/migrations/20260816000000_add_access_keys/ / packages/prisma/migrations/20260816001000_add_xray_acquisition/ for how they were introduced.