Booking for people who work with their hands
A hairdresser in Madrid runs her week from a paper notebook. Citameya turns that into a link she sends once — and, when her hands are busy, a sentence she says out loud.



What shipped · 29 Apr → 23 Jul 2026
Three months, one person, one product.
01 Landing
Sell it to someone who has never booked online
The audience is not a salon chain with an office manager. It is one self-employed person, mid-haircut, being asked to trust software with the thing that pays her rent. So the promise is the first line, in her words: «Deja la libreta. Habla, y Citameya lo apunta.» — drop the notebook, speak, and Citameya writes it down.

02 Voice booking
Say it while your hands are busy
A form is the wrong interface for someone holding scissors. So you talk, and the fields fill themselves as you speak — one chip at a time, each tappable if the machine got it wrong. It runs on every public booking page, and this is the reason the product has the shape it has.
Phrases arrive as they are spoken. Nothing has left the device but text.
The running transcript is matched against this master’s own service list, once every three seconds.
Service, date, time, name, phone. Tap one to correct it and your edit is locked against later passes.
The recording gets a final, slower transcription that merges with your corrections instead of overwriting them.
Shipped as an experiment, not as an opinion
Live captioning could just as easily be noise — a row of chips jittering while somebody is trying to talk. So it went out as a real 50/50 split in PostHog, measured on a funnel from first tap to a booking actually created by voice, with iOS and Firefox excluded because they have no Web Speech to stream.
The experiment decides whether it stays. That is the point of building it behind a flag instead of shipping it because it demos well.
03 Booking page
The whole product is one link
citameya.com/maria-garcia — four steps, no account, no app. Services, day, time, details — with price and duration visible before the first tap, because the only questions a client really has are what it costs and how long she is there.




Colour is doing work here
Each service row carries its own pastel. Not decoration: on a page with eight services, the tint is what a returning client scans for instead of reading the list again. The price pill is always the same shape, in the same place.
The name on the row resolves through a 191-entry catalogue at render time, in the language of whoever is looking — not the language the master typed it in.
No calls.
No DMs.
One link.
as it runs on the landing: «Deja la libreta. Habla, y Citameya lo apunta.»
04 The master’s day
The screen she opens with wet hands
The client’s side is four steps and then it is over. This is the side she opens between two clients to answer one question: what is next. So the day is the default view, the week is one tap away, and every action she actually performs is a single round button at the top.
Before the day exists, there is the setup
Asking a hairdresser to type out thirty services with prices and durations is where onboarding dies. So the first question is only «¿A qué te dedicas?» — and answering it loads a slice of a 191-entry catalogue, already translated, with default durations she can correct. Three steps and the public page exists: no sales call, no import, no card.
The magic link goes out over WhatsApp as well as email, because this audience lives there and forgets passwords.



Distribution was a design problem, not a marketing one. The product cannot make her post on Instagram — but it can hand her a finished Story with today’s free slots already in it.
Creating a booking used to leave her looking at the wrong day. Now the view jumps to the date she just booked. That bug was invisible until you used the thing for a week.
Blocking time off needed its own calendar: a native date input cannot grey out days that are already taken, so she could only find out by getting an error.
05 Trust & no-shows
Everything here exists to stop an empty chair
A missed appointment is not an inconvenience for a self-employed hairdresser. It is an hour of income that does not come back. Four features, one job.
A deposit, when she wants one
Stripe takes a deposit up front on the services where a no-show hurts most. Per-master and off by default — charging a regular for a €15 beard trim is a good way to lose her.
Cancel by link
Every confirmation carries a tokenised cancel link. Letting a client cancel in one tap sounds like a loss; it is how the slot gets back on the market while it can still be sold.
A waiting list on a full day
A sold-out day used to be a dead end. Now it collects a name, a phone and a language — and when a cancellation frees the slot, that list hears about it first.
Reviews nobody invented
The scheduler that sends the reminder also asks for the review, and it lands on the page the next client reads. If nobody has reviewed, nothing is shown — there is no score to fake.

06 Notifications
The product also lives in WhatsApp
Nobody in this market opens a dashboard to learn that a booking arrived. WhatsApp, email and web push carry the product to people who never log in — and each message is written in the language of whoever receives it, which is not the same as the language of whoever caused it.
Templates, master-facing and client-facing
Ten scheduled jobs, all on Madrid time
07:30 the day’s bookings, before she opens
11:00 a nudge to try dictating one
10:00 forty-eight hours with no bookings — something is wrong
10:15 trial ends in two days
hourly registered, never shared the link
Language is stored per person, not per account: the master has a notify_lang, and every booking snapshots what the client was reading when she made it. One row in the database, two languages out.

07 Design system
Three components the platform forced
The base is shadcn/ui over Tailwind, driven by a single set of HSL tokens, so the public page and the dashboard are unmistakably one product. What is worth showing is not the token list — it is the three components that had to be written because the native browser control was broken on the phones this audience actually owns.
Palette · cyan is a fill, never text
Deja la libreta.
Pills — one component, five sizes
Every chip in the product is this one component: specialisations, languages, awards, booking statuses, and the voice chips in chapter 02. Unifying them was a single afternoon that removed five one-off styles.
The three components that were not on any plan
- NativeDateFieldiOS Safari draws an empty native date input as a blank box with no hint of what it wants. A placeholder is overlaid — only on iOS, only while empty, because on every other engine it doubled up.
- BlockCalendarA native date input cannot disable arbitrary dates, so a master could pick a day that was already blocked and only find out from an error. An inline calendar greys out past and blocked days instead.
- PhoneInputSix raw phone fields had six different ideas about the country code. One component with a locked + prefix replaced all of them.
Each of these came out of using the product on a real phone and catching the browser doing something a screenshot never shows.
08 Who it’s for
Two people, and the silence between them
Written as design hypotheses, not research findings — I did not run a study. But every screen is answerable to one of them, and the ones that were not got cut.
Job 01 · the autónoma
“I am booked out for the week, but I am also the receptionist — and I am doing that job with wet hands, from a notebook, while somebody is in the chair.”
Job 02 · the client at eleven at night
“I want the appointment now, while I am thinking about it — not to send a message and wait until tomorrow to find out if that time is even free.”
The dangerous moment is neither of those. It is the gap between booking and turning up, where a client forgets, double-books herself, or quietly decides not to come. The dip in the line below is the whole reason chapters 05 and 06 exist.
Taps a link from a bio, a Story or a reply
Reads services, price and duration
Day, time, name, phone — or dictates the lot
Waits, for days, with nothing to do
Turns up and pays the master directly
Gets asked for a review, rebooks from the same link
“Is this a real business or a dead page?”
“What does it cost and how long am I here?”
“Did that register, or should I message her too?”
“Was it Thursday or Friday?”
“Was that worth the price?”
“Do I have to find the link again?”
A profile with no way to book anything
“Precio a consultar” — the answer to nothing
A form demanding an account first
Silence — the single biggest cause of a no-show
The slot was double-booked on paper
Nothing carries over; next time starts from zero
A Story with real free slots and a QR
Price and minutes on the row, tinted per service
No account; confirmation on the spot
Reminder the day before, plus a one-tap cancel link
Server-side collision check — it cannot happen
Review request, follow-up, details remembered
Find
- Thinking
- “Is this a real business or a dead page?”
- Pain
- A profile with no way to book anything
- Answered by
- A Story with real free slots and a QR
Choose
- Thinking
- “What does it cost and how long am I here?”
- Pain
- “Precio a consultar” — the answer to nothing
- Answered by
- Price and minutes on the row, tinted per service
Book
- Thinking
- “Did that register, or should I message her too?”
- Pain
- A form demanding an account first
- Answered by
- No account; confirmation on the spot
Wait — the dip
- Thinking
- “Was it Thursday or Friday?”
- Pain
- Silence — the single biggest cause of a no-show
- Answered by
- Reminder the day before, plus a one-tap cancel link
Attend
- Thinking
- “Was that worth the price?”
- Pain
- The slot was double-booked on paper
- Answered by
- Server-side collision check — it cannot happen
Return
- Thinking
- “Do I have to find the link again?”
- Pain
- Nothing carries over; next time starts from zero
- Answered by
- Review request, follow-up, details remembered
09 Architecture
The app, mapped
One React app and one Express API. Five surfaces hang off it, and every one renders through the same tokens against the same nine tables.
Client
- Public page · /b/:slug
- Four-step wizard
- Voice booking
- Cancel · review · deposit
Master
- Day & week dashboard
- Clients & statistics
- Services, hours, blocks
- Share kit & promo codes
Notifications
- Transactional email
- Web push
- Ten scheduled jobs
Billing
- Stripe subscription · €15
- 20-day trial, no card
- Client deposits
- Webhook idempotency
Admin
- Activity feed
- Per-master drill-down
- Force-cancel
- Bulk test-data cleanup
10 How it was built
Three things the product taught me
Working solo means nobody argues back, which makes a bad decision very easy to keep. The code was written with AI assistance; I set the direction, made the product calls and reviewed what landed — and the things that went wrong went wrong where no assistant can see them.
The trial was too short to reach a second week
19 May
- Was
- A 14-day free trial.
- Became
- Twenty days, with everyone on the old clock topped up by hand.
- Why
- Fourteen days is not two weeks of use for someone who works Tuesday to Saturday and only sets things up on a quiet Monday. Twenty buys a second full cycle — the first point at which the product has proved anything.
Headless Chrome cannot see an iPhone
4–5 June
- Was
- Native date and time inputs everywhere, verified in WebKit.
- Became
- Two purpose-built components — after two fixes that shipped and made it worse.
- Why
- On a real iPhone the empty date field rendered blank. The first fix was a styling hack; the second overlaid a placeholder that doubled up on the actual device. Playwright’s WebKit is not iOS Safari.
Guardrails before speed
19 May, after the third incident
- Was
- Three deploys in a row lost production environment variables.
- Became
- One vault file as the source of truth, a script that derives the required keys by grepping every process.env read, and a pre-deploy hook that refuses an unsafe sync.
- Why
- “Be more careful” is not a fix. The hook has blocked one bad sync since.
I was the review gate
The blank date field on a real iPhone. A booking created on the wrong day, invisible until reload. Service names frozen in the language the master typed them in, on five different screens. None of that shows up in a test suite — it shows up when you use the thing as the person who pays for it.
Three analytics stacks, a feature flag and a running experiment exist so the arguments I cannot win on taste get settled by traffic instead. The one number I will not put on this page is how many people pay for it — that is not mine to publish.
Live, in Spanish, in production
Designed, built and shipped
by one person.
Landing, public booking page, master dashboard, voice booking, WhatsApp notifications, Stripe billing and an admin console — running in Madrid, in six languages.
citameya.com↗