Calendars, conflict prevention, deposits, no-show fees, and the timezone work nobody warns you about — mapped step by step for a solo founder.
Research-based methodology. This guide synthesizes Stripe documentation, Supabase RLS docs, public Calendly/Cal.com architecture writeups, and our own builds with Claude. Where we have first-person experience we say so; otherwise we’re working from public sources. How we research.
Booking is the rare SaaS vertical where the underlying problem hasn’t been solved at the long tail. Calendly owns the meeting-scheduling category. Cal.com owns the open-source story. Acuity, Square Appointments and Vagaro have a chunk of the small business market. But under all of those there are still thousands of operator types — dog groomers running a Notion doc, yoga studios charging through a deprecated MindBody plan, photo studios DM’ing on Instagram — whose “booking system” is a calendar, an email, and a hope. That’s the wedge for a solo founder in 2026.
This guide is for someone who wants to ship a real, paid booking product in 2–6 weeks using Claude as their primary thinking partner. You’ll spend less time on UI and more time on the boring, load-bearing parts: timezone math, double-booking prevention, deposit capture, and confirmation flows that actually arrive in the customer’s inbox. If you want the broader build playbook first, our How to build a SaaS with Claude guide covers the general scaffolding workflow this one builds on top of.
Most builders underestimate booking. The visible product is a calendar with some clickable slots and a payment button. The actual product is a small distributed system that has to keep three calendars (yours, your provider’s, and the customer’s) in agreement at all times.
You will store everything in UTC. You will display in the provider’s timezone for the dashboard, the customer’s timezone for the booking page, and the venue timezone for the confirmation. A 9am Tuesday yoga class in Denver is a different UTC instant during DST than it is in November. Get this wrong once and your “Tuesday 9am” class shows up at 10am for half your customers in the spring.
If you check “is this slot free?” in your application code and then insert the booking, two concurrent customers on the same slot can both pass the check before either insert lands. The fix is a Postgres exclusion constraint or a unique partial index on (provider_id, starts_at) — we’ll generate it with Claude in step 2. Trying to handle this in JavaScript leads to angry customers double-charged for the same slot.
The moment you add Google Calendar or iCloud sync, you’re responsible for two-way state. Provider creates an event in their personal Google Calendar — your DB doesn’t know about it — you let a customer book over it. The fix is webhook-driven sync (Google Calendar push notifications, CalDAV polling for iCloud) and treating your DB as the source of truth for paid bookings only.
For a meeting tool, no-shows are an annoyance. For a paid service (training session, photo shoot, dog grooming), no-shows are the difference between a profitable week and a losing one. If you don’t collect a deposit or capture a card on file, you’re building Calendly. If you do, you’re building something operators will pay $29–$49/month for.
The data model for a booking SaaS is small but unforgiving. You need services (what can be booked), providers (who delivers them), availability_blocks (when they’re free), bookings (the actual reservation), and payments (deposit and final charge state). Get the relationships right once with Claude and you won’t fight them later.
I'm building a booking SaaS for [yoga studios / dog groomers / photo studios]. The model needs to support: - A studio/business with multiple providers - Each provider offers one or more services with different durations and prices - Providers publish recurring weekly availability AND one-off blocks - Customers book a specific service with a specific provider for a specific time - Each booking can require a deposit (Stripe) and a final charge - A booking has lifecycle states: pending_payment, confirmed, completed, no_show, cancelled - All times stored in UTC; each business has a timezone Design the complete Postgres schema. For every table give me: - Columns with types and constraints - Foreign keys with ON DELETE behavior - Indexes for the hot queries (look up free slots for a provider on a date) - A separate column or constraint approach for double-booking prevention - Audit columns: created_at, updated_at, cancelled_at Then write the matching Supabase RLS policies so: - A business owner can read/write only their own business's data - A public anonymous user can read services + free availability for booking - Booking writes from the public page go through an RPC, not direct insert Output as one SQL file I can run in the Supabase SQL editor.
Spend 30 minutes refining Claude’s output. The most common miss is the relationship between availability_blocks (recurring rules) and bookings (point-in-time reservations). A booking does not consume an availability block — it just has to fit inside one at the moment of insert. Claude will get this right if you push back on its first draft when it tries to model availability as discrete slot rows.
This is the single most important piece of code in your product. If two customers try to book the same provider at 3pm on Tuesday, exactly one should succeed. Not “mostly one.” Exactly one.
The right primitive is Postgres’s exclusion constraint with btree_gist, which can enforce “no two rows where the time ranges overlap for the same provider.” The Supabase docs cover the extension; the SQL is short but easy to get subtly wrong.
I have a `bookings` table with columns:
- id uuid primary key
- provider_id uuid references providers(id)
- starts_at timestamptz not null
- ends_at timestamptz not null
- status text -- one of: pending_payment, confirmed, completed,
no_show, cancelled
I need a Postgres exclusion constraint that prevents two bookings for the
same provider from overlapping in time, BUT only when status is in
('pending_payment', 'confirmed'). Cancelled and no-show bookings should
not block new bookings.
Output:
1. The CREATE EXTENSION statement for btree_gist if needed
2. The ALTER TABLE adding the exclusion constraint with a WHERE clause
3. A `book_slot` Postgres function (SECURITY DEFINER) that the public
booking page calls via supabase.rpc(). It should:
- Take provider_id, service_id, customer_email, starts_at
- Compute ends_at from the service's duration
- Insert a booking row with status='pending_payment'
- Return the booking id, or raise a clean error if the slot is taken
4. The Supabase RLS that lets anonymous users execute this RPC but not
directly INSERT into bookings
Explain in comments why a Postgres function is safer here than a server
route checking availability and then inserting.
Claude will produce a function that uses the exclusion constraint to fail fast on a conflict. If you skip this and try to do the check in your Next.js route, you will eventually have a race condition. Solo founders who skip this end up issuing apology refunds at 8am on Saturday.
The public page is what your customers’ customers see. It is also the page Google indexes for the “book a yoga class in Boulder” long-tail searches that drive your operators’ traffic. Spend time on it.
The interaction is conventional: pick a service, see a calendar with available days, pick a day, pick a time, enter your name and email, pay deposit, confirm. The hard part is the time grid: it must render in the customer’s local timezone (detected from the browser), it must reflect the operator’s real availability minus existing bookings, and it must update in near-real-time if someone else grabs a slot while the page is open.
Build a public booking page at /book/[business-slug]/[service-slug] using
Next.js App Router and Supabase.
Requirements:
- Server component fetches the business, service, and the next 30 days of
available slots via a Postgres function `get_open_slots(provider_id,
from_ts, to_ts)` that I will write
- Client component renders a horizontal scrollable date strip + a 30-min
vertical time grid for the selected day
- All times displayed in the visitor's browser timezone, but submitted to
the server as UTC ISO strings
- When a slot is clicked, open a side drawer with name, email, phone
fields and a "Continue to deposit" button
- On submit, call `supabase.rpc('book_slot', {...})` and on success
redirect to /book/[business-slug]/checkout/[booking_id]
- Handle the "slot just got taken" error with a clear inline message and
refresh the slot grid
Use Tailwind. Mobile-first, since 70%+ of bookings happen on phones. Make
the whole flow work without JavaScript-heavy state — URL params for
selected date and slot so the back button does the right thing.
One detail Claude will not give you unprompted: render the next available slot prominently above the grid (“Earliest opening: tomorrow at 2:30pm”). Operators tell us this single change lifts conversion by double digits because most customers don’t want to scan a calendar — they want to know if they can get in soon.
For booking, you almost certainly want Stripe over Lemon Squeezy. Lemon Squeezy is a merchant-of-record — great for SaaS subscriptions but awkward for in-person service bookings where the legal seller is the studio, not you. Stripe Connect lets each operator onboard as their own merchant, so the deposit flows to their account and you take a platform fee. Our best payment processor for SaaS guide covers this tradeoff in depth, and the Lemon Squeezy vs Stripe comparison is the right read if you’re still on the fence.
I'm using Stripe Connect (Express accounts) so each business is a
connected account. I need:
1. An onboarding flow at /dashboard/payments that creates an Express
account, generates an Account Link, and stores the account_id on the
business row
2. A Next.js route handler /api/checkout that:
- Accepts a booking_id
- Loads the booking + service + business (with stripe_account_id)
- Creates a Stripe Checkout Session in payment mode for the deposit
amount (e.g., 30% of service.price)
- Sets payment_intent_data.application_fee_amount for my platform fee
- Sets transfer_data.destination = business.stripe_account_id
- Sets metadata.booking_id so the webhook can correlate
- Returns the checkout URL
3. A webhook handler /api/webhooks/stripe that:
- Verifies signature with STRIPE_WEBHOOK_SECRET
- On checkout.session.completed: marks the booking confirmed,
stores payment_intent_id, sends the confirmation email
- On checkout.session.expired: cancels the booking row so the slot
opens back up
- Saves the customer payment method for later no-show charges
(setup_future_usage)
Reference the official Stripe Connect docs for direct charges with
application fees. Use the latest API version.
Critical: setup_future_usage on the checkout session is what lets you charge the no-show fee later without the customer being present. Skip this and your no-show policy is decorative.
The confirmation email is not a side effect — it’s part of the product. Customers screenshot it, forward it to their partner, calendar-block from it. If it doesn’t arrive in 30 seconds, they assume the booking didn’t work and they re-book or call.
I'm using Resend for transactional email and Twilio for optional SMS. Build a Supabase Edge Function `send_booking_confirmation(booking_id)` that runs after the Stripe webhook marks a booking confirmed. It must: 1. Load booking + service + business + customer 2. Build an .ics calendar attachment (RFC 5545) with: - The booking time in UTC with the provider's IANA timezone (TZID) - A reminder VALARM 60 minutes before - The business address as LOCATION 3. Render a Resend email with: - Subject: "Confirmed: [service] with [business] on [local date/time]" - Body: time displayed in customer's submitted timezone - Cancellation link (signed URL valid until starts_at - 24h) - Reschedule link - Add-to-Google-Calendar quick link - The .ics as attachment for Apple Mail / Outlook users 4. If business.sms_enabled is true and customer.phone is set, send a short Twilio SMS with the same time + a link to the booking page 5. Insert a row into a `notifications` table for audit 6. On any failure, retry with exponential backoff up to 3 times Use Resend's React Email templates for the HTML email so I can edit the template visually later.
Two non-obvious wins: send the .ics file as an attachment and include an “Add to Google Calendar” link — different customers will use different ones. And put the cancellation deadline in the email body in the customer’s words (“cancel by Monday at 6pm to get your deposit back”), not in policy language.
The no-show fee is the feature that justifies your price. It’s also the feature most likely to be misconfigured. Here’s the working pattern: a Vercel Cron or Railway worker runs every 15 minutes, finds bookings where ends_at < now() - interval '30 minutes' and status = 'confirmed', and prompts the provider in their dashboard to mark each one as completed or no_show. If the provider marks no_show, you create an off-session PaymentIntent against the saved payment method for the no-show fee amount.
Don’t auto-charge no-show fees without provider confirmation. The cost of one wrongful charge in customer goodwill is much higher than the cost of a small UI delay.
Two viable models for booking SaaS:
Avoid pure free trials longer than 14 days — booking businesses test with one or two real bookings, then either commit or churn. A 14-day trial with a credit card on file (charge on day 15) converts dramatically better than a 30-day no-card trial.
Generic “appointment scheduler” is a bloodbath. Calendly, Cal.com and Acuity will out-feature you. The win is a niche where a generic tool is 70% wrong and the operator is willing to pay for the missing 30%.
Each of these is a real micro SaaS idea with thousands of operators in the US alone. Pick one. Talk to ten of them before you write a line of code. Build the booking page they’ll send their customers to first; everything else (admin, analytics, marketing automation) comes after.
A booking SaaS that prevents double-booking with Postgres constraints, captures a deposit at booking time, and ships a clean confirmation flow is already better than 80% of what small operators currently use. Pick a vertical, talk to ten operators, ship the public booking page first.
The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.