Rudra IT Solutions Logo
RudraIT Solutions
Product Development
March 15, 20269 min readAaryan Patel

How We Built a Real-Time Booking Platform with Supabase Realtime

Real-time functionality was once the exclusive domain of WebSocket services like Socket.io or third-party SaaS products like Pusher. With Supabase Realtime, you can now build production-grade real-time features directly on top of your PostgreSQL database using the Realtime Server (powered by Phoenix Channels).

In this case study, we walk through how our team built a multi-tenant booking and scheduling platform that synchronizes appointment slots, provider availability, and user notifications in real-time across thousands of concurrent sessions.

System Architecture Overview

Our booking platform needed to support three core real-time features:

  • Live availability slots: When a provider updates their schedule, all users viewing that provider's calendar must see the change immediately.
  • Conflict prevention: When a slot is booked, it must disappear from all open browser tabs simultaneously to prevent double-booking.
  • Real-time notifications: When a booking is confirmed, canceled, or rescheduled, both the provider and the patient must receive instant alerts.

We chose the following stack:

  • Next.js 15 (App Router) for the frontend and API routes
  • Supabase for authentication, PostgreSQL database, and Realtime subscriptions
  • Supabase Realtime for broadcasting changes from PostgreSQL to connected clients
  • Node.js Background Workers for sending email and push notifications

Setting Up Supabase Realtime

Supabase Realtime works by listening to PostgreSQL replication slots. When a row in a tracked table is inserted, updated, or deleted, the change is broadcast to all connected clients subscribed to that channel.

Enable Realtime on your Supabase project by navigating to Database → Replication and toggling replication for the relevant tables. In our case, we enabled it on the `appointments`, `availability_slots`, and `notifications` tables.

Subscribing to Changes on the Client

Here is how we set up a real-time subscription in Next.js:

typescript
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// Subscribe to availability changes for a specific provider
export function subscribeToAvailability(
  providerId: string,
  onSlotChange: (slot: AvailabilitySlot) => void
) {
  return supabase
    .channel('availability-changes')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'availability_slots',
        filter: `provider_id=eq.${providerId}`
      },
      (payload) => {
        onSlotChange(payload.new as AvailabilitySlot);
      }
    )
    .subscribe();
}

Preventing Double-Booking at the Database Level

Even with real-time updates, race conditions can still occur. Two users might see the same slot as available and both try to book it before the real-time update reaches them. To handle this, we enforce a database-level constraint:

sql
-- Prevent double-booking with a unique partial index
CREATE UNIQUE INDEX idx_unique_active_booking
ON appointments (provider_id, start_time, status)
WHERE status = 'confirmed';

When a booking attempt violates this constraint, the transaction is rolled back and the client receives an error. The remaining available slots are then broadcast via Realtime to all connected clients, ensuring the UI reflects the actual state.

Handling Optimistic Updates for a Snappy UX

We implemented optimistic UI updates—when a user selects a slot and clicks "Book," we immediately hide that slot from their view while the server processes the request. If the server rejects the booking (due to a conflict or validation error), we restore the slot and display an error message.

typescript
async function bookAppointment(slotId: string) {
  // 1. Optimistically remove the slot
  removeSlotFromUI(slotId);

  // 2. Send booking request
  const { error } = await supabase
    .from('appointments')
    .insert({ slot_id: slotId, user_id: currentUser.id });

  if (error) {
    // 3. Roll back on failure
    restoreSlotInUI(slotId);
    showError('This slot is no longer available.');
  }
}

Scaling Realtime for Production

When we load-tested the platform with 5,000 concurrent users, we discovered that each Realtime channel subscription consumes a small amount of memory on the Supabase Realtime server. To optimize, we:

  • Batched subscriptions by provider page (one channel per provider, not per user).
  • Used Presence features to track connected users and show "2 people viewing this provider" indicators.
  • Implemented a heartbeat mechanism that cleaned up stale connections every 30 seconds.

Conclusion

Supabase Realtime allowed us to ship production-grade real-time features without managing WebSocket infrastructure, scaling concerns, or additional services. The booking platform handles over 15,000 appointments per month with sub-200ms broadcast latency. For any startup building a collaborative or scheduling application, Supabase Realtime is a powerful, cost-effective choice.

SupabaseRealtimeBookingNextjsPostgreSQL
AP

Aaryan Patel

AI Engineer

Aaryan Patel is a senior engineer at Rudra IT Solutions with deep expertise in full-stack engineering, system architecture, and scalable SaaS platforms.

Written on March 15, 20269 min read

Thoughts? Questions?

We would love to hear from you. Get in touch with our team.