SnoekByte LogoSnoekByte
Case study · Live in production
Lead platform for local fitness studios

From empty screen to first customer live — in eight days.

Fit in de Buurt is a lead platform for local fitness studios: landing pages that convert, a portal to follow up on called leads, a postcode-based guide and the invoicing around it. Design, build, database, hosting and operations — built solo.

View the live platform
Role
Design, build, database, hosting & operations (solo)
Timeline
August 2026 · 8 days to first customer live
Stack
Next.js 16 · React 19 · TypeScript · Tailwind 4 · Supabase · Resend · Vercel
Live
fitindebuurt.nl

Live on fitindebuurt.nl · Row-level security on everything private · Next.js 16, React 19, Supabase

8
days to first customer live
±33k
lines of TypeScript (265 files)
464
automated tests
Live
first real customer in production

The problem

A small fitness studio advertises locally, sends people to its Instagram or a generic website, and loses them there. Those who do respond come in via a DM, an email or a text; the owner keeps track of who to call back in her head. Two days later half of them are forgotten and the interest is gone.

The three things that exist for this each solve only a slice. A website builder delivers a page without follow-up. A CRM is too heavy for someone with a handful of leads a month. A linktree offers no insight whatsoever. And everything that does work puts her customers’ personal data somewhere she has no control over.

What it became

One platform serving three kinds of visitors at once — the athlete, the studio and the operator.

For the athlete

The homepage is a guide: enter your postcode and see which studios, gyms, salons and practices are closest to you, with distance, reviews and a button to that studio’s booking system or lead page. No cookies, no account, and we never store what someone searched for.

For the studio

Its own lead page on name.fitindebuurt.nl and fitindebuurt.nl/name, built from blocks and matched to its branding. Anyone who leaves their number lands straight in her portal, with an email alongside. That portal has one list: who do I call. Per lead the system tracks call attempts, lets you snooze with a note, and mark it won or dismissed.

For the operator

An editor where I build and publish a customer page, statistics per version of that page, and invoicing with subscriptions, numbering, PDFs and a nightly cron job.

There is no call button or tel: link anywhere. That is the entire idea: a visitor leaves their details and the studio calls back. That way the owner knows who was interested, even when the call doesn’t connect right away.

Four choices that define the project

Decisions that ripple through the whole codebase — and mark the difference between done and rushed.

1

Separation between customers lives in the database, not the interface

Every page has one owner. Row-level security in Postgres decides who sees which row; the app can’t bypass it, nor can I. A test suite uses the real credentials of two owners to try to read, edit and delete each other’s leads. Without that green test, nothing ships.

2

A page’s content lives in two places, on purpose

The template is a typed file ending in satisfies LandingPagina — miss a text and the build fails. What I change in the editor becomes an overlay in the database and renders on top of the template. So I keep the compiler’s guarantees and can change copy without deploying.

3

Publishing happens under a name, numbers belong to a version

Every publication is an immutable row in pagina_versies. The daily counters remember what was counted per version, so I can see what "Shorter hero" delivered versus "Summer promo", and restore an old version as a draft with one click.

4

Everything in Dutch, down to the database

Columns are named belpogingen, snooze_tot, weggedrukt_op. It reads the same for the customer and for me, and it keeps the distance small between what we say in a conversation and what the code says.

Three parts worth highlighting

The places where most of the thinking and building time went.

The page editor

A live preview on the left, the page top to bottom on the right: one panel per block with everything it needs. You edit text in the preview itself — point, click, type — and a visitor gets the exact same HTML. The accent colour lives in one CSS variable; color-mix derives five shades from it so every block fits any branding.

The postcode guide

Postcodes go to the Kadaster location server (PDOK): open, free, no key. What comes back is stored in our own table, so a second visitor with the same postcode costs no request. Postgres computes the distance and returns only studios that meet every condition. We keep nothing about the visitor: no cookie, no IP.

Invoicing

Subscriptions per customer, automatic drafts from a nightly run, an invoice number sequence that must not skip, amounts in cents, VAT per line and a PDF in storage. A sent invoice is frozen: database triggers refuse every change, because a sent invoice must stay the same for seven years.

Need something like this for your business?

How it fits together

One Next.js project serves four kinds of visitors, separated with route groups: the guide, the marketing pages, the customer landing pages and the logged-in side. Subdomains run through proxy.ts, which refreshes the Supabase session and locks everything behind the login.

LayerChoice
AppNext.js 16 (App Router), React 19, TypeScript strict
StylingTailwind CSS 4, custom components, fully Dutch interface
Data + authSupabase (Postgres, auth, storage), Frankfurt region
ValidationZod, on both sides and on the environment variables
MailResend, five transactional emails with one layout
PDF@react-pdf/renderer, invoices in Supabase Storage
HostingVercel, wildcard *.fitindebuurt.nl, cron jobs, preview per push
SecretsDoppler; no key in the repo
MonitoringSentry, production only
  • Two nightly jobs on Vercel behind a shared secret: 06:00 invoicing, 04:00 cleaning up expired requests.
  • No names, phone numbers or email addresses in logs or error messages, no IP addresses in the statistics.
  • Leads are written server-side only — the browser has no write access to the database.
  • A snoozed lead returns on its own: snooze is not a status but a date, no background job that can stall.

Quality and workflow

One command is the gate: npm run check runs typecheck, ESLint with zero warnings, Prettier, all unit tests and a full build. What doesn’t pass isn’t called done. Husky and lint-staged do the same on every commit, GitHub Actions repeats it on every push.

WhatSize
Code265 files, ± 33,000 lines of TypeScript
Database27 migrations, 20 tables, RLS on everything private
Unit tests412, across 37 files
Browser tests (Playwright)52, across 9 files
Templates and blocks8 templates, 12 reusable blocks
Commits94, in eight days

Browser tests do what a human does

Log in, capture a lead, snooze it, turn off a block in the editor, publish and verify the public page changes too. That’s exactly where the bugs live that typecheck and unit tests never see.

Discoverability is fixed in four places

robots.ts (naming twenty-three AI crawlers), a sitemap that pulls customer pages from the database, a generated /llms.txt with the whole site in markdown, and structured data per page type — never more than what’s on the page itself.

Built with AI assistance, but not on a hunch

Every part was first written out as a plan in the repo — why, which phases, which trade-offs — and then built. Fixed agreements live in a rules file that every session reads. The tests, the types and row-level security are the brakes.

Where it stands now

The site is running, the first real customer is live and in the guide, and invoicing is ready for her first subscription.

On the roadmap
  • Map view and dedicated overview pages per type and location
  • Verified Google reviews
  • Lead export
  • Two-factor authentication
  • A/B testing across two published versions at once

From idea to live platform

Custom software that lasts.

A lead platform, a portal, a webshop or an internal tool — built with tests, types and privacy as the foundation. Let’s discuss what you need.

Contact