In-venue

Booking kiosk

A self-service touchscreen kiosk: a stepped walk-up booking flow, on-screen keyboard, automatic reset, payment at the counter or by QR code, and server-priced bookings.

Released
September 29, 2026
npx skills add timerise-ai/booking-kiosk
v0.1.8
Current release
7
Reference docs
5
Non-negotiables
MIT
License

The problem

What problem does Booking kiosk solve?

A walk-up kiosk is a booking website with the trust turned inside out. The screen is shared, nobody is logged in, and anyone can tap anything. Prices, capacity and double-booking protection have to live on the server. Whatever is on the screen has to disappear when the customer walks away.

This skill builds the kiosk for a gym, karting track, bowling alley, climbing wall, escape room or clinic: a stepped flow from service to confirmation, an on-screen keyboard, automatic reset after inactivity, payment at the counter or by QR code, live availability, and a find-my-booking screen for changes.

The booking backend sits behind a seam, so it works with your existing system. A Firestore reference implementation is included.

The module

What does the skill build?

An agent with this skill builds one module for a Next.js App Router app, on your stack. For Booking kiosk that module consists of:

  • 1. Walk-up booking flow
  • 2. On-screen keyboard
  • 3. Pay-at-counter or pay-by-QR
  • 4. Server-priced idempotent booking API
  • 5. LAN failover contract

Provenance

Where do the rules come from?

It was written by the engineers who have shipped this kiosk module. The earlier implementation it was audited against ran as one of several surfaces sharing a booking engine at a multi-location venue with a LAN fallback server. The templates carry its state machine, screen flow, timers, stock model, capacity transaction and offline failover, and hold the properties a kiosk has to hold: every price, capacity check and promo decision is computed on the server; every submit is idempotent; a configured device key makes a missing header a 401; a stock lock is released on every failure path; a session lives in memory and resets on inactivity. Each one is stated as what the reducer suite and the route guard tables verify. `references/provenance.md` is the record of what the audit changed, what was kept deliberately, and what is new in the skill.

Written by the engineers who have shipped this kiosk module. The earlier implementation it was audited against ran on Next.js App Router, React, Firestore and Stripe, as one of several surfaces sharing a booking engine at a multi-location venue with a LAN fallback server. The state machine, screen flow, timers, stock model, capacity transaction and offline failover are production-proven there. The templates are the hardened version: a three-pass audit (correctness, code quality, operator usability) found the defects below, and the skill ships the fixes rather than the bugs.

The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.

  1. Fixed in the templates

    • Device auth that a missing header bypassed All four kiosk routes checked if (sent && configured && sent !== configured) , only a wrong key was rejected; omitting the header passed, and the kiosk client never sent one. Unauthenticated internet callers could create and mutate bookings and consume stock. Shipped: configured key means required header (api-contract.md).
    • Client-priced Stripe charges Slot and consumable prices arrived in the request body and flowed into the total and the Stripe amount. Any caller could book for zero. Shipped: prices removed from the wire; server resolves from its catalog (api-contract.md, booking-backend.md).
    • Online payment was a dead end The confirmation screen rendered QR: <first 50 chars of URL>... as text (a placeholder comment said "use a QR library in production"); the main-flow customer had no way to pay online. Separately, three of the six text inputs set inputMode="none" (suppressing the native keyboard) without opening the on-screen keyboard, email/phone/promo entry and the whole booking-lookup screen were unusable on touch-only hardware. Shipped: QR requirement + keyboard-on-every-field rule (screens.md).
    • Background refresh teleported the user The silent availability refresh re-dispatched SET_SLOT, whose reducer arm also navigates, a booking made by anyone else yanked a kiosk user from the summary back to station selection, discarding progress. Selected stations also never reconciled against refreshed availability, so a stolen station could still be submitted. Shipped: separate REFRESH_SLOT data-only action that drops taken stations and keeps names index-aligned, with tests (state-machine.md).
    • Stock reserved and never released on failure paths A capacity conflict returned 409 with the just-taken stock locks still ACTIVE for the full 15-min TTL; the edit route released old locks before acquiring new ones (a failure stranded the booking with no reservation); the payment webhook released locks for failed add-item payments but not for failed bookings. Shipped: release-on-failure in the route skeleton; webhook failure path releases booking locks (api-contract.md, booking-backend.md).
    Show 12 more
    • No idempotency The client generated a sessionId nobody ever read; the server minted lock session keys from Date.now() (collides across terminals); the only duplicate-submit guard was a client boolean. Retries and double-taps made double bookings. Shipped: sessionId as a server-checked idempotency key (api-contract.md).
    • Promo failures were silent, then fatal An invalid/expired/wrong-location code was ignored, the customer silently paid full price. And usage-limit increments ran after booking commit without a catch, so hitting the limit returned a 500 for a booking that existed. Shipped: invalid promo is a 400 before any write; post-commit usage recording is caught (api-contract.md).
    • PII on an unauthenticated, unthrottled lookup GET .../lookup?shortId= returned full name, email, phone and participants for any guessable 8-char code (31-char alphabet), min query length 3, with no rate limit, an enumeration target. Shipped: rate limit + masked contact fields (api-contract.md).
    • Edit/add-items trusted and raced The edit route re-wrote slots with no capacity re-check (could double-book a station), recalculated pricing without the promo (silently un-discounting on every edit), and skipped the module-block gate its siblings had. Add-items had no cancelled-booking guard, mutated pricing in a read-modify-write outside a transaction (concurrent adds lost increments), appended lines via a set-union that deduplicated identical purchases while still charging for both, and its webhook wrote back absolute totals computed minutes earlier, with no event-id dedupe on redelivery. Shipped: the guard table and delta-in-transaction rules (api-contract.md, booking-backend.md).
    • Unbounded transactional read The capacity re-check read every PENDING/CONFIRMED booking for the location, no date filter, no limit, inside every create transaction. Grows forever; eventually every sale times out. Shipped: date-overlap filter as part of the transaction spec (booking-backend.md).
    • A drawer of smaller correctness fixes Back button inert on the side-flow steps (they were outside STEP_ORDER); participant seeding clobbering a hand-edited contact name; cart surviving a location switch with the old location's item ids; the calendar's "today" memoized once (wrong after midnight); a 60 s clock under seconds-granularity UI and slot cutoffs; the summary's payment method captured once and stale after connectivity changes; a retry timer firing after unmount; no request aborts anywhere; catch(() => {}) on the pricing/equipment/locations fetches (symptom: submit disabled with no reason shown); raw Error.message in 500 bodies; two error-envelope shapes (client showed blank errors for one); localhost as the default payment-redirect base URL; inactivity ref written during render; silent per-lock confirm failures; equipment lines silently skipped when unresolvable. Each fix lives in the reference that owns the topic.
    • Operator and accessibility gaps No remote heartbeat (a dead kiosk was found by walking past), no stock-confirm-failure surfacing, modal without dialog semantics or focus management, globally stripped focus outlines, icon-only buttons without labels, 32 px touch targets on the busiest screen, failing contrast on hint text, hardcoded source-language strings in shared hooks and persisted values, dead dictionary keys, and an edit endpoint whose UI trigger was wired but never called. Shipped: rules in screens.md and the operator table in operations.md; heartbeat is documented as an extension, not shipped code.
    • A new slot kept the stations taken in it Found by the prompt-1 agent eval of 0.1.6, in this skill's own reducer, not in the audit. SET_SLOT replaced the slot and kept selectedStations as they were, and SET_DATE kept the previous date's slot. A customer who picked stations, went back and chose another slot carried a station already taken there onto the participants screen, which derives from context, and the submit came back 409. Shipped: SET_DATE drops the slot; SET_SLOT keeps only the stations free in the new slot, names aligned, and still navigates. The thirteenth reducer test covers both (state-machine.md).
    • A failed checkout kept its locks Found while verifying the templates for the same release. The create route's online path called createCheckoutSession after the booking and its stock locks existed, with no catch: a provider error escaped as an unhandled 500 and left the booking PENDING, holding its stations and locks until the cleanup jobs ran, and a retry with the same sessionId returned that booking without a checkoutUrl. The fifth hard rule, broken by its own template. Shipped: failPendingBooking on KioskBackend marks the booking FAILED, releases its locks and stations and clears its idempotency key; the route calls it and answers 502 (api-contract.md, booking-backend.md).
    • Optional fields reached the backend unchecked Found by the prompt-1 agent eval of 0.1.7, reported in its handover. parseCreateRequest said the shape was proven field by field, yet accepted a number as promoCode, an object as parentBookingId, an array as email, a missing locale, any value as equipment, and a null slot, which threw; a missing consumables array passed through as undefined. Shipped: every optional field is absent or a bounded string, equipment lines are checked, and parseCart returns the cart with consumables normalised, shared by create and the promo check (api-contract.md).
    • A failed stock confirm answered 500 for a booking that existed Found by the same eval. On the counter path the route awaited confirmStockLocks after the booking was committed; a confirm that threw escaped the route, and the customer saw an error for a booking that was made. Shipped: the confirm contract is retry once, flag the booking, reject; the route logs the rejection and answers 201 (booking-backend.md).
    • An idle kiosk kept a booking on screen Found by the same eval. The inactivity timeout spared service-select and the booking-* side flow. A session adding a slot to a looked-up booking sits on service-select with parentBookingId set, so the next customer could book onto it; the side flow showed a looked-up customer's name until someone touched the screen. Both break the first hard rule. Shipped: resetsOnIdle in the reducer module spares only the untouched entry screen, with the fourteenth reducer test (state-machine.md, screens.md). The staff-assisted exemption of the side flow is dropped: a staff member who is helping touches the screen.
Read the full record in provenance.md

Non-negotiables

What are the 5 rules the module never breaks?

Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.

  1. Never persist kiosk session state.

    In memory, 120 s inactivity reset, 30 s confirmation reset, so an abandoned session never shows one customer's name to the next. The reducer suite covers the reset paths.

  2. Never trust the client for prices, capacity, or promo validity.

    The server computes every amount and answers an invalid promo with PROMO_INVALID instead of a full-price fallback, because a customer at a kiosk has no way to check the amount. The guard tables in references/api-contract.md state each check.

  3. One fetch wrapper for every kiosk request.

    The wrapper carries the device header, the error envelope and the failover base URL, so auth, errors and offline failover apply to every request at once.

  4. If the device key is configured, a missing header is a 401.

    Rejecting only a wrong key is the same as no auth. The device-auth guard in references/api-contract.md checks presence before value.

  5. Release stock locks on every failure path after acquiring them.

    A 409 that keeps the reservation freezes stock for the full 15-minute TTL. The failure-mode table in references/booking-backend.md lists every path.

Fit

When should you use it, and when not?

Use it for

  • Building a walk-up booking terminal for slot-based capacity (stations by time windows) with optional equipment and consumable add-ons.
  • Adding kiosk mode to an app that already has online booking.
  • Auditing an existing kiosk against the hard rules below; provenance.md records the reason behind each.

Not for

  • The on-prem offline fallback serverInsteadThe sibling `island-mode-server` skill; this one defines only the kiosk's client-side failover contract
  • Digital signage or display screensInsteadThe sibling `digital-signage` skill: playback, no input
  • Staff-facing POS or admin booking toolsInsteadThe host's authenticated admin; staff are trusted, and this skill's trust model is wrong for them
  • A plain booking websiteInsteadThe backend seam if useful; the kiosk machinery (keyboard, timers, touch hardening) is dead weight there

Build it yourself

How do I install it?

One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.

$ npx skills add timerise-ai/booking-kiosk

Claude Code

Invoke with /booking-kiosk

Codex CLI

Invoke with $booking-kiosk

Gemini CLI

Invoke with /skills

Name the agents instead with -a, for example npx skills add timerise-ai/booking-kiosk -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.

What is inside the repository (14 entries)
  • SKILL.mdEntry point: when to use and when not to, the architecture diagram, six critical facts, five hard rules, the quick start, the Adaptation Contract table, and the reference directory
  • references/state-machine.mdCanonical vocabulary and rename table, state shape, actions, the reducer and provider, the rules an orchestrator must follow, and 14 inline reducer tests
  • references/screens.mdScreen flow, the per-screen contract, payment-method resolution, the on-screen keyboard, the inactivity timer, touch hardening, modals, and the dictionary key tree
  • references/api-contract.mdRoute surface, device auth, the single error envelope, create / lookup / add-items / edit with their guard tables, and the one fetch wrapper, createKioskFetch
  • references/booking-backend.mdThe KioskBackend interface, the capacity transaction, the two stock models, payments, failure modes, a Firestore reference implementation, and a relational sketch
  • references/realtime-offline.mdThe freshness signal, availability fetching and caching, offline failover against a LAN server, and what stays out
  • references/operations.mdLaunching the kiosk, access gating, the operator surface, environment variables, and the post-deploy smoke test
  • references/provenance.mdThe engineering ledger: what the audit of the earlier implementation changed and how the templates verify it, what was kept deliberately, and what is new in the skill
  • README.mdThis file: install, activation, contents, the non-negotiables, where to go instead, contributing
  • CHANGELOG.mdKeep a Changelog, one section per release, newest first
  • CLAUDE.mdWhat the repository is and its editing conventions, for an agent editing the skill itself
  • LICENSEMIT
  • evals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested
  • .github/workflows/agent-eval.ymlThe caller of the index's reusable eval workflow, run on every published release and on a maintainer's dispatch; the same in every skill

Recent releases

  1. v0.1.8September 29, 2026

    Fix release, from the prompt-1 agent eval runs against 0.1.7: one agent reported four template defects in its handover instead of editing them, and each was reproduced before the fix.

  2. v0.1.7September 29, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.6 and verifying every template.

  3. v0.1.6September 29, 2026

    Wording release that brings the repository to the skill standard. The templates behave as in 0.1.5; only their comments, diagrams and two glyphs are written in ASCII.

After installing

What do I tell my agent?

Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.

  1. Build a self-service booking kiosk for our karting track: pick a session, a time slot, the number of karts and the driver names, then pay at the counter. Go back to the start after two minutes without a touch.

  2. Add kiosk mode to our booking app: a touchscreen flow with an on-screen keyboard, pay by QR code, and a find-my-booking screen to change a reservation.

  3. Audit our kiosk against the skill's hard rules and list every place it breaks one.

Tested

How does it do in each agent?

We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.

  • Gemini CLI0.61.0

    gemini-3.8-flash

    Built, checks pass
    Build a self-service booking kiosk for our karting track: pick a session, a time slot, the number of karts and the driver names, then pay at the counter. Go back to the start after two minutes without a touch.
    Typecheck: passBuild: passTests: pass
    Time
    8 min
    Changed
    43 files, +5,816 lines
    Stack
    Firestore
    Skill
    v0.1.7
    Run
    Sep 29, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the final summary; no local rerun was possible (no Gemini CLI on the scoring machine). The templates sit at their documented paths, checkKioskKey is the shipped digest compare, and the 13-test suite runs unchanged under vitest beside its own 10 backend tests. The routes call the KioskBackend seam, releasing locks through releaseStockLocks and failPendingBooking, and every request goes through createKioskFetch with the LAN base URL. .env.example is tracked with the three kiosk variables and the Stripe pair, all empty. The handover names the variables, the open API without the key, the ?locationId= boot URL and the in-memory backend to replace. Items 2 and 3 rest on the summary.

  • Codex CLIcodex-cli 0.159.0

    gpt-6-astra

    Built, checks pass
    Build a self-service booking kiosk for our karting track: pick a session, a time slot, the number of karts and the driver names, then pay at the counter. Go back to the start after two minutes without a touch.
    Typecheck: passBuild: passTests: pass
    Time
    16 min
    Changed
    39 files, +3,286 lines
    Stack
    Firestore
    Skill
    v0.1.7
    Run
    Sep 29, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the full transcript and final diff. It extracted every template by its // file: line and ended by asserting all twelve template files byte-identical to the skill's blocks; it kept the canonical vocabulary, so no rename was needed. It installed vitest with npm install -D vitest and ran the 13 reducer tests unchanged. .env.example is tracked (un-ignored) and lists KIOSK_API_KEY, KIOSK_PUBLIC_BASE_URL and KIOSK_LAN_FALLBACK_URL, all empty. The page injects the key and the LAN URL as props into one createKioskFetch; only the health probe calls fetch directly, as the skill allows. The handover names the variables, the open API without KIOSK_API_KEY, the ?locationId= boot URL and the demo backend to replace.

  • Claude Code2.1.284

    claude-opus-5-5

    Built, checks pass
    Build a self-service booking kiosk for our karting track: pick a session, a time slot, the number of karts and the driver names, then pay at the counter. Go back to the start after two minutes without a touch.
    Typecheck: passBuild: passTests: pass
    Time
    14 min
    Changed
    58 files, +4,786 lines
    Stack
    Firestore
    Skill
    v0.1.7
    Run
    Sep 29, 2026

    Result fileAgent log

    What we saw

    Rubric 8/8, scored from the final summary; a maintainer's dispatch on 0.1.7 after the release run's Claude Code job failed on an API 503 before its first turn. The 14 reducer tests are not yet in 0.1.7: it ran the 13 shipped ones under vitest, unchanged apart from the vocabulary rename. .env.example is tracked with the three kiosk variables, empty. The handover names the open API without KIOSK_API_KEY, the ?locationId= boot URL and the memory store wired in lib/kiosk/server/instance.ts. Instead of editing templates it reported four defects in them, each reproduced against 0.1.7 and fixed in 0.1.8: optional create fields and equipment lines unchecked by parseCreateRequest, a failed stock confirm answering 500 for a booking that existed, no server idempotency on add-items, and the idle reset sparing a looked-up booking. Its three declared additions, a promo-check route, kiosk.errors keys and an idle reset on the start screen while parentBookingId is set, filled gaps the skill had and are in 0.1.8 too; none crosses a hard rule.

Build it with Timerise

How long does it take, and what does it cost?

We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:

  1. Step 1

    Brief

    Tell us what the module must connect to. Takes minutes, in a chat.

  2. Step 2

    Prototype in 48 hours

    A clickable prototype of your system and a quote, at no cost.

  3. Step 3

    Build and handoff

    One project price. Source code, documentation and IP are yours.

Two ways to get Booking kiosk

Build it yourself

Install the skill. Your own agent builds the module.

  • MIT licensed, no strings
  • Runs in Claude Code, Codex CLI and Gemini CLI
  • The same rules our engineers build by
$ npx skills add timerise-ai/booking-kiosk

Build it with Timerise

Send a brief. We build Booking kiosk into a system you own.

  • Clickable prototype and a quote within 48 hours, free
  • One project price, no subscription, no commission
  • Source code, documentation and IP handed over
3 years of support included.

Generated from the skill's own files at commit 9332998. Every rule above links to where the repository says it. All skills