Skip to content
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 → KiosksNew kiosk.

The flow:

  1. Enter a label for the kiosk ("Harlingen line", "Waco pass", or "Front counter"). Pick the campus it'll serve. Click Generate.
  2. PrepTable produces a one-time registration URL — a deep link that includes a single-use challenge token and the campus scope.
  3. 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.
  4. 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_SECRET in the API's environment)

PrepTable's job on receipt is:

  1. Verify the HMAC signature.
  2. Upsert the order into the kitchen queue as a ticket in queued state.
  3. Broadcast a WebSocket / SSE event so connected kitchen displays update without a refresh.
  4. 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):

  1. Generate a new secret with openssl rand -hex 32.
  2. Update SAAVOR_WEBHOOK_SECRET in /opt/preptable/api/.env on titan.
  3. Restart the API.
  4. 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:

SymptomLikely causeFirst thing to check
No orders on any kitchen display for 5+ minutesNetwork between Saavor and titan, or DNScurl the Saavor health endpoint from titan; check Caddy access logs for the webhook
One campus's display silent, others fineThat campus's workstation is misconfigured in SaavorAsk the cashier to confirm campus selection at the register
All displays silent, API is upWebhook secret was rotated and Saavor didn't get the new oneCheck the API logs for HMAC failures; check journalctl -u preptable-api
Display shows a queued ticket but lines are wrongSaavor-side menu drift; the menu Saavor is sending doesn't match what PrepTable cooked todayCompare Saavor's menu to the day's PrepTable menu; alert Saavor team
Display refreshes constantly, queues flickerWebSocket disconnect loopCheck 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

EventCaptured
Kiosk token issuedIssued-by admin, label, campus scope
Kiosk registeredTimestamp of registration, device passkey fingerprint
Kiosk passkey revokedRevoked-by admin, old device fingerprint's last-four
Saavor order receivedOrder ID, line count, campus
Saavor HMAC failureSource IP, request ID (but not the request body)
SAAVOR_WEBHOOK_SECRET rotatedRotated-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

Built for campus dining operations teams.