Skip to main content
A photographer carrying a tripod and camera bag stands on a highway at night, looking toward a chain of lit towns in the distance. A gold route line runs between them, with empty booking slots floating above each stop.

How I Built a Production Booking System with Claude Code

A photographer wanted bookings and payments on his own site instead of a platform. I estimated eight hours. It took eighteen, and the hours went somewhere I did not expect.

Alex GomezAlex Gomez
12 min read

And everything that turned out to be harder than I expected.

A few weeks ago I picked up a project for a photographer whose site we built about three years ago. A homepage, an about page, a contact form. It did what he needed at the time.

Since then the business has grown. He had been taking bookings through Airbnb and was tired of watching a real chunk of every booking go to platform fees. He wanted bookings on his own site, with payments running through Stripe instead, where the cut is a fraction of what he had been paying.

So the job was: add scheduling, take payments, and make sure people can only see and change their own bookings.

This is the first project I have built where the authentication and authorization actually mattered. Not in a "this is good practice" way. In a "if I get this wrong, real money moves and a real business gets double booked" way.

Here is how it went, including the parts that took longer than I thought. Which was most of it. I estimated eight hours and it took eighteen.

The stack

Supabase for the database and auth. Stripe for payments. Claude Code for most of the building.

He already runs his schedule out of Google Calendar and is not going to stop doing that because I built him a booking page. So the site needs to know when he is already busy. We connected it through the iCal feed rather than the Google Calendar API.

That is a deliberate tradeoff and worth being upfront about. An iCal subscription is pull based, so it refreshes on a delay rather than instantly. For a business doing this volume, a delay is acceptable. For a high volume operation it would not be, and you would want proper two way sync through the API. I made that call knowing what I was giving up.

The booking flow's first step, choosing a location and a session type, showing real sessions and live pricing for Niagara Falls.The front door. Real sessions, real pricing, live.

How I actually work with Claude Code

I think this part matters more than anything else in the post, because it is the difference between getting something solid and getting something that looks finished and is not.

For a change this size I use plan mode and the brainstorm skill, and my first prompt is very long. It covers the full scope of what I am building, what needs to happen for authentication, authorization, the database, and payments, plus the business rules I had already worked out in conversations with him and my own preferences from past projects on how this sort of thing should behave.

Then Claude comes back with questions. I answer them, we go back and forth, and the architecture comes out of that conversation.

If you asked me whether I came up with the final design or Claude did, I honestly could not give you a clean answer. It came out of the dialogue. But the dialogue was only useful because I showed up with enough context to have it. If I had opened with "build me a booking system," I would have gotten something that ran and was much worse.

What went smoothly

Payments and auth. Both of them, basically first time.

I expected these to be the scary parts and they were not. Stripe checkout and Supabase auth are two of the most heavily documented integrations on the internet. There are thousands of tutorials, official docs, and example repos covering exactly these patterns, and Claude Code has clearly seen a lot of them.

I did verify the authorization properly rather than assuming it was fine. I checked that data is only visible to authenticated users and that a logged in user cannot reach or modify someone else's records by changing an ID. That came back clean, but I checked, and I would not skip that on any project where the data belongs to somebody.

The live Stripe checkout page for a Couples Session at 270 Canadian dollars, with Apple Pay and card payment options.Live checkout, real currency.

The decisions that mattered

A few architectural choices are the reason this project has held up, and they are worth spelling out because they are not obvious if you have not been burned before.

All scheduling and payment logic runs server side. None of it is trusted to the browser.

The database enforces race conditions. If two people try to grab the same slot at the same moment, the database is what says no, and the user gets a clear message rather than a silent double booking. This is not something you can reliably solve in application code.

The booking record is written before payment is attempted. It gets a pending payment flag. This means there is never a state where money has moved but no record exists of what it paid for.

Failed or abandoned payments hold the slot for 30 minutes. If someone's card fails, they get a window to sort it out without losing the time to somebody else. If they never come back, the hold releases on its own.

None of that is exotic. But it is the kind of thing that has to be decided up front, because retrofitting it into a working system is miserable.

The admin bookings view showing a Niagara Falls Couples Session on December 1 from 9 to 10 AM, flagged pending payment.The record exists before the money does. This is the slot held while the customer sits on the Stripe page.

The same booking in the admin view, now flagged cancelled after the payment hold expired.The same booking, minutes later. They never came back, so the hold released itself and the slot went back on sale.

Where it actually got hard

Scheduling. Every time.

The problem is not fetching the calendar feed. The problem is that once you connect an external calendar, you have two sources of truth about when someone is busy, and they do not agree about what time it is.

His calendar events, my availability table, timezone offsets, daylight saving, and buffer time between sessions all have to reconcile into one answer to a simple question: can somebody book this slot or not.

And then there is the part I had not fully thought through when I gave my estimate: he does not work in one place.

He has to physically get there

He shoots across southern Ontario. Toronto, Niagara Falls, Hamilton, Barrie, Kitchener, Mississauga, and more. A booking is not just a block of time. It is a block of time in a specific city that he has to drive to.

So there is a table of every pair of locations, and for each pair he enters how long the trip takes. Toronto to Niagara Falls is 100 minutes. Toronto to Hamilton is 60. Toronto to Mississauga is 30. Those numbers include his standard 30 minute buffer between sessions, unless a particular session type has its own buffer configured, in which case that one wins.

The travel times admin tab, listing each pair of cities with an editable minutes field: Toronto to Niagara Falls 100, Toronto to Hamilton 60, Toronto to Barrie 120, Toronto to Kitchener 140, and Toronto to Mississauga, Brampton and Vaughan at 30 each.Every pair of cities, with his own numbers. The arrows go both ways, and so does the constraint.

Two things about that design are worth saying out loud.

The numbers are his, not an API's. I could have wired this to a maps service and computed drive time automatically. I did not, and I would make the same call again. A routing API tells you how long it takes to drive between two points. It does not know that this location has no parking, or that loading and unloading gear adds twenty minutes, or that he refuses to cut it fine on the QEW on a Friday. He has driven these routes for years. His estimate is better than the API's, and it is his business, so he should be able to change it in ten seconds without me deploying anything.

The constraint runs in both directions. This is the part that ate my hours.

If he is booked in Niagara Falls in the morning, the system cannot just check whether the Toronto afternoon slot is empty. It has to know where he is coming from. A 10am finish in Niagara plus 100 minutes of travel means the earliest he can start in Toronto is 11:40, so the noon slot is the first one that can be offered.

It runs forward too. If someone books Toronto at 2pm, that reaches backwards and closes off part of the Niagara morning, because he now has to be in Toronto by two.

So availability is not a lookup. Every existing booking on a given day reshapes what is bookable at every other location for the rest of that day, in both directions, and each new booking reshapes it again. A slot being empty does not make it available. It has to be reachable from wherever he already has to be, and it has to leave him able to reach wherever he has to be next.

The Toronto booking page for December 1, showing no morning section at all and a first available slot of 12:00 PM.Toronto on December 1, the same day as that Niagara Falls booking. There is no morning section at all. The travel table above says Toronto to Niagara Falls is 100 minutes, the Niagara session ends at 10, and the first slot offered here is noon.

That is a constraint problem wearing a calendar's clothes, and it is the single biggest reason an eight hour estimate turned into eighteen.

It is also, I think, the general lesson of this build. Payments felt dangerous and went in cleanly because that pattern has been written a hundred thousand times. Working out whether a photographer can get from Niagara Falls to Toronto in time for a two o'clock is not in anybody's tutorial. The reliability tracked how common the problem was, not how much it mattered to the business.

The risk is not in the parts that feel hard. It is in the parts that are uncommon.

The tests kept passing

This deserves its own section because it nearly cost me.

Early on I asked Claude to write tests. Sensible. The problem is that as I kept building, the tests stopped describing what I was actually building. They had been written against an earlier version of the logic. I would add or change behaviour, run the suite, and get a wall of green.

The tests were passing honestly. They were just passing against code that no longer existed in that form.

This is not Claude writing bad tests. They were fine when written. It is that models are very good at building and, unless you explicitly tell them to, will not go back and clean up. Maintenance is not a step that happens on its own.

I caught it by hand. I opened his calendar on one screen and the booking app on the other and started checking them against each other. He was booked on a Saturday from 7pm to 8pm, so I went to the app and tried to book that same Saturday at that same time. If everything was working it should have refused, and it should have blocked the buffer around it too.

It let me book it.

I worked through it with Claude Code until the conflicts resolved, and I knew it was actually fixed the same way I found it: that Saturday slot disappeared from the available times, and so did the other dates he was already booked for. Then I went back and expanded the test suite to cover what we had actually built rather than what we had built three weeks earlier.

The numbers

I have built similar workflows before, so I estimated around eight hours.

It took eighteen. Three weeks of calendar time at an hour or two most days.

My rate is $100 an hour, so this was an $1,800 build: a custom booking system with payments and calendar sync, added to a site that already existed.

Here is the part worth sitting with. If I had quoted off my own estimate, I would have quoted $800. I would then have worked ten extra hours for free and lost a thousand dollars on a job I thought I understood, on a workflow I had done before, with AI help the whole way.

I am not going to tell you this would have taken six weeks without Claude Code, because I do not know that and neither does anyone else making that claim. What I can tell you is that it came in at more than twice my own estimate, and I would still build it this way again. The speed is real. It just does not show up where you expect, and it does not make your estimates better.

What I would tell you

Bring the business rules yourself. The build is fast now. Knowing what to build is the part that is still entirely on you, and it is where the value is. Nothing in my first prompt about travel time came from Claude. It came from the fact that I knew he drives to Barrie.

Decide the money and concurrency architecture before you write anything. Server side, database enforced, record before payment. Those are hard to add later.

Be suspicious of the uncommon parts, not the scary ones. The integration with a thousand tutorials behind it will go in fine. The thing specific to this business is what will eat your week.

Estimate the uncommon parts separately, and pad them. Not the whole project, just those.

And verify the important paths by hand at least once. Open the two screens and compare them yourself. A passing test suite tells you the tests passed, which is not the same thing as the software working.

The final step of the booking form, collecting name, email, phone and an optional venue address, with a Cloudflare Turnstile check showing success.The last step before payment. The bot check was decided at the same time as everything else, not bolted on afterwards.

The system is live now and taking real bookings across southern Ontario, which means the first thing it ever did in production was correctly refuse to let someone book a slot that was not actually free.

Frequently asked questions

How long does it take to build a custom booking system?

This one took eighteen hours of build time spread over about three weeks, for a booking flow with Stripe payments, Supabase auth, calendar sync and multi-location travel constraints, added to a site that already existed. I estimated eight hours, and I had built similar workflows before. The overrun was almost entirely in the scheduling logic, not the payments.

Should a small business build its own booking system instead of using a platform?

It depends on volume and on what the platform is costing you. This client was losing a real share of every booking to platform fees, and Stripe's cut is a fraction of that, so a one-time build pays for itself over time. If you take a handful of bookings a month, a platform is almost certainly the cheaper answer.

Can AI coding tools like Claude Code build production payment systems?

The common, heavily documented integrations go in cleanly, and Stripe checkout and Supabase auth both worked close to first time. The risk is not in the parts that feel dangerous. It is in the parts specific to your business that nobody has written a tutorial about, because those are where the model has the least to draw on and where you have the most to contribute.

Why would a passing test suite still miss a real bug?

Because tests written early describe the system as it was when they were written. As the logic changed around them, they kept passing honestly against behaviour that no longer existed in that form. Models are very good at building and will not go back and maintain tests unless you tell them to. The bug surfaced only when I checked the live calendar against the app by hand.

Share this article

Help others discover this content

Follow The Biz Spark

How Ready Is Your Business for AI?

Find out in 2 minutes. Our free assessment scores your business across the areas that actually matter and shows you exactly where to focus.

Take the Free Assessment
© 2026 The Biz Spark™. All rights reserved.
Privacy Policy | Terms of Service

The Biz Spark · Web design & development in Thorold, Ontario · Serving St. Catharines, Niagara Falls, Welland, and the Niagara region

Follow us on social media: