MoviFit

Class booking and memberships for movement studios

Booking, credit-based memberships and payments for pilates, yoga and barre studios. Pura Vida Moves, a boutique studio in Guanacaste, runs 25 weekly classes and 61 enrolled students on it, with the business rules enforced in PostgreSQL and tested with pgTAP.

Role: Founder & Full Stack Developer
Duration: 2026 - Present
MoviFit hero image

Screens

Public site of Pura Vida Moves, rendered by MoviFit on its own subdomain

Tech Stack

React TypeScript Vite TanStack Router TanStack Query Supabase PostgreSQL pgTAP Playwright Astro Cloudflare Workers Tailwind CSS

Overview

Pura Vida Moves is a boutique movement studio in Guanacaste, Costa Rica: 16 classes, 25 weekly time slots, 8 instructors, adults and kids. Enrolment, payments and attendance were handled over WhatsApp and by hand. MoviFit replaced that. Students book classes and buy packages from their phone, guardians manage their children's classes, instructors take attendance, a tablet at the door works as a check-in kiosk, and the studio gets its own public site. I started the repository in late August 2026, and within weeks the studio was running real bookings and payments on it.

⚡ The Challenge

A class credit is money. Students buy packages that expire, gift credits to friends, cancel late or don't show up, and every one of those has to be explainable to a customer months later. Two people can try to take the last seat in the same second. A shared tablet at the front desk must let anyone check in without exposing anything about anyone. And the public site must never become a way to read the database.

✓ The Solution

I wrote the architecture down first as ADRs, 14 so far, each with the alternative I rejected. The browser only reads and calls functions: every write is a PostgreSQL RPC, so rules, transactions and the audit log live in one place. Credits are an append-only ledger; booking holds a credit and attendance consumes it; the last seat is decided by a single conditional UPDATE in the database. Everything is tested where it lives, with pgTAP, and the capacity race is tested with ten parallel connections fighting for one seat.

Impact & Results

61
Enrolled students

At Pura Vida Moves, booking classes and managing monthly memberships in the app

25
Classes per week

16 classes and 8 instructors across two programmes

851
pgTAP assertions

55 database test files run in CI, next to Vitest and Playwright

Key Decisions

1

React SPA, not Next.js (ADR-001)

The business logic lives in Postgres, so the only thing Next.js would add is SSR for SEO, at the cost of server and client components and cache layers. SEO went to a separate Astro site instead.

2

Credits are a ledger, not a counter (ADR-002)

The balance is the sum of non-expired rows, spent first-in first-out by expiry date. Nothing is ever edited or deleted: a correction is a new row. Triggers block UPDATE and DELETE, and a statement-level trigger blocks TRUNCATE, which row-level triggers never see.

3

Booking holds the credit, attendance spends it (ADR-006)

A late cancel or a no-show only costs a class if the class was full, because that is when it kept somebody else out. The last-seat race is resolved with one UPDATE ... WHERE reserved_count < capacity, not in React.

4

The client reads; writing is calling a function (ADR-011)

Row-level security answers which row, not which column. An audit found that a table-wide UPDATE grant let a client move a student to another organisation. The grant was narrowed, and any insert, update or delete in the frontend now has to be justified in the pull request.

5

A kiosk with a minimal role (ADR-008)

The front-desk tablet holds a device token, not a logged-in admin. It can do exactly three things: list today's sessions, search active students by name, and insert one attendance row. It resets after 10 seconds so the next person never sees the previous name.

6

The public site sees one view (ADR-013)

Each studio gets a site on its own subdomain, rendered by Astro on Cloudflare Workers. It can read a single Postgres view and four public functions, so the question of what the public site can see has a one-screen answer. A database outage returns 503, never 404, so Google does not drop the studio.

Technical Highlights

Key Learnings

A student wrote that a class had been deducted twice, once as a no-show and once as attended. The fix was a new ledger row that returns the credit in the same transaction, plus an idempotent migration that repaid everyone already affected, proven by running it twice in a test.

I granted EXECUTE to authenticated and assumed anonymous callers were out. They were not: Postgres grants EXECUTE to PUBLIC by default, and two of my three fixes only looked like they had worked. Measure privileges, do not assume them.

A confirmation email went out with a broken link. The function logs showed that the version serving production had never been built from the repository. Anything deployed by hand eventually disagrees with git.

The kiosk registration link could only be copied on the administrator's computer, never on the tablet across the room. The only path that worked was the one my end-to-end test used. A QR code fixed it.

Writing the rejected alternative next to every decision is what makes the decision reviewable a month later.

View all case studies