For Administrators · 7-minute read
Kiosk tokens & integrations
Three things only an admin can touch:
- The kiosk service accounts that drive wall-mounted kitchen displays and station tablets
- The Saavor webhook that pushes orders into PrepTable
- The per-kiosk-scoped tokens that authenticate both
This page covers how each is created, used, and rotated, plus what to do when one breaks.
Kiosks are service accounts, not roles
A kiosk is not a person and not a role in the RBAC enum. Internally each kiosk is a service account: a users row with role = 'kiosk' (the bootstrap migration adds the enum value; the MaybeBootstrapKioskUser helper creates one on first start). It authenticates with a per-kiosk WebAuthn passkey registered to that physical device.
The result is the same operational model as the people-side:
- Passkey-based auth (no shared passwords to leak)
- A passkey registry per "user" (the kiosk)
- Last-credential protection — the system refuses to remove a device's only passkey
- Audit-logged sessions
The differences are:
- A kiosk is scoped to exactly one campus at registration time. It can't see another campus's kitchen display — even if someone steals the token.
- A kiosk can't be promoted to staff, manager, or admin. The role enum is enforced.
- A kiosk's sessions are short-lived (a typical passkey assertion on a kiosk is good for the duration of the shift) and refreshed by re-asserting the same device-bound passkey.
Generating a kiosk token
You generate tokens from Admin → Kiosks → New kiosk.
The flow:
- Enter a label for the kiosk ("Harlingen line", "Waco pass", or "Front counter"). Pick the campus it'll serve. Click Generate.
- PrepTable produces a one-time registration URL — a deep link that includes a single-use challenge token and the campus scope.
- Bring the physical device to the wall. Open the URL on the device's browser. The browser registers the device's passkey bound to the kiosk service account.
- After registration, the kiosk service account has exactly one passkey (the device's). The kiosk logs in by re-asserting that passkey each shift.
If someone walks off with the device, the passkey is bound to the hardware — they can't extract it. You can revoke the device-bound passkey from Admin → Kiosks → the label, which invalidates future logins but doesn't erase past session records.
Multi-kiosk at one campus
A single campus typically has:
- One kitchen display at the pass (the line's order queue)
- One or more station tablets at prep stations (a cook sees their own station's queued tickets)
- Sometimes a manager tablet
Each is a separate kiosk service account, all scoped to the same campus. From the campus's perspective they share the same kitchen queue, just rendered differently per device.
The Saavor webhook
The Saavor webhook is the wire that pushes orders from Saavor POS into PrepTable's kitchen display. It's a single POST endpoint at POST /api/v1/saavor/orders (or whatever your Saavor integration team confirms the path is — check the integration config in your deploy).
Each incoming order carries:
- A Saavor-side order ID (so we can deduplicate re-pushes)
- The line items, modifiers, and customer-visible notes
- The destination campus (resolved by Saavor from the workstation's campus assignment)
- A HMAC signature header using a shared secret (
SAAVOR_WEBHOOK_SECRETin the API's environment)
PrepTable's job on receipt is:
- Verify the HMAC signature.
- Upsert the order into the kitchen queue as a ticket in
queuedstate. - Broadcast a WebSocket / SSE event so connected kitchen displays update without a refresh.
- Log the receipt in the audit log with the order ID and line count.
PrepTable never reaches back into Saavor's transaction data — it never sees payment, tip, customer-saved card, or sell price. The direction is one-way: Saavor → PrepTable for prep state, never prep state → sell record.
Rotating the Saavor webhook secret
If the secret leaks (or your security policy mandates periodic rotation):
- Generate a new secret with
openssl rand -hex 32. - Update
SAAVOR_WEBHOOK_SECRETin/opt/preptable/api/.envon titan. - Restart the API.
- Hand the new secret to your Saavor integration contact and have them update the webhook signature side before the next push arrives. There's a brief window during the restart where the API rejects incoming requests with the old secret and the new one; coordinate the timing so no orders are missed (or accept that any order arriving in that 30-second window is dropped and needs to be re-rung from Saavor).
The audit log records the secret rotation as an "integration config" event by an admin user, with the old secret's last-four-chars and the new one's — enough to correlate if an order is reported missing.
When orders stop arriving
This is the most common integration failure. The symptom: the kitchen display goes silent, the line stands around, and someone pages you. The likely causes, in rough order of frequency:
| Symptom | Likely cause | First thing to check |
|---|---|---|
| No orders on any kitchen display for 5+ minutes | Network between Saavor and titan, or DNS | curl the Saavor health endpoint from titan; check Caddy access logs for the webhook |
| One campus's display silent, others fine | That campus's workstation is misconfigured in Saavor | Ask the cashier to confirm campus selection at the register |
| All displays silent, API is up | Webhook secret was rotated and Saavor didn't get the new one | Check the API logs for HMAC failures; check journalctl -u preptable-api |
| Display shows a queued ticket but lines are wrong | Saavor-side menu drift; the menu Saavor is sending doesn't match what PrepTable cooked today | Compare Saavor's menu to the day's PrepTable menu; alert Saavor team |
| Display refreshes constantly, queues flicker | WebSocket disconnect loop | Check the API logs for SSE errors; reload the display |
The audit log will show the last successful order receipt. The timestamp is your starting point: orders stopped being processed shortly after that time.
What the audit log captures for integrations
| Event | Captured |
|---|---|
| Kiosk token issued | Issued-by admin, label, campus scope |
| Kiosk registered | Timestamp of registration, device passkey fingerprint |
| Kiosk passkey revoked | Revoked-by admin, old device fingerprint's last-four |
| Saavor order received | Order ID, line count, campus |
| Saavor HMAC failure | Source IP, request ID (but not the request body) |
| SAAVOR_WEBHOOK_SECRET rotated | Rotated-by admin, old/new last-four-chars |
Anything that looks like "the line is broken" should be answerable in five minutes by cross-referencing this log with the kitchen display's last refresh.
Where to go next
- Roles & campus permissions — the access model that determines which users can do anything in this section
- The audit log — the log these events surface in, and the rest of what it captures