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.
CriticalAnyone 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".
CriticalA 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.
HighBookings 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.
HighA 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.
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.
HighLocked 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.
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.
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.