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.
Live on fitindebuurt.nl · Row-level security on everything private · Next.js 16, React 19, Supabase
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.
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.
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.
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.
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.
| Layer | Choice |
|---|---|
| App | Next.js 16 (App Router), React 19, TypeScript strict |
| Styling | Tailwind CSS 4, custom components, fully Dutch interface |
| Data + auth | Supabase (Postgres, auth, storage), Frankfurt region |
| Validation | Zod, on both sides and on the environment variables |
| Resend, five transactional emails with one layout | |
| @react-pdf/renderer, invoices in Supabase Storage | |
| Hosting | Vercel, wildcard *.fitindebuurt.nl, cron jobs, preview per push |
| Secrets | Doppler; no key in the repo |
| Monitoring | Sentry, 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.
| What | Size |
|---|---|
| Code | 265 files, ± 33,000 lines of TypeScript |
| Database | 27 migrations, 20 tables, RLS on everything private |
| Unit tests | 412, across 37 files |
| Browser tests (Playwright) | 52, across 9 files |
| Templates and blocks | 8 templates, 12 reusable blocks |
| Commits | 94, 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.
- 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.







