YSBDesigns

← All work

Own product

Peony

Supabase Postgres with Row Level Security · Stripe · pg_cron · Static front end, no build step · Eight tracked migrations

Problem

A solo lash, brow or nail technician has two bad options. A marketplace takes a slice of every booking and keeps the client relationship, or a salon system charges monthly for a diary built for a shop with a front door and staff rotas. Neither fits someone working out of other people’s front rooms.

This started as one page for èllash, a mobile technician covering three towns, where the awkward part was not the booking form but the travel: no salon diary understands that Tuesday is a Coventry day and Friday is Birmingham. Having built it once, the interesting question was whether the same thing could be handed to anyone else — which turns a page into a platform, and a platform has a problem a single page never had.

Approach

Two people on two phones can take the same 09:00. Checking “is that slot free?” in application code does not fix it; it only narrows the window between the check and the insert. So the database refuses the second write itself — an EXCLUDE USING gist constraint over the studio and the appointment’s time range, so two overlapping confirmed bookings cannot both exist. The second transaction to commit raises 23P01 and rolls back. Cancelled rows sit outside the constraint, so cancelling frees the slot without any code deciding that it should.

That is the whole design in miniature: put the rule where it cannot be talked round. A studio’s plan is written only by the Stripe webhook, through a SECURITY DEFINER function with no grant to the anonymous or authenticated roles — so nothing running in a browser can promote itself to Pro. Cancelling at period end leaves her on Pro until the paid time actually runs out rather than cutting her off mid-month. And the view the booking page reads tells the client which times are gone without telling them who took them.

Each studio gets its own address and its own page, and the free tier carries a small line in the footer that Pro removes. The commercial promise is the one thing the architecture has to keep true: no commission, ever, however many clients she takes. There is no per-booking figure anywhere in the schema to take a percentage of.

Outcome

It is live. The schema is eight tracked migrations deep and the app runs against a real database rather than the sample data this case study is illustrated with — those captures are the demonstration studio, which labels itself a sample on the page.

What is written: the schema and its exclusion constraint, sign-up, cancellation, photo handling, transactional email, a sender that pg_cron runs every minute, the day-before reminder that is the first thing Pro actually buys, and Stripe billing. The cron file carries a warning in capitals that the repository is public and no key goes in it.

Pricing is published and deliberately simple — free to start, £10 a month for Pro, or £8 a month paid yearly. The reminder has to exist precisely because it is on the pricing card, which is the sort of thing worth writing down next to the code rather than trusting to memory.

Nothing here claims a customer count, because a studio signing up is their business and not a number this site gets to publish.

The screens

The Peony marketing site: a laptop dashboard and a phone booking page side by side
The product site · the phone picks a time and the booking arrives on the dashboard
A sample studio booking page, step one of four, listing lash treatments and prices
A studio’s own page · a sample, and labelled as one on the page itself
The same sample booking page on a phone, showing treatments and the running appointment summary
Where it is actually used · booked from a phone, in a minute

If this looks like your problem

Tell me about it and I'll tell you what it takes.