All Case Studies

Divine Booking

Salon Booking Platform

A booking platform I designed and built for an Ontario salon. Two surfaces, a staff app for the receptionist and a public booking page for the client, sharing one calendar that the salon trusts will be correct tomorrow morning.

Role
Founding Designer and AI Builder
Year
2026
Services
Product Design, UX, UI, Brand, AI Builder
Industry
Salon Tech, SaaS
Duration
Ongoing
Deliverables
Staff App, Public Booking Page, Brand System, Reminder Pipeline
Scroll Down
DivineBooking brand visual

Challenge

The salon was running on a fifteen-year-old desktop app where a cancellation meant colouring the appointment black, reminders did not exist, and the receptionist held everything in her head. The product had to replace a category, not a feature, and it had to handle two extremes at once. A receptionist who needs to drop a new client into a slot in twenty seconds while the phone is ringing, and a client who is half-distracted on a phone deciding whether to book a haircut between two emails.

Approach

Two surfaces with two operating philosophies. The staff app is dense, optimistic, and built for muscle memory. The public booking page is calm, generous, and built for one decision per screen. Underneath, a service is a fully loaded template, a cancellation is a status not a delete, and a phone-only client is a first-class state in the data model, not an edge case.

Outcome

A real product in pilot. I designed and built it solo with Claude Code over a few months. Staff app, public booking page, a daily reminder pipeline, and a printable call list for phone-only clients. Three hundred test appointments imported successfully from the old system in under a minute. The salon cuts over in two weeks.

The screen the receptionist opens at eight.

I designed this around what she actually needs to know in the first ten minutes of her shift. Today's bookings, confirmed revenue, the no-show rate trend, and the number of phone calls she has to make before the day starts. The right rail surfaces the day's call list with phone numbers ready, because phone-only clients are the part of the workflow that breaks if it lives in a side menu.

DivineBooking staff dashboard
Staff dashboard, Tuesday morning

Colour belongs to the staff member, not the status.

Most booking products use colour for status. Booked is one colour, confirmed is another, completed is a third. The result is a grid of beige with red and green sprinkles. I went the other way. Each staff member gets a colour the day they're added, and that colour follows them everywhere. The calendar column header, the appointment tile, the staff filter chip, the dot next to their name, and the event the system pushes to their personal Google Calendar.

The receptionist's primary scanning task at the calendar is "whose column is this in," not "what status is this." Status sits in the tile border and the timestamp underneath. Colour becomes a navigational primitive, and the day-view feels like four people instead of a generic schedule. When a stylist glances at their phone between clients, their own colour is on their work event too. Identity follows the work.

DivineBooking day calendar with staff columns colour-coded
Day view, four staff columns
DivineBooking new appointment dialog
New appointment, service auto-filled

Pick the thing and the system knows what the thing is.

I sat with the receptionist once and she pointed at the screen and said, "I just want to pick the thing and have it know what the thing is." So I made a service a fully loaded template. The duration, the colour, the eligible staff, the prep and cleanup buffers all populate the second she chooses one. She types the client name and the start time. The rest is already correct.

The dialog also surfaces the client's history at the top. "Usually Women's haircut with Lize, 60 min. Last 3 visits averaged $84." That panel exists because the receptionist's hardest moment is the client she has not booked in a year. The system remembers what she would not.

The most important promise I made was that the calendar would be correct tomorrow morning.

In the salon's old desktop tool, cancelling an appointment just turned the box black on the day. There was no record left of what had been cancelled, by whom, or why. Fifteen years of history sat that way. I designed the opposite. No appointment is ever hard-deleted. A cancellation is a status change. The row stays in the database, and on the calendar it renders as a greyed, hashed tile in the original time slot with a strikethrough on the client name. When the receptionist scans the day she can see exactly what happened. Including what did not.

DivineBooking edit appointment view
Edit appointment, cancellation visible in the timeline

Memory the receptionist would not have.

The client detail page is built around the question the receptionist asks at the counter: "What did we do last time?" The appointment history at the centre answers it. The notes column on the right surfaces things stylists wrote weeks or months ago, ranked by recency, with the staff member's name on every note. The right rail predicts the next booking based on cadence — "books roughly every 10 weeks, last visit 8 weeks ago, due about a month from now" — so the receptionist can call before the client drifts.

The badges across the top encode the client's communication preferences. Active, email and phone. Or in the case of a phone-only client, no email at all and the badge changes. The data model accepts a client row without contact info because some real clients have none, and the reminder pipeline routes them to the printed call list instead of silently failing.

DivineBooking client detail
Sarah Klassen, client since 2021
DivineBooking import flow using AI to parse calendar exports
Import, AI on the on-ramp not on the runway

I used a language model for one thing on purpose.

Every booking tool ships an AI feature this year. A "draft a reminder for me" button. A "suggest a time" tool. I argued against all of that. The actual problem in front of me was migration. The salon was booked into December in the old system, and the only export format was an iCalendar file with summary strings written by humans. "Sarah Klassen - Women's haircut with Lize" in one row. Just "Women's haircut" in another. No deterministic parser handles all of them.

So I built one path. The receptionist drops in a calendar export. The importer parses what it can deterministically, then sends the unclear summary strings to Claude Haiku with the workspace's services and staff as context. Claude returns structured fields. The importer fuzzy-matches, creates missing services and staff on the fly, conflict-checks against the booking engine, and inserts. Three hundred appointments come in in under a minute. After that, the AI is gone. The daily product is a calendar, not a copilot.

Emails for everyone with one. Printed call list for everyone without.

Reminders did not exist in the old system. The receptionist arrived in the morning with a stack of sticky notes. I designed the reminder pipeline so it runs at a time the salon picks (6 PM the night before, by default), sends an email to every client with one, and routes phone-only clients to a printable PDF that lands on the dashboard at run-time. The PDF is the artifact that actually moves the salon's no-show rate. Some of the salon's regular clients deliberately do not use email, and a real-world receptionist does not have time to dig through a contact list to find them.

DivineBooking reminder settings
Reminders, with phone-only as a first-class state

One decision per screen. Calm enough to hold a distracted phone.

I built the booking page as a separate app with its own deployment, its own middleware, and its own database role that literally cannot see any data outside the tenant it's serving. The two products diverge visually on purpose. The staff app is dense and optimistic. The booking page is generous and slow in the boring sense. Big serif headlines for the brand moments, big tap targets for the decisions, and a held-slot message on the time picker because Tuesday afternoons fill up quickly and the client should feel like the system is on their side.

Pick a service
Pick a service
Pick a date
Pick a date
Pick a time
Pick a time
Your details
Your details
Confirmed
Confirmed
Email receipt
Email receipt

Four more design decisions I would defend in a critique.

Each one had a more conventional alternative I argued myself out of.

01

Drag between staff columns to reassign.

The conventional pattern is an edit modal with a staff dropdown. It is more discoverable and it works on touch. I went the other way because the receptionist does this fourteen times in a row when a stylist calls in sick. Three seconds per drag versus ninety seconds of modal clicks. Touch users get a fallback edit dialog, but the receptionist is on a desktop all day and the keyboard is for typing client names. I would revisit if the product picked up a stylist-side mobile use case.

02

One-way Google Calendar push. We never read back.

Two-way sync is what Calendly does, what Acuity does, what Square does. It is the obvious thing and it is also the problem that looks two days and is actually two years. Watch channel infrastructure, webhook retries, scope expiry, quota errors, manual edits on a phone diverging from the database. One-way push gives the staff their work appointments on their phone in two days of work. Two-way sync is a Phase 3 problem I am not crossing until somebody pays me to.

03

Optimistic UI everywhere, with real rollback.

I watched the receptionist double- and triple-click during quarter-second spinner states in the old tool. The audit log filled up with phantom appointments. So every action commits to the local UI immediately and waits for the server to confirm or reject. If the server rejects, the UI rolls back and a toast appears. The trade-off is real engineering on the rollback path and a yellow offline banner at the top of the app so a network blip never looks like a successful action.

04

The booking page is its own app, deployed separately.

Sharing routes and auth state under one app would have been fewer moving parts. I split them because the security blast radius is completely different. The staff app is authenticated and every query is scoped to a workspace. The booking page is public and the whole world can hit it. The booking page's database role literally cannot see anything outside its own tenant because the workspace is bound at request time. One careless query in a shared app is a tenant data leak. Two apps, two operating philosophies, both correct for what they do.

Four features I shipped and then took back out.

The work is more useful with the path than without.

×

The custom-fields system for client profiles.

I built an editor where the owner could define per-category custom fields for clients. Nobody had asked for it. The salon owner looked at it for thirty seconds and said it made no sense, and she was right. The editor was confusing and the field types were a leaky abstraction. I removed it last week. The data column survives in the schema in case a future tenant actually needs it.

×

The new-owner product tour built on Joyride.

The dismiss flag lived in browser localStorage, which was fine until the owner switched to her phone for an evening and the tour fired again. I moved the dismiss state to a server-side per-user row. The localStorage shortcut stayed to avoid the tour flashing back during navigation, but the source of truth lives in the database now.

×

The clever overlapping-appointment layout.

I designed a side-by-side half-width tile layout for two appointments at the same time on the same staff member. I built it, deployed it, looked at it, and realised the situation it was solving was the situation we had spent two months making impossible at the data layer with a Postgres exclusion constraint. The constraint is the real fix. The visual handling of a state that cannot exist is dead weight.

×

Almost-shipped two-way Google Calendar sync.

I had the watch channel logic drafted. I caught myself one Saturday and asked what problem I was actually solving beyond "every competitor has it." I could not give myself a non-prestige answer. I pulled it. The complexity was going to fund a feature the receptionist did not need.

Premium enough to put in front of a client. Boring enough not to distract from twelve hours of work.

The brand had to do two contradictory things at once. Linen and ink, with a deep aubergine accent I picked because it survived all four of the salon's likely client demographics. The supporting palette goes warmer with pearl cream, champagne, mauve, and rose so the surfaces have variety without ever shouting. Display serif for the brand moments. Humanist sans for everything the receptionist actually reads.

DivineBooking brand system
Brand system, linen and ink with an aubergine accent
Type

Playfair Display and Satoshi.

An editorial serif for the brand moments, like the booking page hero and the dashboard greeting. A humanist sans for everything the receptionist reads at small sizes for ten hours straight. Serif type at 11px is unreadable and I am not making her squint.

Voice

Calm. Specific. No exclamation marks.

A booking is not a celebration. A confirmation is not a party. The product talks the way a quiet receptionist talks. Forbidden words include innovative, seamless, empower, leverage, solution, and platform. The product is a tool that books appointments.

Errors

Tell the user what to do next.

"Email or phone required for new clients" is fine. "Oops, something went wrong" is not. Every error message names the field and the action. No generic apologies, no exclamation marks even on the bad day.

1 Designer
start to finish
2 Apps designed
and built
3 Months from
idea to pilot

The next milestone is one full Tuesday on the new system, with the calendar still correct at the end of it.

The salon is two weeks out from cutover. After that the roadmap is payments, reporting, and recurring memberships. None of that ships until the receptionist's first full week of real bookings has gone through it without losing a single one.