Aesthetic clinics

Qualify the patient before the phone call

An embeddable AI face preview that scores, verifies and sorts every enquiry before a clinic picks up the phone. The visitor sees their own possible result; the clinic gets a phone-verified lead with timing, readiness and budget already answered.

Aesthetic clinicsNext.js 16gpt-image-1Twilio Verify
Revenue OS shown across a laptop and two browser windows
About the product

See the result, then meet the patient who is ready to book

Aesthetic clinics do not have a lead problem. They have a sorting problem. A contact form gives every enquiry the same weight: the patient who has saved for a year and the one who is idly curious arrive as the same row, with the same three fields, and someone has to phone all of them to find out which is which.

Revenue OS replaces that form with a funnel that earns its information. A visitor picks the area they are thinking about, uploads a photo, and while an AI preview of their own result is being generated they answer six short questions about timing, readiness and budget. Those answers are scored server-side out of 110 and banded into three tiers, the phone number is proved by SMS code before a lead record exists, and only then is the result revealed behind the clinic's own booking call to action.

The second half of the product is what the clinic gets: an operator dashboard that opens on seven revenue-ordered sections rather than one undifferentiated table, a complete lead database with sixteen columns and fifteen filter controls, a ten-state lifecycle with a wrap-up dialog that records what blocked each conversation, and alerts for anyone scoring 90 or more. Both halves run in Dutch and English, and a single row of configuration gives each clinic its own palette, treatment list, copy, booking URL, webhook and default language.

Industry
Aesthetic and cosmetic clinics
Platform
Responsive web: an iframe-embeddable patient widget plus an authenticated operator dashboard
Timeline
About eleven weeks, March to June 2026: a four-week MVP then a seven-week build in numbered batches
Team
Two contributors

What we did

  • Product definition
  • UI/UX design
  • Frontend development
  • Backend development
  • Database design
  • AI integration
  • Third-party integrations

Stack

Next.js 16React 19TypeScriptTailwind CSS v4SupabaseOpenAI gpt-image-1Twilio VerifyUpstash RedisResendVercel
Goals

What the build had to achieve

Qualify before the call

The build was aimed at one number: how many phone calls it takes to find a patient who will book. Every screen exists to move information out of the phone call and into the form, so the clinic starts a conversation already knowing timing, budget and readiness.

Make the wait do work

An AI face generation takes real seconds, and a spinner spends them. Generation is kicked off the moment the photo lands and runs in the background while the visitor answers the six questions, so the slowest part of the product collects the most valuable data.

One lead, one truth

Lead state is split deliberately into a scoring tier the funnel computes, a manual priority the operator sets, and a ten-state lifecycle label. The dashboard's sections derive from the operational columns and never read the label, so the label cannot contradict where a lead actually sits.

Speak the patient's language

The funnel and the dashboard are both fully bilingual, and the default is a property of the clinic rather than of the visitor's browser, so a Dutch clinic opens in Dutch without a URL parameter while the widget still honours an explicit choice.

Inside the product

Revenue OS, screen by screen

Five tablets showing the patient funnel in Dutch, in cream and gold. The first is headed Zie jouw transformatie with six treatment cards, Neus, Lippen, Kaaklijn, Wangen, Kin and Voorhoofd, each with a hand-drawn line icon. The second, Upload je foto, lists three rules for a usable photo above a tap-to-upload area. The third reads Je voorbeeld wordt voorbereid, explaining that a few short questions follow while the visualisation is made. The fourth asks when the visitor would ideally want a change, with four timing options. The fifth asks what budget they have in mind, with the 5,000 euro and above option selected and ticked.
01

A funnel that earns its information

The visitor picks an area, uploads a photo, and answers six short questions about timing, readiness and budget. Generation starts the moment the photo lands, so the slowest part of the product is also the part collecting the most valuable data.

Three overlapping tablets showing the capture step in Dutch. The first is headed Je bent er bijna, telling the visitor their personal preview is ready on the next screen, above a gold button reading Bekijk mijn resultaat. The second asks where the result should be sent, with fields for name, phone number and email, a Code versturen button beside the phone field, two separate consent checkboxes for data processing and for contact, and a gold Onthul mijn resultaat button. The third shows the same form completed, with a green Telefoon geverifieerd confirmation under the phone number.
02

Verified before the lead exists

A short recap of the effort already invested, then name, phone, email and two separate consents. The number is proved by SMS code before a lead record is written, so the clinic never calls a number that does not exist.

Two browser windows at app.revenueos.io. The first shows a lead record for Sanne de Vries, tagged HIGH with a score of 105 and phone verified, at Lumière Aesthetics Amsterdam for a nose procedure, language NL. A status block shows In Conversation with a change-status control, a Hot Opportunity priority and a Wrap up button. A qualification snapshot gives timeline as soon as possible, consultation openness yes absolutely, readiness to invest I feel ready, and budget over 5,000 euros. The second window shows an activity trail listing notes added, a priority change, a status change from new to in conversation, and lead created, each with an actor and timestamp, above a notes field.
03

The record, and how it got there

Everything the funnel learned on one screen: score, verified phone, language, status and the answers behind the number. Beside it, an append-only trail of every status change, priority change and note, with who did it and when.

Two panels. The first is the Revenue OS sign-in card with an email field, a password field and a dark Sign in button. The second is a Wrap up lead dialog with dropdowns for outcome set to Consultation Completed, consultation outcome set to Showed up converted, conversation outcome set to Reached, main blocker set to Price and next step set to Await decision, plus empty final outcome and future opportunity reason fields, a revenue field in euros, a revisit date picker, and Cancel and Save wrap-up buttons.
04

Closing the loop

One dialog records what happened, what blocked it and what happens next: outcome, blocker, next step, final outcome, revenue and a revisit date. The next call starts where the last one ended.

Two browser windows at app.revenueos.io/leads. The first is headed All Leads with 43 results, a row of quick filters reading Future opportunities, Price objections, Needs follow-up, Consultation scheduled, Post-consult thinking, Lost and High intent, above filter controls for status, category, priority, procedure, clinic, main blocker, conversation outcome, future opportunity, final outcome, minimum and maximum score, and created and revisit date ranges. The second shows the resulting table with columns for name, phone, email, procedure, clinic, score, category, status, main blocker, conversation outcome, next step, future opportunity, revisit date, created, last activity and priority, noted as sixteen columns that scroll right.
05

Every lead, on any dimension

Seven one-click views over fifteen filter controls, and sixteen columns per lead. Score, status, blocker, conversation outcome, next step and revisit date sit side by side, so a whole segment reads at a glance.

Two browser windows at app.revenueos.io/leads. The first lists leads whose status is Future Opportunity, each with a main blocker such as price, timing or comparing clinics, a future opportunity reason such as financial timing, work schedule, recovery timing or personal circumstances, and a revisit date months ahead. The second shows the same database filtered by a clinic query parameter, listing only leads for Noordkliniek Rotterdam across a range of statuses from New Lead to Consultation Scheduled, Thinking, Lost and Future Opportunity.
06

The segment most clinics lose

The leads that said not now, with the reason they said it and the date to come back. The same database narrows to a single location, so the founder sees the group and a clinic sees itself.

A tablet showing the funnel's opening screen in English, headed See Your Transformation, with treatment cards for Nose, Lips, Jawline and Cheeks. Beside it, a browser window at app.revenueos.io/notifications listing hot-lead alerts naming each patient and their score out of 110, each with view lead and mark read actions. A third window shows the same dashboard in Dutch at a lang parameter, with section headings reading Topkansen and Prioriteit-opvolging and score tiers reading HOOG and GEMIDDELD.
07

Hot leads announce themselves

A lead scoring 90 or more raises its own notification, named and scored, one click from the record. The funnel and the dashboard both run in Dutch and English, and the default belongs to the clinic rather than the browser.

Challenges

The parts that were genuinely hard

01

Choosing a model that keeps the patient's face

The problem

An aesthetic preview is worthless if it returns a different person, and the first three image models did exactly that in three different ways. One was the wrong class of model for an edit, one generated a convincingly attractive stranger, and one needed inpainting masks, which static masks cannot supply for a face that is not centred and straight-on.

What we did

We moved to an image model that supports high input fidelity on an edit, with a server-side resize to a fixed 1024 by 1024 before the call, and put the whole thing behind a provider interface so the model is a one-variable swap rather than a rewrite.

The outcome

Edits that preserve identity and skin texture, and a provider seam that survived the change: either implementation is still selectable by environment variable.

02

Serverless killed the background job

The problem

Generation worked locally and failed in production. The design held results in a module-level map and polled for them, but on a serverless platform each request can land on a different instance, and work started after the response is killed when the function returns. The poll was asking an instance that had never heard of the job.

What we did

We collapsed the two-call design into one: the generate endpoint now awaits the provider and returns the image, with a raised max duration and an abort controller replacing the polling loop and its cleanup. A parked branch that moved the call into an edge function documents the alternative that was measured and rejected.

The outcome

Generation succeeds on the production runtime, and 51 lines of polling state came out of the processing screen.

03

Reading the SMS code destroyed the funnel

The problem

On mobile, the moment a visitor left the browser to read their verification code, the funnel reset to step one. Every lead was lost at the last screen. Three causes stacked: funnel state lived only in React memory, the page guards fired before the restore had run, and when the widget is embedded in an iframe a reload of the parent page resets it too.

What we did

We added a session-storage persistence layer for funnel state, gated every guard on a hydration flag so a returning visitor is not bounced before the restore completes, added resume-forward for the iframe case, and raised the verification attempt allowance to match the provider's own default.

The outcome

A mid-funnel reload resumes where the visitor left off, on mobile and inside an iframe alike, covered by unit tests.

04

A status label that could contradict the screen

The problem

The client wanted fast, unrestricted status changes and a homepage organised by revenue priority. Those pull against each other: free status edits can leave a lead labelled Consultation Scheduled while sitting in the new-leads section. The obvious fix, a restrictive state machine, was explicitly not wanted.

What we did

We kept transitions permissive, so any status may move to any other, and moved integrity into column invariants instead: scheduling needs a date, post-consultation states need a recorded outcome, converted and lost need a final outcome, and a future opportunity needs a reason. The homepage derives its seven sections from those same columns and never reads the label.

The outcome

Operators change status freely, and the label cannot disagree with the section a lead is in.

05

Locking down a database with no per-user rows

The problem

A patient photo funnel and a clinic CRM live in one deployment, and one of them must be embeddable in any third-party website. The widget has no login, so the browser-side key must be able to do nothing at all, and the two halves need opposite framing policies.

What we did

Row-level security is enabled on every table with no policies at all, so the anonymous key reads nothing and all data access goes through a service-role client server-side behind the auth gate. Every dashboard API route re-checks the session itself, the clinic config endpoint strips the webhook URL, secret and backup email before the response reaches the browser, and the framing headers are split so the dashboard refuses all framing while the widget allows it.

The outcome

An embeddable widget and a private CRM in one application, with no browser-reachable path to patient data.

Approach

How the work ran

  1. 01

    Discovery and research

    The scoring model was not invented in code. Question wording, per-answer point values and the tier boundaries were written down first and reconciled against the client's own confirmations, including that question six is shown to the patient but scored zero because it is a psychological prime. Both that and a reverted four-tier reading are recorded in the module that implements them.

  2. 02

    UI/UX design

    The funnel is mobile-first and single-purpose: one decision per screen, a nine-step progress bar with the last two steps deliberately outside it, hand-drawn icons for each treatment area rather than stock iconography, and an acknowledge-then-advance pause on every answer that the client specifically asked for. Clinic identity is data, not a fork: colours, copy, treatment list and language come from the clinic row and are applied as CSS variables.

  3. 03

    Development and testing

    The work shipped in seven numbered batches, each a vertical slice from migration to screen. The pure domain modules, scoring, section partitioning, the status machine, wrap-up validation, lead filtering, phone normalisation and funnel-state persistence, are deliberately free of database imports so they unit-test in isolation.

  4. 04

    Deployment and support

    Deployment is a git push, with the database evolved by numbered, additive SQL migrations, so the running release never broke. A daily cron checks for consultations whose outcome was never recorded and raises a dashboard notification, and lead delivery has two independent paths, an outbound webhook per clinic and an email fallback, so a clinic's own system going down does not lose the lead.

Features

What shipped

AI treatment preview that keeps the patient's face

The visitor uploads one photo and sees their own face with the selected area refined, not a model's. The image is normalised server-side, edited at high input fidelity, and returned with two variants per request.

A six-question funnel that scores itself

Timing, emotional alignment, consultation openness, readiness to invest and budget are each worth points, and a sixth question is asked for momentum and scored zero. The total out of 110 is computed server-side from the stored answers, never trusted from the browser, and banded High, Mid or Low.

Phone verification before a lead exists

A six-digit code goes out by SMS and must be confirmed before the lead is written, with the number normalised identically in the send, verify and submit paths so a verified number cannot fail the final gate.

A pipeline that sorts itself by revenue

Seven sections, strict first-match-wins: hot opportunities, priority follow-up, consultation scheduled, post-consultation, high-intent new, mid-intent new, then everything else. Sorting within a section is by score or by consultation date, whichever that section is for.

A conversation memory, not a contact list

The wrap-up dialog records outcome, blocker, next step, final outcome, future opportunity reason, revenue and a revisit date, each as a canonical option or a typed alternative. Every change lands on an append-only activity trail with an actor and a timestamp.

Multi-clinic from one deployment

One row per clinic carries palette, treatment list, copy, booking URL, webhook endpoint and secret, email fallback and default language. The founder dashboard spans clinics with a clinic column and filter; the widget is routed by a query parameter that survives every navigation.

Results

Measured, not estimated

99 / 100

Lighthouse performance

On the funnel's entry screen, with first contentful paint at 0.2 seconds, largest contentful paint at 0.5 seconds and zero total blocking time. Measured by us against a production build served locally, so it is our own measurement rather than field data.

112

Unit tests across 11 files

Covering the parts where a silent error costs money: the scoring model, the seven-section partition, the ten-state invariants, wrap-up validation, lead filtering, phone normalisation and funnel-state persistence.

2 languages

Funnel and dashboard alike

Dutch and English across both halves, with the default owned by the clinic rather than the visitor's browser, so section names, status labels and score tiers all switch together.

Turn curious visitors into booked consultations

If your enquiry form is telling you nothing, we will build you one that answers the questions your team is currently asking on the phone.

Have a Project To Discuss?

Contact Us