CRC: parents booking in week one.Read more

Technology

Supabase vs Convex for a Booking App: Which to Pick

For most booking apps we would pick Supabase, because Postgres can refuse a double booking at the database level and your reports are plain SQL. Convex is the better pick when the live, shared calendar is the product.

Nate Laquis12 min read

The Short Answer: Supabase for Most Booking Apps

Pick Supabase for a booking app unless live collaboration is the whole product. Postgres lets you tell the database that two bookings for the same resource may never overlap, and it will enforce that rule no matter which code path tries to break it. Pick Convex if your booking screen is a shared, constantly changing grid that many people watch and edit at once, and your team would rather write TypeScript than SQL.

That is the verdict for the comparison most founders search as "supabase vs convex booking system". It rests on how booking software fails. Nobody complains that the calendar updated 200 milliseconds late. They complain about two customers holding the same slot, a reminder sent at the wrong hour, and a payment that cleared while the booking did not exist. Each of those is a data-integrity problem, and Postgres has the oldest and sharpest tools for it.

Dashboard of charts and tables showing booking activity over time

This post goes through the booking-specific parts one at a time: double-booking, time zones, recurring slots, Stripe webhooks, reminders, realtime availability, access control, reporting, pricing at small scale and lock-in. If you want the general comparison that includes Appwrite, read Convex vs Supabase vs Appwrite first. Here we only care about one product shape.

Double-Booking: Constraints vs Serializable Mutations

This is the section that should decide it for most readers, so it comes first.

How Postgres handles it

Postgres has exclusion constraints. With the btree_gist extension you declare that, for the same resource, the time ranges of two rows may not overlap. You store each booking as a tstzrange (a start and end with time zone) and add a constraint along the lines of "resource id equal, range overlaps, reject". After that, an insert that collides fails with a clear error, whether it came from your web app, a mobile app, an admin script, a support person editing a row in the dashboard or a bug you wrote at midnight.

That last part matters. A rule in application code protects you only from the code that remembers to check it. A constraint protects you from everything, including future you. You can also wrap the check and insert in a transaction at the serializable isolation level if the rule is more complex than "no overlap", for example "a coach may have at most four sessions a day".

How Convex handles it

Convex mutations run as transactions with serializable semantics. Under the hood it uses optimistic concurrency: a mutation reads the data it needs, computes its writes, and if another mutation changed what it read in the meantime, Convex retries it automatically. So a mutation that says "look for an overlapping booking, and if none exists, insert" is safe. Two customers clicking the same slot at the same moment will not both succeed. One will win and the other will see the slot as taken on retry.

That is good design, and it is simpler to reason about than most people expect. The catch is where the rule lives. Convex has no exclusion constraint. The no-overlap rule exists in your mutation function, and only there. Every write path has to go through that one function, and anyone editing data by hand in the dashboard can break the invariant. A disciplined team can live with that. A team with three contractors and a deadline will eventually write a second path.

The practical difference

Both prevent the race. Postgres also prevents the human mistake, and it does so with one line of schema instead of a convention. For a business where a double booking costs a refund and a bad review, we want the database to say no. That is the first point for Supabase.

Availability, Time Zones and Recurring Slots

Availability is the part of a booking app that looks easy and eats the schedule. Two facts shape the backend choice.

Time zones

Store every instant in UTC and store the business location's IANA zone name, such as America/New_York, as a separate field. Never store an offset like -05:00 as the zone, because daylight saving changes it twice a year. A 9:00 a.m. appointment in March is a different UTC time than a 9:00 a.m. appointment in January, and your recurring rules have to be expanded in the location's zone, not in UTC.

Postgres is strong here. It has a real timestamptz type, a built-in zone database, and the AT TIME ZONE operator, so a query like "all slots that fall on a Saturday in the shop's local time" is one expression. In Convex you store numbers or ISO strings, and all of that zone logic happens in your TypeScript, usually with a library such as Luxon or date-fns-tz. That works, and many teams prefer one language. But you carry the whole burden yourself, and you cannot ask the database a time-zone question in a report without exporting the data.

Recurring slots

Recurring availability ("Tuesdays 9 to 5, except the first Tuesday of the month") and recurring bookings ("every Thursday at 6") need a model, and the model is the same on either backend. Store the rule, not the occurrences, and generate concrete slots for a rolling window, say 60 days. Store exceptions as separate rows. Then the exclusion constraint or the mutation check applies to the generated, concrete bookings.

Where Postgres helps is in the generating. A generate_series query can expand a weekly rule into dated slots and subtract existing bookings in a single statement. On Convex you write that loop in a function. Neither is hard. If you want the full model for slots, buffers and calendars, our guide to building a scheduling app goes through it step by step.

Calendar and clock on a desk representing appointment scheduling across time zones

Payments and Stripe Webhooks

Taking a deposit sounds like one feature. It is a small distributed system: your server creates a Stripe Checkout session, the customer pays on Stripe's page, and Stripe tells you about it later through a webhook. Meanwhile the slot has to be held, and released if payment never arrives.

The reliable shape is the same on both backends:

  1. Create a booking row with status pending and an expiry a few minutes out. The no-overlap rule counts pending rows, so the slot is held.
  2. Create the Checkout session and store its id on the booking.
  3. When the checkout.session.completed webhook arrives, flip the booking to confirmed. Make this idempotent, because Stripe retries and may deliver the same event twice.
  4. A scheduled job cancels pending bookings past their expiry and frees the slot.

On Supabase the webhook lands in an Edge Function, which verifies the Stripe signature and updates the row. The idempotency check can be a unique constraint on the Stripe event id, so a duplicate delivery fails harmlessly. On Convex you receive the webhook in an HTTP action, verify the signature, and call an internal mutation. Because the mutation is transactional, the "check event already handled, then confirm" logic is naturally safe. Both are fine.

The difference is again about who enforces the rule. The unique constraint on the event id is one line in SQL. In Convex it is a lookup inside the mutation. If you go on to pay out to providers with Stripe Connect, the webhook count grows, and the more webhooks you have, the more you will appreciate constraints doing the guarding.

Reminders and Scheduled Work

Reminders are where Convex is clearly pleasant. When a booking is confirmed, the mutation can call the scheduler to run a function at an exact future time, for example 24 hours and two hours before the start. The scheduled call is stored with the transaction, so if the mutation fails, the reminder is never queued. If the customer reschedules, you cancel the scheduled function by id and queue a new one. Convex also has cron jobs for the recurring sweeps.

Supabase can do this too, with a different shape. You use pg_cron to run a job every minute that selects bookings whose reminder is due and not yet sent, then calls an Edge Function that sends the SMS or email through a provider like Twilio or Resend, and marks the row sent. It is a polling design rather than a per-booking timer. It handles reschedules for free, because it reads the current start time every run, and it is easy to inspect: the "reminders due" list is just a query you can run by hand when a customer says they never got a text.

Which feels better depends on your team. Convex's per-booking scheduling is more elegant. Supabase's polling is more boring and easier to debug at 7 a.m. on a Saturday when the first appointments are due. For a business where a missed reminder means a no-show, we lean boring.

Realtime Availability UI

Here Convex wins, and the win is real. Every Convex query is reactive by default. You write a query that returns open slots for a day, subscribe to it in React, and when another customer books a slot the screen updates without you writing any subscription code. The cache stays consistent with the database. For a calendar grid that a front desk, three providers and a customer app all look at together, it removes a category of bugs.

Supabase has Realtime, which streams Postgres changes over websockets. It works, and for a booking app the pattern is simple: subscribe to changes on the bookings table for the date range on screen and refetch or patch local state. You write more of that wiring yourself, and you decide how to handle reconnects and missed events. For a typical customer flow, where someone picks a slot and the worst case is a "sorry, that slot was just taken" message from the constraint, this is plenty. We have a separate look at the realtime options in Ably vs Pusher vs Supabase Realtime.

Be honest about whether you need live updates at all. A customer-facing booking page does not need a live grid. It needs a fast availability query and a clear error when someone else wins the slot. Live updates matter for staff tools: a dispatcher view, a shared front-desk calendar, a classroom or studio with walk-ins. If that staff view is the heart of your product, move toward Convex. If the heart is the customer booking flow, the realtime gap is small.

Laptop showing an analytics dashboard used for managing bookings

Access Control: Row-Level Security vs Auth Functions

A booking app has at least three kinds of user: a customer who sees only their own bookings, a provider who sees their own schedule, and an admin who sees everything. Getting this wrong leaks other people's names and phone numbers.

Supabase uses Postgres row-level security. You write policies such as "a customer can select rows where customer_id equals their user id" and the database applies them to every query that comes through the client libraries. The upside is strong: even a frontend bug cannot read a row the policy forbids. The downside is that policies are SQL, they are easy to get subtly wrong, and a missing policy on a new table is a silent hole. Test them. Write a short test per role that tries to read someone else's booking and expects nothing.

Convex has no row-level security. Access control is code: each query and mutation checks the caller's identity, usually through a helper like "get current user, throw if missing", then filters. You get full TypeScript for the logic, which makes complex rules (a provider sees bookings for locations they are assigned to) pleasant to write and to unit test. The risk is the mirror image of the benefit: every function must remember to call the helper, and one function that forgets is a leak. Review discipline and shared wrappers fix most of it.

Neither is safer by nature. Supabase gives you a default-deny floor once you enable RLS. Convex gives you flexibility and asks for discipline. For teams that change often, we prefer the floor.

Reporting, SQL and Pricing at Small Scale

Reporting

Booking businesses live on questions: bookings by week, utilization per provider, no-show rate by service, revenue by hour of day, how many repeat customers came back within 60 days. On Supabase these are SQL queries, and your accountant, your analyst or a BI tool like Metabase can run them straight against Postgres. On Convex you either write functions that aggregate, which gets slow and awkward as data grows, or you export data into a warehouse and report from there. Convex supports streaming export for exactly that, so it is solvable, but it is one more piece to build and pay for. If the owner will ask a new question every month, SQL is a real advantage.

Pricing at small scale

We checked both vendors' pricing pages on the day this post was published, and plans change, so treat the figures as a snapshot and confirm before you commit.

  • Supabase: a free plan with 500 MB of database and 50,000 monthly active users, with free projects paused after a week of inactivity. Pro starts at $25 a month with 8 GB of disk and 100,000 monthly active users included.
  • Convex: a free and starter plan with 1 million function calls, 0.5 GB of database storage and 1 GB of bandwidth included, then pay as you go. Professional is $25 per developer per month with 25 million function calls and 50 GB of storage included.

For a booking app with a few thousand bookings a month, both bills are small, and neither should drive the decision. Two differences are worth knowing. Supabase's free tier pauses idle projects, so a launch-week demo can greet a customer with a cold start, which is a reason to start on Pro for anything customers touch. Convex bills per developer on its paid plan, so a team of five costs more than a solo builder, while Supabase charges per project. Realtime-heavy screens on Convex also count function calls and bandwidth, which is worth modelling if a staff calendar polls all day.

Lock-In and Migration

Lock-in is where the two backends differ most, and it is a business question, not a technical one.

Supabase is Postgres. Your schema, your data, your constraints and your SQL all move with a dump. If Supabase vanished tomorrow, you would point the same database at any other Postgres host and rewrite the auth and storage glue. Supabase is also open source and can be self-hosted. That is the safest long-term position for a business whose customer and booking data is the asset.

Convex is a different data model with its own query API, functions and reactive client. The backend is open source and can be self-hosted, which softens the risk, but your application code is written against Convex's API throughout. Moving off means rewriting the data layer, not just changing a connection string. That cost is acceptable if Convex saves you months at the start. It should be a decision you make on purpose, not a surprise in year three.

A rule we use: if the data is relational and reporting-heavy, as a booking business is, pick the store you can leave. If the product is a live collaborative surface, pick the tool that makes that surface cheap to build, and accept the tie.

Which One for Your Product

Here is how we would map real situations to a pick. If you are weighing the whole build, our walkthrough of building an AI appointment booking app shows where the backend sits among the other pieces.

  • A salon, clinic or studio with a customer booking page and a staff calendar: Supabase. The no-overlap constraint, SQL reports and boring reminders fit the work.
  • A multi-location business with resources like rooms and equipment: Supabase. Exclusion constraints per resource are exactly this case.
  • A marketplace where providers set their own hours and payouts run through Stripe Connect: Supabase, for the relational data and the reporting you will need.
  • A live dispatch board or shared scheduling surface where many staff edit at once: Convex. Reactive queries save real engineering time.
  • A small TypeScript team shipping an MVP in a few weeks that expects modest volume: Convex is a fair pick, provided every write goes through one booking mutation and you plan an export for reporting.
  • A booking site where discovery and local search matter more than the calendar: neither choice is the hard part. Our Community Run Camps case study is a booking site where the camp finder, program pages and local SEO did the work of getting parents to enroll, and parents were booking the week it went live. That is a reminder to spend your first effort on the page that gets found, then the backend that takes the reservation.

For most of these the choice is close enough that your team's skills should break the tie. If you are still unsure, the broader Convex vs Supabase vs Appwrite post is the next read, because it covers the general trade-offs outside booking.

Kanopy helps founders pick a stack and build the product on it, so if you want a second opinion on your booking model before you write a line of code, book a free strategy call.

supabase vs convex booking systembooking app backenddouble booking preventionsupabase for booking appconvex for scheduling app

You picked the stack. We ship the product.

Thirty minutes. You leave with a plan and a number.

Book a free call