Works/Citameya
CITAMEYA

Booking for people who work with their hands

Product designer · Brand · Solo build2026MadridLive at citameya.com

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.

I owned Product & UXBrandDesign system Front endAPI & databaseStripe billing WhatsApp & emailSix languagesAnalytics & A/BDeploys
Citameya public booking page for a Madrid hairdresser
Citameya booking summary and details step
Citameya master dashboard on a phone

What shipped · 29 Apr → 23 Jul 2026

Three months, one person, one product.

191commits · one author
33app routes
83API endpoints
32database migrations
191catalogued services
6languages, end to end
10scheduled jobs
€15per month · 20-day trial

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.

The voice promise first, then what switching costs her
Citameya landing page on mobile
Mobile — nearly all of the traffic

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.

01 · IN THE BROWSERWeb Speech streams

Phrases arrive as they are spoken. Nothing has left the device but text.

02 · ON THE SERVERGemini parses

The running transcript is matched against this master’s own service list, once every three seconds.

03 · BACK ON SCREENChips appear

Service, date, time, name, phone. Tap one to correct it and your edit is locked against later passes.

04 · ON STOPOne accurate pass

The recording gets a final, slower transcription that merges with your corrections instead of overwriting them.

Live chips — date, time, name and phone recognised nine seconds into a dictated booking
Nine seconds into «quiero un corte de pelo el viernes a las cinco de la tarde, me llamo Ana López…» — parsed live, from the running transcript.
The voice offer on the public booking page, with its privacy note
The offer on the public page. The disclaimer is part of the design: audio is processed in the browser, only text goes to Google AI, nothing is stored.

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.

ControlButton, then a form
50%
KaraokeChips as you speak
50%
ExcludediOS · Firefox — no Web Speech
The hero animation, generated for the landing. It carries the whole pitch without a word of interface: hands busy, phone up, appointment written down.

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.

Booking step one — choose services
01ServicesA tinted row per service, price and minutes on the row. Pick several and they become one appointment.
Booking step two — pick a day
02DayOnly days she actually works, drawn as tiles rather than a native date input.
Booking step three — pick a time
03TimeSlots computed from the total duration, so a 120-minute colour never lands in a 45-minute gap.
Booking step four — details and confirm
04DetailsName, phone, optional email — summary pinned above, so nothing is confirmed blind.

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.

Corte de pelo45 min
25.00€
Tinte completo120 min
65.00€
Tratamiento hidratante60 min
40.00€

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.

Master dashboard — setup progress, action bar, counters, week strip and the day’s bookings
Setup progress sits at the top until it is finished, then disappears for good
Demo mode showing a full day of bookings
Demo mode — the same screen with a full day, so a prospect sees it working before signing up
Statistics — bookings, revenue, top services and traffic sources
Bookings, revenue, most-booked services, and where the bookings came from
Settings — profile, working hours, services and subscription
Settings — profile, hours, services and the €15 subscription, all in one place

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.

Citameya registration screen
Onboarding — what do you do?
Master dashboard on a phone
The 1080 by 1920 Instagram Story the share kit generates — her name, her services and a QR to her booking page
What the share kit hands her, 1080 × 1920
  • 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.

Waiting-list form on a fully booked day

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

new bookingmorning briefcancelledrescheduled day-before reminderleave a reviewfollow-up slot freed — waiting listmagic linkemail verification password resetrecurring summarytrial ending payment receivedfirst booking

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.

Settings — profile, hours, services and subscription

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

Cyan#00C2E8
Ink#0B0D10
Cream#F7F4EE
Emerald#16A34A
Cream tint#FFF4E0
Mint#C6F0D6
Lilac#E5D6FF
Coral#FFD1C8

Deja la libreta.

Display 800/900 · body 400/600 · 0.75rem radius base, doubled on cards

Pills — one component, five sizes

pill-dark pill-cyan pill pill-sm pill-xs

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.

Citameya key visual — “Deja la libreta. Habla, y Citameya lo apunta.” over cyan, with a barber dictating an appointment

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.

Emotion curve across the journey, dipping at the wait stage
Find
Choose
Book
Wait
Attend
Return
Doing

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

Thinking

“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?”

Pain

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

Answered by

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.

CITAMEYAone app · one API · one database

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

  • WhatsApp
  • 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
ONE SHARED FOUNDATION 33 routes · 83 endpoints · 9 tables · 32 migrations · 191 catalogued services · 6 languages

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.

191commits, one author
69automated specs
32database migrations
1live A/B experiment

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