11 November 2026 · 3 min read
Never sell the same night twice.
How a direct booking flow holds dates, takes payment and confirms exactly once, even when two guests reach for the same nights and the network misbehaves.
A booking engine has one job that matters more than all the others: a night can only be sold once. Everything else, the photos, the search, the nice calendar, is decoration on top of that rule.
When I built the direct booking flow for Luminous, the rule had three enemies. Two guests can pay for the same dates at the same moment. The payment provider confirms asynchronously, sometimes twice. And the property management system that owns the calendar (Hostaway) only hears about a booking after we tell it, so for a while our site is the only one that knows.
Price on the server, always
The browser shows a quote, but it never gets to decide what the guest pays. When the guest presses Reserve, the server recalculates the stay from scratch: nightly rates, cleaning fee, extra guests and the listing's own tax rules. The amount sent from the page is ignored. It sounds obvious; it is also the first thing that goes missing when a checkout is built in a hurry.
Hold the dates inside a transaction
Before any money moves, the dates are claimed. In one database transaction the server checks for anything overlapping those nights and, if they're free, writes a pending booking that holds them for a short window:
await db.$transaction(async (tx) => {
const clash = await tx.booking.findFirst({
where: { listingId, status: { in: ["pending", "paid", "reserving", "reserved"] },
checkIn: { lt: checkOut }, checkOut: { gt: checkIn },
OR: [{ status: { not: "pending" } }, { holdUntil: { gt: now } }] },
});
if (clash) throw new DatesTaken();
await tx.booking.create({ data: { listingId, checkIn, checkOut, status: "pending", holdUntil, paymentIntentId } });
});
If two guests race, one transaction wins. The other gets a clear "those dates were just taken" message, and the payment it had started is cancelled before the card is charged. Nobody pays for a stay they can't have.
Let the webhook be quick, and the work be careful
The payment provider tells us a payment succeeded through a webhook. Webhooks retry, arrive late and occasionally arrive twice, so the handler does as little as possible: verify the signature, mark the booking paid, answer 200 straight away. The slow part, creating the reservation in Hostaway, recording the payment against it and emailing the guest, runs right after the response.
That slow part is written to be safe to run more than once. It starts by claiming the booking with a conditional update, moving it from paid to reserving only if it is still paid:
const claimed = await db.booking.updateMany({
where: { id, status: "paid" },
data: { status: "reserving" },
});
if (claimed.count === 0) return; // someone else is already on it
Two copies of the same webhook can't both get past that line. The confirmation email has the same kind of guard: it is sent only if it hasn't been sent before.
Assume something will stall
Networks fail between "paid" and "reserved". So a small scheduled job sweeps every few minutes for bookings that are paid but not yet reserved, and finishes them. In practice it rarely finds anything; it exists so that "rarely" never becomes a guest standing at a door with no booking.
Show availability from every source
The calendar a guest sees isn't just Hostaway's. It is Hostaway's calendar, plus reservations already synced from other channels, plus the site's own bookings that are paid or still being reserved. That last layer covers the gap between our payment and Hostaway catching up, which is exactly when a double booking would otherwise slip through.
What I'd keep from this
- Decide prices and availability on the server, inside the same step that claims the dates.
- Make the step that talks to the outside world idempotent, then let anything retry it.
- Give stalled work a sweeper instead of hoping it never stalls.
None of it is exotic. It is a short list of rules applied every time, which is most of what reliable systems are.