I Built a Real-Time Booking System From Scratch — Then Rebuilt It Twice

A friend of mine is a barber. He was using Booksy — the industry-standard booking app — and hated it. Limited customization, no real brand presence, another platform taking a cut. He asked if I could build something better.

110+
Hours Invested
2
Full Rebuilds
~2wk
Final Build Time
0
Lines of WebSocket Code

The result is kenshincuts.com — a custom booking platform with real-time availability, Stripe payments, and an admin dashboard with live updates. No templates. No third-party booking widgets. Everything from scratch.

This is the story of every major technical decision — including the ones that didn't work.

The Journey

The Build Timeline

Build 1: Medusa + Postgres + Redis

Started with the "enterprise" approach. Got close to a finished product but drowned in tech debt. Every small adjustment felt like paying interest on a loan I didn't mean to take.

Build 2: Medusa Rearchitect

Tried to salvage the Medusa build with a cleaner architecture. Same fundamental friction — SQL migrations, manual WebSocket plumbing, cache invalidation. The stack was fighting the product.

Build 3: Convex + Next.js

Reached feature parity in roughly two weeks. Then exceeded what I'd built before. The right tool made the difference.

What Went Wrong

The First Build: Medusa + Postgres + Redis

I started with Medusa v2, a headless e-commerce framework built on Node.js with Postgres and Redis. The idea was sound: Medusa handles products, carts, and payments out of the box. I'd extend it to support barber-specific concepts like time slots, service durations, and availability windows.

I got close to a finished product. The customization surface was powerful. But the problems compounded:

The Medusa Pain Points
  • SQL migrations for every schema tweak. Adding a field to the appointment model meant migration scripts, testing rollbacks, and hoping nothing broke in the ORM layer.
  • Real-time required manual WebSocket plumbing on top of Medusa's REST API. When a barber blocked off a slot, other clients wouldn't know until refresh.
  • Redis existed purely for caching, but cache invalidation logic grew with every feature. "Why is this stale?" became a recurring debug session.
  • Type safety eroded at every boundary — between the Medusa API, the Postgres schema, and the Next.js frontend. Types maintained in three places.

Despite getting close to done at least twice, the tech debt accumulated faster than features. Each "small adjustment" cascaded into hours of plumbing work.

Medusa Stack
Convex Stack

The Decision to Rebuild

This is the part most developers don't talk about: scrapping working code because the architecture is fighting you.

I didn't switch to Convex because it was trendy. I switched because I needed to get back to momentum. The original stack created constant friction between the frontend world (reactive, component-driven) and the backend world (imperative, migration-driven). The context-switching was killing my velocity.

💡Lesson Learned

The right tool isn't the most powerful one — it's the one that lets you ship. "Enterprise" infrastructure for a single-barbershop booking app was overkill from day one.

Why Convex Changed Everything

The Database Re-renders Like a Component

In a traditional architecture, keeping the UI in sync with the database is a manual chore. You're either stuck with inefficient polling or burying yourself in Pub/Sub complexity.

Convex flips the model. Queries are reactive by default — when underlying data changes, subscribed clients update automatically through a persistent WebSocket connection managed entirely by the platform. I didn't write a single line of socket code.

Just as React components re-render when state changes, Convex queries re-run and push updates whenever the underlying data changes. This isn't a metaphor — it's the technical reality.

When a barber updates their availability, every client viewing that schedule sees the change in milliseconds. Not because I built a notification system — because that's just how queries work.

Schema in TypeScript, Not SQL

The entire data model lives in a schema.ts file:

C:\KENSHINCUTS\schema.ts

When the schema needs to change, I change a TypeScript file — not a migration script. No rollback testing. No ORM configuration. I define an index and lookups stay fast as data grows.

Security as Regular Code

Medusa and Postgres pushed me toward Row-Level Security policies — powerful but notoriously hard to debug. Convex replaces that with ctx.auth.getUserIdentity() inside regular TypeScript functions.

SQL RLS Policy
Convex Auth Check

Mutations vs. Actions: Solving the Stripe Problem

Here's a scenario every developer dreads: a slow Stripe API call blocks your database transaction. If the API hangs, your database locks up.

Convex separates this cleanly:

  • Mutations — pure, deterministic database writes. Atomic. No external calls allowed.
  • Actions — the designated zone for external APIs. Stripe charges, email sends, webhooks.
ℹ️Serializable Consistency

Because mutations are pure, Convex handles concurrent conflicts automatically. Two customers booking the same slot simultaneously? Automatic retry with rollback. I didn't write retry logic — the system guarantees the mutation either succeeds entirely or rolls back cleanly.

No More Cache Invalidation

The Redis layer from my Medusa build? Gone entirely.

Time spent on cache invalidation (Medusa)
NaN%
Time spent on cache invalidation (Convex)
NaN%

Convex's reactive queries handle caching at the platform level. Results are reused and only recomputed when data actually changes. I stopped debugging "is this stale?" and started shipping features.

The Admin Dashboard

Where the Architecture Paid Off

The admin panel was where Convex's reactive model truly justified the rebuild. Barbers need to:

  • Manage services (add, edit, remove) with prices and durations
  • Set weekly availability windows
  • View and manage upcoming appointments
  • See new bookings appear in real-time

Every one of these is a live-updating view. When a customer books a 2pm slot, the barber's dashboard reflects it instantly — no refresh, no polling interval, no "new bookings" button to click. The data just appears because the underlying query's data changed.

⚠️What This Would Have Cost on Medusa

Building the same real-time admin on the original stack: a WebSocket server, event dispatching, client-side subscription management, and reconnection logic. On Convex, it was just... queries.

Retrospective

What I'd Do Differently

Start with the simpler stack

My instinct was to reach for the "enterprise" solution because it felt more serious. For a booking app serving a single barbershop, that infrastructure was overkill.

Timebox the first prototype

I let the Medusa build run too long before admitting the architecture wasn't serving the product. A stricter "if I'm fighting the framework after X hours, pivot" rule would have saved weeks.

Build the admin dashboard first

The customer-facing booking flow is simpler. The admin panel is where complexity lives — availability rules, service management, appointment conflicts. Starting there would have exposed the real-time requirements earlier.

The Stack

Final Architecture

Next.js 15
App Router, React 19, Turbopack
Convex
Database, real-time, server functions
Stripe
Payment processing
Clerk
Auth (integrated w/ Convex identity)
Tailwind CSS
Styling
Vercel
Frontend deployment

The gap between frontend and backend is shrinking. Tools like Convex don't just save time — they change what feels reasonable to build as a solo developer. A real-time booking system with live admin updates, transactional consistency, and integrated payments used to be a team-sized project. I built it alone in two weeks.

Not because I'm fast. Because the stack got out of my way.