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.

rolesole designer and engineer
stackNuxt 3 SSR, TypeScript, Prisma, Supabase Postgres, Fly.io
models37
routes126
tests1,719 assertions in 95 files1
decisions127 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.
The Prasso search screen: a single search field above a list of studio results.
The search surface. One field, no filter wall. Filters appear once there is a result set.
The same screen signed in: a streak headline above a row of beads, one for each class attended, and a search field below.
Signed in, the directory gives way to the record. The figures belong to one account and count only that account's own classes.

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.

channelencodeswhy it survived the cut
formdisciplineCircle yoga, square pilates and other movement, pentagon meditation and sound. The one thing people scan a year of practice for.
huestyle, scoped to its discipline33 styles assigned by hand around three bases. A hash gave stability without relation; neighbours have to look like neighbours.
valuetime of dayFour steps. A month reads as a rhythm with no legend attached.
corelanding recordedPresence, never degree. The graded core drew the body as a magnitude and was removed twice.
ringteacher namedFine 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.

A quarter of practice rendered as a grid of beads, varying in form, hue, value, core and ring.
A quarter of practice. Form is discipline and hue is style; the other three channels are in the table above.

What is not done

known gaps, stated plainly

Sources

  1. 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.
  2. 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.