index / work / prasso
Building Prasso
A directory of UK studio locations teaching yoga, pilates, reformer and wellness, built around a private record of the classes you take. The directory gets people in; the record is what brings them back.
Live at prasso.co.uk, and unannounced. The app has served the domain since 8 August 2026; no testers have been invited yet, so nothing on this page is a traction number. These are counts of things that exist.
Disclosure: I co-founded The Reset Rooms, a pilates and wellness studio in Crouch End that trades today and that appears in the directory, including in the screenshot below. It is a separate venture, not a Lyon Empire product.
| role | sole designer and engineer |
|---|---|
| stack | Nuxt 3 SSR, TypeScript, Prisma, Supabase Postgres, Fly.io |
| models | 37 |
| routes | 126 |
| tests | 1,719 assertions in 95 files1 |
| decisions | 127 logged |
The mechanism
three parts, one of which does the work
The directory
Studios, disciplines and locations, kept accurate enough to be worth searching. Necessary, and treated as such.
The record
What you did, where, and how often. Private by default, exportable, and the only reason anyone returns after the first search.
The aggregate
Medians drawn across the records, sold back to studios as market insight. Never a named class, never an identifiable person.
A directory is copyable. A record made of real behaviour is not.


What the aggregate is allowed to say
three layers, and the machine only holds two of them
The mechanism above promises never a named class and never an identifiable person. That is an assertion, and this is what sits under it. Studios buy medians: what a month looked like across the records, never a row and never a person. Four rules decide what a report may contain, and not one of them is a policy anyone has to remember.
Nothing is emitted for a cell of fewer than five, so a count of one, two, three or four appears nowhere in any breakdown at any level. Periods are whole and completed, months and quarters, with no day granularity and no custom range, which is what stops a period being narrowed until one person is left in it. The breakdowns are a fixed set with no query-time segmentation, so there is no way to ask a question the product was not built to answer. And suppression is complementary: no period states a number that the periods inside it would give away.
The event boundary is held by the schema. What counts as a visit is a typed manifest with a database CHECK behind it, so a new kind of event is invisible to a studio until a person has classified it. The default is deny, and it is the database that denies.
The scope is held by the build. A new owner-scoped route that reads visit rows fails the build on the day it is written. It is not forbidden, it is reviewed: the exemption exists, and taking it costs a row in a typed list and a reason in a diff that somebody has to approve.
The floor is held by neither. The threshold of five is a presentation rule. It runs at the moment a report is printed, and there is no constraint in the database that could hold it instead, because a suppressed count is a number that was never written down rather than a field refusing a value.
The floor exists at the moment a report is printed, and there is no position in the schema where it could live instead.
That last layer is a weaker guarantee than the two above it, and saying so is the point. A CHECK constraint refuses a row and a build refuses to compile; both fail loudly, and neither depends on anyone being careful. The floor depends on the code that prints a report continuing to apply it. A determined future edit can pass it. It would have to do so in the open, past a scanner that derives what it checks and a typed exemption list, but it can - and a section that claimed otherwise would spend the credit of the two layers that genuinely are that strong.
Decisions
drawn from the log
No advertising, ever.
Listings and studio insight pay for the product. The moment attention is what is being sold, ranking stops being a service to the reader and becomes inventory. That removed the easiest revenue line on the table before it existed.
No timetables and no bookings.
Every request for this is a request to become a booking platform: live integrations with a dozen studio systems, a support burden, and a fight with incumbents on their ground. Prasso records what happened; the studio owns what is next. It is the feature people ask for first.
The importer doesn't guess.
An unmapped string goes to a queue and renders as unclassified, never as a default, because a catch-all row is a bin and bins fill. 48 studios whose site could not be read entered the queue at confidence 0 rather than being dropped, and 38 of the 488 records that have entered review were still held on 5 September 2026, each with the reason it was held. A lenient importer would have produced a bigger directory.2
No number without its denominator.
Nothing appears in the product or on this page without its sample, period and method. It kills the round ones and it cost several charts outright.
What it actually took
the migration, plainly
The first generation of the data was scraped, and scraped data is wrong in ways that only show up at scale: a missing postcode, a discipline the taxonomy does not recognise, two records claiming one slug. The importer refuses all of it and writes down why, rather than cleaning it quietly.
Correcting that first generation and publishing the studios that followed took a weekend, spread across a few. A directory of this size is a weekend's work for anyone with the same sources, which is why the directory was never the thing being built. The months went into what the record is allowed to hold, and what the directory is not allowed to do.
The scraper proposes. It never assigns.
The bead
where the craft is, if it is anywhere
Each attended class earns a bead, and each bead is a five-channel code: form, hue, value, core, ring. Five is a cap set in the schema rather than a guideline, and a channel is spent, never shared. Intensity, duration, load, reps and springs have no channel and never will: the record does not quantify the body. That is a decision with a number on it, not an omission.
| channel | encodes | why it survived the cut |
|---|---|---|
| form | discipline | Circle yoga, square pilates and other movement, pentagon meditation and sound. The one thing people scan a year of practice for. |
| hue | style, scoped to its discipline | 33 styles assigned by hand around three bases. A hash gave stability without relation; neighbours have to look like neighbours. |
| value | time of day | Four steps. A month reads as a rhythm with no legend attached. |
| core | landing recorded | Presence, never degree. The graded core drew the body as a magnitude and was removed twice. |
| ring | teacher named | Fine amber, absent when no teacher was given. It once meant four things at once. |
A field existing in the schema does not entitle it to a channel.
Position carries nothing. An unearned position renders as a plus rather than a pale bead, so the grid shows what has not happened yet without pretending it has.

What is not done
known gaps, stated plainly
- The coverage claim is a claim. What is published is a count of rows that passed the importer, and no page on the site says "every studio in the UK" because that is not true yet. The count itself is off this page until its unit is settled and the country labels in production have been audited: the recorded figures disagree with each other, and a number that cannot survive being asked how it was made does not go up.
- Nobody outside the build has used it. The app has served prasso.co.uk since 8 August 2026, payments are flagged off and no invitation has been sent. Everything I believe about the record habit is a hypothesis with a decision number attached, not a finding.
- Studio-side pricing is unproven. The insight product is built and priced, and no studio has paid for it yet. If it turns out they will not, the model changes and the log records why.
- Repeated pulls over time are not solved. Restricting reports to whole completed periods reduces what month-over-month differencing can recover about a cohort of five. It does not eliminate it, and that is stated here rather than defended.
- Accessibility is partial. Keyboard paths and contrast are done; the bead grid still needs a proper non-visual equivalent, and a data visualisation without one is unfinished.
Sources
- 37 Prisma models, 126 Nitro routes and 1,719 passing assertions across 95 test files, counted from the repository at the same commit. 127 numbered decisions in the log, February to September 2026.
- 38 records pending in the importer's review queue on 5 September 2026, out of 488 rows that have entered review. Pending, not decided: they are not among the 450 already approved or rejected, which is why the denominator is 488. Separately, 48 places whose site could not be read were queued at confidence 0, counted from 16 logged enrichment runs across 856 distinct places judged. The two counts are taken at different stages, and across different generations of the pipeline, and they do not reconcile to each other.