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.
// 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.
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.
-- 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.
-- 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
// 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
OS detection + theme system — zero flash
Synchronous detection before React, 8 themes, localStorage persistence
Auth + RLS — zero bypass surface
Supabase Auth, PBKDF2 via Web Crypto API, PostgreSQL row-level security policies
Two-round booking validation — race-free
Parallel availability check, atomic claim procedure with SELECT FOR UPDATE
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.