Teardown · MVP Rescue

Fixing a vibe-coded booking app

Classly lets people book yoga and HIIT classes. It looks finished, and it would pass a quick demo. It isn't safe to launch: anyone could download every member's password, a two-spot class took four bookings, and on a phone the main content took 25 seconds to appear.

Classly is a sample app I built for this teardown, the way apps from AI builders like Lovable, Bolt and Cursor often turn out. There's no real studio and no real customers. The problems, the fixes and every number below are real, measured on the same code before and after.

The results

Security holes
50
Accessibility (Lighthouse)
54100
WCAG issues found by axe-core
230
Performance on a phone (Lighthouse)
6794
Main content visible, phone on 4G
25.6 s2.5 s
Page weight
6.6 MB156 KB

Measured with Lighthouse 12 (mobile, simulated 4G), axe-core on WCAG 2.2 AA rules, and scripted API checks against the running app. Same machine, same settings, before and after.

On a phone

Before: on a phone the Classly page is a shrunken desktop layout with tiny unreadable text, an empty hero area and blank class photos still loading.
Before. A shrunken desktop page. Photos still loading; white heading unreadable.
After: the Classly page fits the phone, with a readable heading over the hero photo and a full-width class card with a Details button.
After. Fits the screen, readable, and the photo is in.

The diagnosis

This is the written review you'd get from the 48-hour MVP Rescue Diagnostic: what's wrong, why it matters to you, and what fixes it, worst first.

Critical

Anyone could download every member's password

The app had a public /api/users address that returned every account, with passwords stored and sent in plain text. No login needed: one visit to that address in a browser was enough. People reuse passwords, so a leak here exposes their email and bank logins too.

Passwords are now hashed (scrypt) and never leave the server. The member list moved behind a staff-only endpoint.

Critical

"Admin only" was checked in the browser

The admin screen only appeared for the owner's email, but that check ran in the visitor's browser, and the data behind it had no check at all. Anyone who knew where to look could read every booking.

The server checks the logged-in person's role on every staff request. A regular member now gets "Studio staff only".

Critical

A secret key shipped to every visitor

The payments secret sat in a config file that was bundled into the public JavaScript, and was also printed to the browser console on every booking.

// src/config.ts, sent to every visitor
- export const PAYMENTS_SECRET_KEY = "demo_secret_key_…";
- console.log("charging with", PAYMENTS_SECRET_KEY);
+ // secrets live in server environment variables only

Secrets removed from the front end entirely. They belong in server environment variables.

High

Bookings without a login, and overbooking

The booking endpoint trusted whatever user the browser said it was, and never checked capacity. I booked the two-spot Spin class three times as a made-up user: it ended up with 4 bookings for 2 bikes.

Booking now needs a login, takes the user from the session, and refuses full, past and duplicate bookings. Tested again with real accounts: it stops at 2 of 2.

High

A failed login still logged you in

The whole user record (password included) was kept in the browser's local storage, where any script can read or edit it. Worse, a wrong password stored the error message as "the user", so the app behaved as if you were logged in.

A signed, HttpOnly session cookie that scripts can't read or forge, and clear error messages on the form.

High

Unusable on phones

The page told phones it was 1,200 pixels wide, so everything shrank to about a third: 12px text displayed at roughly 4px. Booking from a phone was close to impossible.

A real mobile viewport and a fluid layout. Smallest text is now 14px, and every tap target is at least 44px.

High

Locked out keyboard and screen-reader users

Every "button" was a clickable box that a keyboard can't reach. Form fields had no labels, images had no descriptions, and 13 pieces of text failed contrast. The booking pop-up couldn't be opened or closed without a mouse. The password field showed what you typed. That's a problem for customers, and a legal risk under the European Accessibility Act and the ADA.

Real buttons, labels, headings and alt text; a native dialog that takes focus, closes with Escape and hands focus back; colours darkened to pass WCAG 2.2 AA. axe-core now finds 0 issues, including inside the pop-up.

Medium

A 6.6 MB page

Full-size photos straight from the camera roll, a date library carrying every language on Earth, a utility library used for one sort, and ten font weights.

Resized WebP photos with lazy loading, the browser's built-in date formatting, and three font weights. JavaScript went from 556 KB to 150 KB; the whole page from 6.6 MB to 156 KB.

Medium

Silent failures

If the server hiccupped, the class list simply stayed empty, with no message and no way to retry.

Loading and error states with a "Try again" button, and readable messages for full classes, wrong passwords and lost connections.

How the fix went

  1. 01

    Measure

    Lighthouse, axe-core and scripted attacks on the API, so "before" is numbers, not opinions.

  2. 02

    Security first

    Passwords, sessions, staff checks, booking rules and secrets: one focused change.

  3. 03

    People next

    Phones, keyboards and screen readers. The design stayed the same; it just works for everyone now.

  4. 04

    Speed last

    Images, libraries and fonts. Then every check re-run on the finished build.

Try both versions yourself: the broken before and the fixed after (sample data that resets; please don't use a real password). The fixes are in separate commits, so you can see what changed and why: read the code on GitHub. The look of the app didn't change: same colours, same layout, same flow.

Your app

Built something in Lovable, Bolt, v0 or Cursor that's stuck?

Within 48 hours you get the same written diagnosis for your app, with a fixed-price plan to fix it. If you go ahead within 30 days, the €450 is credited to the fix.