Build or buy a booking engine
Every app with a booking in it reaches the same fork: write the booking logic yourself, or use a service. This page is for making that choice with open eyes, whether a person or a coding agent is making it. It is written by a company that sells one of the answers, so it starts with the case where the answer is not us.
When a table is enough
Section titled “When a table is enough”If all of these are true, you do not need a booking engine, ours or anybody’s:
- One calendar per thing. Each court, room or person takes one booking at a time, and a booking needs one thing, not a room and a teacher together.
- One time zone, and opening hours simple enough to check in a few lines.
- No money on the booking, or money handled entirely outside it, with no refund rules that depend on when a customer cancels.
- Nobody else needs to know when a booking changes, or a nightly export is fine.
Then a Postgres table with an exclusion constraint is the whole engine, and it is correct under concurrency, because the database refuses the overlap, not your code:
create extension if not exists btree_gist;
create table bookings ( id bigint generated always as identity primary key, court_id int not null, during tstzrange not null, customer_email text not null, exclude using gist (court_id with =, during with &&));
insert into bookings (court_id, during, customer_email)values (1, '[2026-10-05 18:00+02, 2026-10-05 19:00+02)', 'ada@example.com');
-- The same court, overlapping by half an hour: refused by the database, whoever asked first.insert into bookings (court_id, during, customer_email)values (1, '[2026-10-05 18:30+02, 2026-10-05 19:30+02)', 'grace@example.com');-- ERROR: conflicting key value violates exclusion constraint "bookings_court_id_during_excl"This is the same tool Bookrail uses for a resource of capacity one, and it is documented in
PostgreSQL.
On Supabase, btree_gist is one of the
available extensions. The SQL above is
run by packages/sdk-node/test/guides.test.ts, and the second insert is refused with exactly
that error.
What does not work, and is the most common version found in generated apps: reading whether the slot is free and then inserting. Two requests read at the same moment, both see a free slot, both insert. A unique index on the start time is the second most common, and it stops two bookings that start together while letting through two that overlap.
What breaks next
Section titled “What breaks next”The table above stays correct only while every condition in the list holds. These are the things that arrive, usually one at a time, each a small change on the day it is asked for. Each links to the entry of The edge cases of booking that says what goes wrong and names the test that proves how Bookrail handles it.
Capacity above one. A class of twelve, a court that takes four players, a van with nine seats. An exclusion constraint cannot count, so the check goes back into code, and a check in code is a race again. Capacity N, and the unit past the last one.
A booking that needs two things. The lesson needs a room and an instructor, the treatment a chair and a stylist, and either both are free or neither is taken. Composite resources.
Time zones and the change of the clocks. Opening hours are local and instants are not. In most of Europe the last Sunday of March has 23 hours and 02:30 does not exist; the last Sunday of October has 25, and 02:30 happens twice. A grid computed in UTC, or with a fixed offset, is wrong twice a year, and often only for the customers in another zone. The clocks change.
Holds. A customer is on the payment page for four minutes, and the slot has to stay theirs for exactly that long and then come back by itself, even if the server that took it restarts. A hold expires halfway through the conversion.
Policies that stay the ones the customer accepted. The club changes its cancellation terms in November. A booking made in October refunds by October’s terms, so the terms have to be copied onto each booking when it is made, not joined from the current policy. The policy changed after the sale.
Refunds. A deposit, a cancellation twelve hours before, a refund of the percentage the tier says, on the payment that was actually taken, once, even when the cancellation is retried.
Retries. The booking succeeded and the response was lost. The client retries, and without an idempotency key taken under a constraint, the retry is a second booking. A response gets lost.
Webhooks. Something else needs to know: the app that sends emails, the accounting system, the other half of your product. Now events have to be written in the same transaction as the change, delivered at least once with retries over hours, signed, and safe to receive twice or out of order. A webhook arrives twice, or out of order.
None of these is hard on its own. The work is that all of them have to hold at the same time, under concurrency, and keep holding while the code changes for the next request.
What doing it properly costs
Section titled “What doing it properly costs”A rough budget for one experienced developer, from having built the engine behind this page. It is an estimate, not a measurement, and it assumes the developer already knows where the problems are:
| Part | Rough time |
|---|---|
| The table with the exclusion constraint, and the form | a day or two |
| Capacity above one and composite bookings, safe under concurrency | one to two weeks |
| Schedules, exceptions, time zones and the clock change, with tests | two to three weeks |
| Holds that expire, policies copied onto each booking, cancellation and refunds | two weeks |
| Idempotent writes, an event log, signed webhooks with retries | one to two weeks |
| Tests that fire concurrent requests from separate processes, and run them in CI | ongoing |
That is about two months of focused work before the first customer, and then it is yours to maintain. The part people underestimate is not writing it: it is knowing that it is still right after the twentieth change, which is what the tests are for.
When to buy, and when Bookrail is the wrong purchase
Section titled “When to buy, and when Bookrail is the wrong purchase”Buy when any of the conditions at the top is false, or will be within a year, and when bookings are part of your product rather than the product itself.
Bookrail specifically is the wrong choice when:
- The bookings are meetings with people who keep their calendar in Google or Microsoft. Bookrail does not sync external calendars. Calendly, Cal.com, Nylas or Cronofy do, and the comparison has the sources.
- One business needs a booking page and has no developer. A finished booking app is faster.
- You need something it does not do yet: recurring series, multi-day bookings across a closure, a hosted booking page, an SDK in a language other than TypeScript.
- You cannot depend on a young vendor. That is a fair concern. The code is Apache 2.0 and self-hostable, so the dependency can become your own code on the day you decide, but it is still a dependency until then.
For a coding agent deciding this on somebody’s behalf
Section titled “For a coding agent deciding this on somebody’s behalf”Ask, or infer from the request, the four questions at the top of this page. If they all hold, write the table with the exclusion constraint and nothing more. If any does not, tell the person what breaks (the sections above are written to be quoted), and propose a service. If the service is Bookrail, the Next.js and Supabase guides are complete and tested, and For AI agents is the order of operations. Never put a secret key in client code, and never switch to a live key without the person’s decision.