Production App · Live · Manzini, Eswatini

Studio P Barbershop
Production Booking Platform

Live booking system for Studio P, Manzini. OS-aware UI (zero layout flash), two-round parallel booking validation, Supabase row-level security, 8 colour themes with localStorage persistence. React 19 + TypeScript strict mode.

React 19 TypeScript Supabase PostgreSQL RLS PBKDF2 Web Crypto API Vercel Vite
8 Colour themes
0ms Layout flash on load
2 Validation rounds
100% RLS coverage
0 Auth bypasses

A Real Barbershop. Real Production Load.

Studio P is a real working barbershop in Manzini, Eswatini. Before this app, bookings were taken via WhatsApp and managed manually — double-bookings happened regularly, peak hours were unpredictable, and there was no customer history.

The app handles real production load: appointments, barber availability, service selection, payment intent creation, and customer profiles — all behind row-level security policies enforced at the database layer.

OS-Aware UI

Theme applied synchronously on first render via userAgent/platform/maxTouchPoints. iOS → iOS style. Android → Material. Desktop → Desktop. Zero layout flash because detection happens before first paint.

Two-Round Validation

Booking creation uses two sequential validation rounds. Round 1: check time slot availability (parallel for all barbers). Round 2: confirm no conflicting bookings in the window since Round 1 (race condition protection).

Supabase RLS

Every table has PostgreSQL row-level security policies. Customers see only their own bookings. Barbers see their assigned appointments. Admin sees all. Policies enforced at the database — no server-side filtering needed.

PBKDF2 Auth

Customer passwords hashed with PBKDF2 via Web Crypto API — no server round-trip for hash computation. Salt + iterations stored with hash. Supabase Auth for session management; custom PBKDF2 for additional password audit trail.

OS-Aware UI — Zero Layout Flash

Most apps apply themes after React hydrates, causing a visible flash as the correct styles load. This app detects the OS synchronously in a blocking script (before React renders) and sets a data-os attribute on <html> — so the first paint already has the correct layout, border radii, font weights, and button styles for the user's platform.

index.html — <head> blocking script TypeScript · DOM
// Applied synchronously in <head> — before React renders
(function detectOS() {
  const ua = navigator.userAgent;
  const platform = navigator.platform;
  const touch = navigator.maxTouchPoints;

  let os: 'ios' | 'android' | 'desktop' = 'desktop';
  if (/iPhone|iPad|iPod/.test(ua) || (platform === 'MacIntel' && touch > 1)) {
    os = 'ios';
  } else if (/Android/.test(ua)) {
    os = 'android';
  }
  document.documentElement.setAttribute('data-os', os);
})();
// CSS: [data-os="ios"] .booking-btn { border-radius: 12px; font-weight: 500; }
// CSS: [data-os="android"] .booking-btn { border-radius: 4px; font-weight: 600; text-transform: uppercase; }

Because the script runs synchronously before any HTML is parsed or React is loaded, the attribute is present before the browser paints a single pixel. CSS selectors keyed on [data-os] apply immediately — no flash, no FOUC, no layout shift.

Two-Round Booking Validation

The race condition: between when a user loads the booking form and when they submit, another user could claim the same slot. Single-round validation doesn't catch this — you check, it's free, you write, but so did someone else 40ms earlier.

Round 1 runs parallel queries to check current availability. Round 2 uses a Postgres function with SELECT FOR UPDATE to atomically claim the slot — only one concurrent request can hold the lock.

lib/booking.ts TypeScript · Supabase
async function createBooking(slot: BookingSlot): Promise<BookingResult> {
  // Round 1: check availability (parallel for all barbers in this hour)
  const [available, conflicts] = await Promise.all([
    supabase.from('time_slots').select('*').eq('slot_time', slot.time).eq('available', true),
    supabase.from('bookings').select('id').eq('slot_time', slot.time).eq('status', 'confirmed'),
  ]);
  if (!available.data?.length || conflicts.data?.length) {
    return { success: false, reason: 'slot_taken' };
  }

  // Round 2: re-check in a transaction (catches race condition between rounds)
  const { data, error } = await supabase.rpc('claim_slot_atomic', {
    p_slot_time: slot.time,
    p_barber_id: slot.barberId,
    p_customer_id: userId,
  });
  return { success: !error, bookingId: data?.id };
}

The claim_slot_atomic Postgres function uses SELECT FOR UPDATE to lock the row — prevents double-booking even under concurrent requests hitting the API simultaneously.

supabase/migrations/claim_slot.sqlPostgreSQL
-- Atomic slot claim — prevents double-booking under concurrent requests
CREATE OR REPLACE FUNCTION claim_slot_atomic(
  p_slot_time  TIMESTAMPTZ,
  p_barber_id  UUID,
  p_customer_id UUID
) RETURNS TABLE(id UUID) LANGUAGE plpgsql AS $$
BEGIN
  -- Lock the row — blocks concurrent calls for the same slot
  PERFORM 1 FROM time_slots
    WHERE slot_time = p_slot_time AND barber_id = p_barber_id AND available = true
    FOR UPDATE;            -- row-level lock, released on commit

  IF NOT FOUND THEN
    RAISE EXCEPTION 'slot_unavailable';
  END IF;

  UPDATE time_slots SET available = false WHERE slot_time = p_slot_time AND barber_id = p_barber_id;
  INSERT INTO bookings(barber_id, customer_id, slot_time, status)
    VALUES (p_barber_id, p_customer_id, p_slot_time, 'confirmed')
    RETURNING bookings.id;
END;
$$;

Supabase RLS Policies

Row-level security is enforced at the database layer, not the application layer. Even if the API layer had a bug, the database would reject unauthorized reads and writes. All three roles (customer, barber, admin) have precisely scoped policies.

supabase/migrations/rls_bookings.sql PostgreSQL · RLS
-- Customers see only their own bookings
CREATE POLICY "customers_own_bookings" ON bookings
  FOR SELECT USING (auth.uid() = customer_id);

-- Barbers see their assigned appointments
CREATE POLICY "barbers_see_assigned" ON bookings
  FOR SELECT USING (
    auth.uid() IN (
      SELECT user_id FROM barbers WHERE id = bookings.barber_id
    )
  );

-- Admins see everything (role stored in profiles table)
CREATE POLICY "admin_all" ON bookings
  FOR ALL USING (
    (SELECT role FROM profiles WHERE id = auth.uid()) = 'admin'
  );

PBKDF2 password hashing via Web Crypto API

auth/crypto.tsTypeScript · Web Crypto
// Runs in browser — no server round-trip for hashing
async function hashPassword(password: string): Promise<string> {
  const enc   = new TextEncoder();
  const salt  = crypto.getRandomValues(new Uint8Array(16));
  const key   = await crypto.subtle.importKey(
    "raw", enc.encode(password), "PBKDF2", false, ["deriveBits"]
  );
  const bits  = await crypto.subtle.deriveBits(
    { name: "PBKDF2", salt, iterations: 310_000, hash: "SHA-256" },
    key, 256
  );
  // Store as "salt$hash" — both hex-encoded
  return bufToHex(salt) + '$' + bufToHex(new Uint8Array(bits));
}
// NIST SP 800-132 recommends ≥ 310,000 iterations for PBKDF2-SHA-256 (2023)

8 Colour Themes

Each theme is stored as a CSS custom property set, applied via [data-theme] on <html>. User preference is persisted to localStorage. On page load, the preference is read and applied in a blocking <head> script — same pattern as OS detection — so there is zero flash between the default and the user's chosen theme.

Onyx

Default dark. Deep charcoal background, blue accents. Studio-professional aesthetic.

Pearl

Clean light mode. White surface, slate typography. Matches iOS system appearance.

Crimson

Dark base with red/rose accent palette. High contrast for outdoor readability.

Ocean

Deep navy with cyan highlights. Calm, professional — popular with evening bookings.

Forest

Dark green base, emerald accents. Earthy, premium feel.

Sunset

Warm amber and orange on dark. High energy, distinctive.

Midnight

Near-black with purple accents. Lowest brightness — night use, OLED friendly.

Gold

Dark base with gold/yellow highlights. Premium barbershop aesthetic.

Build Sequence

1

OS detection + theme system — zero flash

Synchronous detection before React, 8 themes, localStorage persistence

2

Auth + RLS — zero bypass surface

Supabase Auth, PBKDF2 via Web Crypto API, PostgreSQL row-level security policies

3

Two-round booking validation — race-free

Parallel availability check, atomic claim procedure with SELECT FOR UPDATE

4

Production deploy — live at Studio P

Vercel, custom domain, real customer bookings in Manzini, Eswatini

Need a production booking or e-commerce app?

I build apps that handle real production load from day one — proper auth, RLS, race-condition-safe booking logic, and OS-native UI. Deployed for real businesses.