Hair salon — immersive site and booking
A site that feels like the salon, with online booking and an iPad agenda of its own.
- Astro
- React
- Three.js
- GLSL
- Cloudflare Workers
- Cloudflare D1
- Resend
- Playwright
- 100Lighthouse accessibility, best practices and SEO
- 348 mslargest contentful paint
- 6.9 → 1.7 s3D ready on a phone, after tuning
Mirror, chair and a booking engine of its own
Tap a block to see what it does.
- Logic
- People
- Input
- Infrastructure
- Data
- Output
The heart of the system
The salon's own system
A WebGL home with the feel of the place, a booking engine the salon owns instead of a rented tool, an iPad agenda for the staff and reminders that go out on their own, all on Cloudflare.
Connected to Booking engine, Staff agenda, Booking wizard, WebGL home, Reminder cron, Cloudflare D1, SMS and email
Logic
Booking engine
Shared package behind 23 API routes: 30-minute grid, time zone with daylight saving, notice and cancel cut-offs, HMAC tokens for the manage link.
- Astro SSR
- Cloudflare Workers
- TypeScript
Gets from Booking wizard, Staff agenda, The salon's own systemHands to Cloudflare D1, SMS and email
People
Staff agenda
iPad and phone PWA: one column per stylist, long-press drag to move, new appointments, closures. Polls every 5 s with a version check so devices stay in sync.
- React
- PWA
Gets from The salon's own systemHands to Booking engine
Input
Booking wizard
React island in four steps: service, day and a free slot shown live, contact details, confirmation. Prices and durations come from the database, not from the page.
- React
- Motion
Gets from WebGL home, The salon's own systemHands to Booking engine
Logic
WebGL home
Marble-wall shader, a neon sign that flickers on, GPU hair strands per service. Device tiers and a sleeping render loop; no 3D or video with reduced motion.
- Three.js
- GLSL
- WAAPI
Gets from The salon's own systemHands to Booking wizard

Infrastructure
Reminder cron
Runs every 15 minutes. Raises the sent flag before sending, rolls it back if every channel fails, stops after 3 tries. Also anonymises data after 24 months.
- Cron Worker
Gets from Cloudflare D1, The salon's own systemHands to SMS and email
Data
Cloudflare D1
One conditional insert, COUNT(*) below capacity, makes double booking impossible: 201 for a new booking, 409 for a taken slot, 410 past the cut-off.
- Cloudflare D1
- SQL
Gets from Booking engine, The salon's own systemHands to Reminder cron
Output
SMS and email
Confirmation, change, cancellation and reminder, each logged in the agenda. Email carries an .ics invite; the SMS fits in 160 characters with the manage link.
- Resend
- SMS API
- .ics
Gets from Booking engine, Reminder cron, The salon's own system
The problem
A hair salon lived on Instagram and WhatsApp, and the booking tool it relied on was being shut down. It needed a site with the feel of the place (black marble, gold, a neon sign) instead of a template, plus a booking system and a staff agenda it would own.
What I built
- I built the home as a WebGL scene: a black-marble wall, a neon sign traced from the real logo, and thousands of GPU-drawn hair strands that change with each service and follow the cursor.
- I extended the booking engine from an earlier client project: 30-minute slots, local time with daylight saving, signed links to move or cancel, email with a calendar invite, SMS.
- I built an iPad agenda for the staff: one column per stylist, long-press to drag, arrived, paid and no-show states, refreshed every 5 seconds.
- I added a cron worker that sends reminders every 15 minutes and anonymises old customer data.
- I threw away the first version after a scored critique and restarted from a stronger concept, threads of light.
The hard part
Making a shader-heavy home usable on cheap phones. I added device tiers, parallel shader compilation and a render loop that sleeps when nothing moves. With CPU throttling, 3D-ready time on a phone dropped from 6.9 s to 1.7 s and blocking time from 4.8 s to 0.7 s.
Result
The site scores Lighthouse 100 for accessibility, best practices and SEO, with LCP at 348 ms and CLS at 0. The booking flow passes end-to-end checks (new booking, taken slot, too late), after 19 documented QA rounds and with 11 versions that can be restored.
My role
Solo project, directed end to end: I wrote the brand DNA, the rules and the specs, judged every version with a scored critique and tested on real phones. Claude Code wrote most of the code, and I had Codex build a parallel version as a bake-off.