Bike rentals, hop-on buses, timed museums, escape rooms, free welcome vouchers, driver-gated shuttles, FareHarbor-connected attractions — none of them get their own code path. Every product is five dial settings; every booking is the same hold. Tap through nine real products:
Capacity is never a stored counter that drifts. A booking holds units for a time window; free capacity is simply what's not overlapped right now. Expire, cancel, or return early — the space comes back on its own. Free products ride the same rail — they just skip the charge.
This one machine closes today's oversell bug. The hold is taken before payment, atomically — two guests racing for the last seat can't both win, and an abandoned checkout gives the seat back by itself.
A product often bundles several bookables under one card. They divide two ways, and flattening them into a single option list is the classic engine mistake: it is why child vs adult and classic vs electric look alike yet must be modelled as opposites. One is a price on shared stock; the other is a pool of its own.
No new primitive. A variant is the five dials at a finer grain — its own stock, its own optional overrides for when, how many, and how long. A fare is a lens on the hold — it changes price, headcount, or length, never inventory. The hold machine already counts both: free capacity is still “units not overlapped”, now asked per variant.
Your airport shuttle. On FareHarbor this is phone calls and guesswork. Here it's one dial: who confirms → threshold. Bookings pool up; each card is authorized, not charged, and nobody pays unless the trip actually runs. If it never fills, the hold is released and the guest is never charged at all — see payments §06.
Some sellers live in FareHarbor, Ventrata, Bókun — the systems behind hop-on-hop-off fleets and big-city observation decks. Fine: the checkout, concierge and calendar only ever speak four verbs to a booking core. Behind it sit two engines — ours, and a connector speaking OCTO, the industry's open connectivity standard. A seller flips one dial: inventory lives → connected.
Fast in practice: month calendars come from a short-TTL cache; picking a time does a live check; paying places the supplier hold. Supplier webhooks + a nightly re-sync keep the mirror honest — and if their API is down, we show the cache, refuse new holds, and never double-charge.
On most products we are the seller and the booking is the ticket. On others — Alcatraz is ours — we are a reseller: we hold a real allotment and take the money, but the operator issues the boarding voucher and emails it. Promising a QR we never mint is how a guest ends up at the pier with nothing to show.
redemption: ticket
redemption: email · voucher_eta_minutes
redemption: none
The dials don't change. Fulfilment is a property of the product, not a sixth dial: the same hold, the same capacity, the same checkout. All that changes is what the guest is told to expect once the money is taken — which is the one thing a resold product must get right.
Next to the catalogue sits This Week — the lobby tasting, the morning ride, the rooftop party. Two of those you turn up to; one you buy a ticket for. The temptation is to give events their own price, capacity and checkout. That is a second booking engine hiding inside the events table, and it will drift from the first one within a month.
guest_events · experience_id: null
{ when: none + open(date) · cap: seats(n) · dur: point · conf: instant }
This is the NYE party, wearing a different hat. The dials already describe a one-night ticketed thing — a closed schedule with a single open date, a seat count, instant confirm. A paid event is that product, created from a two-field form instead of the full product editor. Events never learn what a hold is.
The guest sees one list. Underneath, the row records who owns it — which decides who may edit it, and whose guests it reaches.
A seller event is still a hotel event. It is an ordinary hotel-scoped row that also carries a seller id, so This Week, the event page and the hotel’s own feed render it with no special case. Ownership is the only new idea: the seller’s writes are scoped to their own rows, and the hotel’s writes skip anything a seller owns.
Each nasty case resolves through one mechanism — the hold, the pool, the exceptions layer, the threshold, or the connector. No special-case code.
window_end = now() → that bike is instantly rentable again.poolFareHarbor's moat is distribution breadth. Ours is the hotel itself — plus a capacity model they don't have. We don't out-FareHarbor FareHarbor; we plug into it and win the cases it fumbles.
Add availability_rules + availability_exceptions; compute calendars from rules. The museum's 313 rows become one rule + holidays. Old tables stay live in parallel.
Bookings gain window + qty + expiry; the atomic hold() replaces the floor-at-zero decrement. Oversell closed; capacity self-healing; $0 products skip capture.
resource_units makes bikes and escape rooms bookable; the threshold dial launches the shuttle; the OCTO connector onboards FareHarbor/Ventrata sellers behind the same four verbs.
new availability_rules
new availability_exceptions
new resource_units
new departures
new connections
alter bookings + window, qty, hold_expires_at, provider_ref
Departure rows materialize on demand — an empty calendar stays purely computed; a row exists only once someone books toward it. The database stores demand, never emptiness.