The problem
What problem does Bookable events solve?
Classes, workshops, tournaments, meetups and open days all have the same shape: a fixed number of seats, a list of people who want one, and money that changes hands. Paid events sell tickets. Free events lose seats to people who sign up and never come.
Events are the easy half. The hard half is money moving while seats are scarce. A seat is taken before payment lands. A payment lands after the seat expired. A card hold lapses before a no-show can be charged. A webhook arrives twice and refunds a ticket twice.
This skill gives a coding agent the events module the way we build it: every change goes through one state machine inside a database transaction, and Stripe is called only after commit. Free events can take a refundable card hold that is released when the guest checks in and captured when they do not show. Staff check people in, refund tickets and cancel events, and every retry replays the first result.
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 Bookable events that module consists of:
- 1. Checkout tickets
- 2. A refundable card hold on free events released on check-in and captured on no-show
- 3. Staff check-in
- 4. Refunds
- 5. Event cancellation
- 6. One pure state machine with Stripe called only after commit
Provenance
Where do the rules come from?
This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was the events module of a multi-location venue-booking system. The templates hold the properties such a module has to hold: a paid event is never free in a currency it has no price in; a declined attempt inside open Checkout keeps the seat, and only session expiry releases it; money that arrives for a released seat is refunded; a refund is the amount frozen at registration; a no-show hold is placed inside the card network's window and captured only after settlement; a redelivered webhook or a repeated action changes nothing. The three suites, 50 tests, state each one, and `references/provenance.md` has the record.
This is the engineering ledger for the person editing the skill. The templates were written by the engineer who owns the events module of a multi-location venue-booking system (Next.js App Router, React, Firestore, Stripe), and audited against that earlier implementation. It ran event types, capacity-limited registration, free, card-paid and wallet-paid tickets, a Stripe webhook, customer cancellation with wallet credit, and a cleanup cron for unpaid seats. The audit made three passes: correctness, code quality and operator usability.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed in the templates
- A paid event was free in any currency it had no price in Registration treated a missing or zero price in the chosen currency as "free", and the admin form pre-filled
0for every enabled currency. Cancelling that free seat then credited the wallet with the price in the default currency: a loop that minted stored credit. Shipped:offeredCurrencies/resolveChargerefuse unpriced currencies; free means free in every currency; refunds use the frozenpayment.amount(money-and-time.md). Regression tests in both suites. - One declined card attempt deleted the seat
payment_intent.payment_failed, sent for each failed attempt while Checkout is still open, deleted the participant. A successful retry then charged the customer for a seat that no longer existed, and the error was swallowed. Shipped: that event is deliberately not handled; only session expiry ends a PENDING registration; late money is refunded (stripe.md). - Session expiry was not handled for events The expiry handler covered other payment types only, so abandoned seats waited for the cron. Shipped:
checkout.session.expiredmoves toCHECKOUT_EXPIREDfor the participant's current session. - Cleanup could delete a participant who had just paid The cron read the status outside the transaction and deleted inside it without re-checking. Shipped: every change is one transaction around the pure state machine; the tick reconciles with Stripe before expiring and fulfils a completed session it finds (participant-lifecycle.md).
- Refunds were never refunds Card-paid seats were "refunded" as wallet credit at the current price; guests without a wallet got nothing while the response reported a refund; a failed credit was swallowed after the status flipped; Dashboard refunds left the seat confirmed. Shipped: real Stripe refunds of the frozen amount, a durable
REFUND_PENDINGretried by the tick,charge.refundedhandling (registration.md).
Show 16 moreShow fewer
- Wallet payments were not atomic Balance check, seat, then debit: three writes; a crash in between left a paid seat unpaid, and the debit had no reference to the seat. Shipped: debit with ref
event:<participantId>; the tick settles an interrupted one from the ledger. - No webhook idempotency, and errors answered 200 Shipped: event-id claims with stale-claim recovery, no-op transitions, idempotency keys on every Stripe write, and 500 on handler failure so Stripe retries.
- The admin update wrote the raw request body, without a location check Any field (seat counter, deleted flag, location) could be overwritten, by an admin of any location. Shipped:
parseEventInputwhitelist, capacity at least seats taken in the transaction,requireStaffForEventon every[eventId]route (api-routes.md). - Cancelling an event refunded nobody A status flip left every participant confirmed and charged. Shipped: a cancel-event action that closes registration, then refunds and releases every seat; the edit form cannot set
cancelled. - Registration trusted a player id from the request body Anyone could attach a registration to another account, whose owner could then cancel it and collect the refund. Shipped: identity from the session only; guests use an HMAC manage link.
- Unpaid seats outlived failed session creation The seat was written before Checkout, and a Checkout error (for example a payment method valid for only one currency) left it held for half an hour. Shipped: the seat is released the moment session creation fails; payment methods follow the Dashboard instead of a hard-coded list.
- "Pay again" could never work The cleanup ran 30 minutes after registration while sessions lived 24 hours, and a new session did not expire the old one, leaving two payable sessions per seat. Shipped: 30-minute sessions, attempt-scoped idempotency keys, old session expired on resume, expiry honoured only for the current session.
- The cron was open when its secret was unset Shipped: fail closed, constant-time comparison.
- Times were UTC pretending to be local "Today" was UTC's date; slot instants were built by appending
Zto local wall-clock strings. Shipped: IANA zone on every event,zonedToUtc,todayIn(money-and-time.md). - Smaller defects A slot-block setting could not be switched off once set (dropped
undefinedkey); the customer lookup for Stripe matched by email only; draft and deleted events were publicly readable; event emails did not exist; the success redirect landed on the home page with a query parameter nothing read; participant lists were hard-coded in one language; the paid amount and currency were not shown to staff. Slot blocking stays with the host's booking module, so the first item is out of scope here; each of the others is addressed in the corresponding reference. - An old session's expiry could release a resumed seat Found by the agent eval of 0.1.1, in the templates themselves. "Pay again" expired the old session before the new one was stored, and the webhook compared session ids outside the transaction, so the old session's
expiredevent could release the seat the customer was paying for; the payment then came back as late money. Shipped: resume clears the stored session ids first;CHECKOUT_EXPIREDcarries the session it saw and the machine ignores one that is no longer current. A lifecycle test delivers the webhook mid-resume. - A second payment for a paid seat was never refunded Found by the same eval. Two Checkout tabs paid for one seat: the second
TICKET_PAIDfailed the transition, the webhook answered 500 until Stripe gave up, and the money stayed. Shipped: it raisesLatePaymentError, so the webhook refunds it; a payment already recorded is a no-op in every status. A lifecycle test pays twice. - Cancelling an event skipped guests already checked in Found by the same eval. The no-cancel-after-arrival rule applied to every actor, so a cancelled event left arrived guests charged and reported them as failures on every re-run. Shipped: the rule binds the customer only; staff and the event cancel refund arrived guests. A lifecycle test cancels after check-in.
- The check-in list lacked its own header Found by the same eval. The screen in ui.md shows the event name and times, and the list route returned only capacity and seats taken. Shipped: the route returns name, date, times and zone.
- A refunded second payment reported the paid seat as refunded Found by the agent eval of 0.1.2, in the fix for entry 17. Refunding the second payment sends
charge.refundedfor that payment, and the handler applied it to the participant whose seat the first payment holds, releasing it. Shipped:charge.refundedis ignored unless it names the participant's own payment intent. The second-payment lifecycle test delivers that webhook. - A guest back from Checkout could not read their status Found by the same eval. The success URL carried only the participant id, and the status route needs the session owner or the manage token, so a guest's "registered" page could not poll. Shipped:
urls.successreceives the manage token, and the host's URL carries it ast.
- A paid event was free in any currency it had no price in Registration treated a missing or zero price in the chosen currency as "free", and the admin form pre-filled
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.
Transaction first, Stripe second.
No Stripe call inside a transaction, and no state written after a Stripe call without a durable intent first, because transaction callbacks re-run and a failed write after a capture loses the record of money moved. The engine is the only writer of
payment,depositandattendance, and the lifecycle suite drives every flow through it.Identity never comes from the request body.
Customers come from the session, guests from an HMAC manage link, staff from a location-scoped guard on every staff and admin
[eventId]route, because a body field lets anyone attach a registration to someone else's account and collect its refund. A lifecycle test holds that a manage token verifies only for its own participant.Never capture before settlement.
A hold that would lapse is renewed or released, never captured early, because check-in must stay possible until the event is over. The lifecycle suite holds that a no-show is captured after the grace period and not before.
Cancelling an event is an action, not a status.
It closes registration, then refunds and releases every seat; a status flip would leave every participant charged. A lifecycle test cancels an event and checks every hold is released.
A webhook error is a 500.
The handler releases its event-id claim and answers 500 so Stripe retries, because a swallowed error is money nobody recorded. A redelivered webhook is a no-op, which a lifecycle test holds.
Fit
When should you use it, and when not?
Use it for
- Capacity-limited events (free or paid) with online registration and payment.
- Free events that need a refundable deposit to stop no-shows.
- Staff check-in at the door that must drive money (release or capture).
- Auditing or upgrading an existing event-registration module: provenance.md has the audit ledger and the order of work.
Not for
- Marketplace splits, payouts, connected accounts, subscriptionsInsteadThe sibling `stripe-connect-subscriptions` skill
- Walk-up touchscreen registrationInsteadThe sibling `booking-kiosk` skill, which can call this engine
- Hourly resource booking (lanes, courts, rooms)InsteadThe host's booking engine; events only block its slots
- Seat maps, ticket resaleInsteadA ticketing platform
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/bookable-eventsClaude Code
Invoke with /bookable-events
Codex CLI
Invoke with $bookable-events
Gemini CLI
Invoke with /skills
Name the agents instead with -a, for example npx skills add timerise-ai/bookable-events -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 (22 entries)
SKILL.mdEntry point: when (not) to use, architecture and the adaptation contract, six critical facts, five hard rules, quick start, reference directoryREADME.mdThis front doorCHANGELOG.mdKeep a Changelog, one section per release, newest firstCLAUDE.mdWhat this repository is and the conventions for editing the skill itselfLICENSEMITreferences/data-model.mdRename table, the neutral types, why the shape is what it is, seat accountingreferences/money-and-time.mdAny-currency conversion (0, 2 and 3 decimals, ISK and UGX), pricing that is never free by accident, DST-correct timereferences/participant-lifecycle.mdThe pure state machine, the effects diff, and the one due-work pointerreferences/no-show-deposits.mdHybrid hold (Checkout hold, or saved card and off-session hold), timeline and policy, outcomes, disclosure, the tickreferences/stripe.mdCheckout Sessions, webhook handling and idempotency, the gateway, an 11-step test-mode walkthroughreferences/registration.mdRegistration, cancellation and refund flows, guest manage links, request parsingreferences/engine.mdcreateEventsEngine, its dependencies and the host-facing portsreferences/firestore.mdFirestore store, indexes, security rulesreferences/postgres.mdPostgres schema, store, RLS, apgclientreferences/api-routes.mdRoute surface, error codes, authorization rules, the host seam file, every route handlerreferences/ui.mdEvent card, registration dialog, manage page, staff check-in screen, admin formreferences/testing.mdVitest config, in-memory store and fake gateway, the core and state-machine suitesreferences/testing-lifecycles.md20 end-to-end lifecycle tests, including three regression testsreferences/operations.mdEnvironment, the tick, what operators need to see, reconciliation, emails, go-livereferences/provenance.mdThe engineering ledger: what the audit changed and how the templates verify it, what was kept on purpose, what is new in the skill, and the order of work on an existing moduleevals/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: prompt 1 in Claude Code, Codex CLI and Gemini CLI on every published release, and any agent and prompt on a maintainer's dispatch
Recent releases
- v0.1.3September 28, 2026
Fix release, from scoring the prompt-1 agent eval runs against 0.1.2. Apps built from 0.1.2 should copy in the new webhook.ts; apps built from any earlier version, also engine.ts and the success URL in host.ts.
- v0.1.2September 28, 2026
Fix release, from scoring the prompt-1 agent eval runs against 0.1.1. Apps built from 0.1.0 or 0.1.1 should copy in the new participant-machine.ts, engine.ts, webhook.ts, tick.ts and the check-in list route.
- v0.1.1September 28, 2026
Documentation-only release: the templates and references are unchanged from 0.1.0. The repository now carries the agent eval prompts and workflow the skill standard requires, and the front door describes them.
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.
Add events to this Next.js app on Postgres: limited capacity, paid tickets through Stripe Checkout, staff check-in at the door, and refunds when we cancel an event.
Our free workshops have a no-show problem. Put a card hold on each registration that is released when the guest checks in and captured when they don't show.
FirestoreAudit our event registration module: we have seen double refunds and overbooked events.
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.
- Built, checks pass
Gemini CLI0.61.0
gemini-3.8-flash
Add events to this Next.js app on Postgres: limited capacity, paid tickets through Stripe Checkout, staff check-in at the door, and refunds when we cancel an event.
Typecheck: passBuild: passTests: pass- Time
- 10 min
- Changed
- 37 files, +4,378 lines
- Stack
- Postgres
- Skill
- v0.1.3
- Run
- Sep 28, 2026
What we saw
Rubric 8/8 (gemini-3.8-flash, scored from the JSON summary; dispatched re-run of 0.1.3 as round 3). The summary lists the shipped layout and the three suites at 22, 8 and 20.
getCustomerandgetStaffreturnnull, and.env.examplelists the five variables plusDATABASE_URL. The final message names the five variables, the webhook and its eight event types, the tick with its schedule and header, the logged emails and the 401 staff and admin routes. - Built, checks pass
Codex CLIcodex-cli 0.158.0
gpt-6-astra
Add events to this Next.js app on Postgres: limited capacity, paid tickets through Stripe Checkout, staff check-in at the door, and refunds when we cancel an event.
Typecheck: passBuild: passTests: pass- Time
- 13 min
- Changed
- 69 files, +5,474 lines
- Stack
- Postgres
- Skill
- v0.1.3
- Run
- Sep 28, 2026
What we saw
Rubric 8/8 (gpt-6-astra, high effort, scored from the transcript; dispatched re-run of 0.1.3 as round 3). No template edits besides
host.ts, whose store line calls adatabase.tshelper for the pool. No extra checks or token code, fixtures and suites untouched (the skill's 50 tests plus 11 of its own), and.env.examplelists the five variables. The final message names them, the webhook and its eight event types, the five-minute tick, the logged emails and the 401 staff routes. - Built, checks pass
Claude Code2.1.283
claude-opus-5-5
Add events to this Next.js app on Postgres: limited capacity, paid tickets through Stripe Checkout, staff check-in at the door, and refunds when we cancel an event.
Typecheck: passBuild: passTests: pass- Time
- 5 min
- Changed
- 54 files, +5,147 lines
- Stack
- Postgres
- Skill
- v0.1.3
- Run
- Sep 28, 2026
What we saw
Rubric 8/8 (claude-opus-5-5, scored from the JSON summary; dispatched re-run of 0.1.3 as round 3). The templates are unchanged apart from
host.ts, and the 50 tests pass as shipped. With no env vars set, staff and cron routes answer 401 and the webhook answers 400..env.examplelists the five variables empty, plusDATABASE_URL. The final message names them, the webhook and its eight event types, the tick with its bearer header, the logged emails and the closed staff routes. It also reports that an event cancel refunds checked-in guests and a second payment is refunded, as 0.1.2 made the templates do. Leaving out the Supabase policy is the documented variant.
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:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
Step 3
Build and handoff
One project price. Source code, documentation and IP are yours.
Two ways to get Bookable events
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/bookable-eventsBuild it with Timerise
Send a brief. We build Bookable events 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
Generated from the skill's own files at commit 03762ac. Every rule above links to where the repository says it. All skills