A finished interface can hide an unfinished system. RSVQuick could create a styled invitation, attach questions, generate a QR code, and record a guest’s reply. That happy path was real. So were three risks behind it: anonymous clients queried application tables, responses and survey answers were written separately, and the browser decided whether the final seat was still available.

Known issues41 295.1% fewer
Serious risks24 0None remain
App download1.08 MB 366 KB66.1% smaller
Safety checks0 125Unit · DB · browser

Start with evidence, not a rewrite

The original checkout built successfully. We pinned that exact upstream commit with an annotated baseline tag, saved the production dependency audit and build output, and measured the only JavaScript bundle before changing anything. The baseline contained 41 production advisories: 21 high and 3 critical. It also had a 1,081,500-byte gzip main bundle, no tests, no CI workflow, and no versioned database migration.

Baseline contract

Every before-number in this article comes from the tagged commit fixmymvp-baseline-2026-07-25. Every after-number comes from saved machine-readable evidence in the rescue repository.

Rescue timeline · from a reproducible baseline to release evidence
01PinTag the upstream commit and preserve its build and audit.
02ModelTurn privacy, capacity, dates, and retries into database rules.
03RepairNarrow public access, make submission atomic, and explain every state.
04ProveTest components, authorization, race conditions, mobile, and accessibility.
05MeasureEnforce bundle budgets and save repeatable Lighthouse evidence.

The browser was doing the database’s job

The guest page fetched the invitation, survey questions, choices, and response rows directly. It counted accepted replies in the browser and disabled the submit button when the event looked full or closed. On submit, it inserted a response first and survey answers second.

Before · a cooperative browser was the trust boundary
Anonymous guestBrowsercounts seats · checks date · writes rows
direct reads + writes
Supabase
requests
responses
answers

A disabled button is useful feedback, but it is not a capacity guarantee. Any second browser can make the same decision from the same stale count.

The failure modes were concrete

01 · Privacy

Public clients knew too much

The guest experience only needed a safe invitation view and an accepted count. Direct table access widened the data exposed to anonymous code.

02 · Concurrency

Two guests could see one seat

Both browsers could read the same count, decide capacity remained, and insert independently.

03 · Atomicity

Half a response could survive

A response and its survey answers were separate client operations. A second failure could leave partial data behind.

04 · Reproducibility

The repository was not the system

Table definitions and deployed access policies were absent, so a clean environment could not recreate the application contract.

Move the rule to the boundary that can enforce it

The rescue adds one versioned migration for the full schema, constraints, indexes, row-level policies, and two deliberately narrow public functions. Authenticated hosts keep normal CRUD access through owner-scoped policies. Anonymous guests lose all direct table access.

After · a narrow public API and a private data model
Authenticated host
Owner-scoped RLS
Anonymous guest
2 RPCs onlysafe read · atomic submit
Trusted boundary
Private tablesconstraints · locks · transactions
ActorInvitation dataResponsesSurvey answers
OwnerOwn rowsRead · dismissRead results
Anonymous guestSafe RPC viewAtomic RPC onlyAtomic RPC only
Other ownerDeniedDeniedDenied

Make the last seat impossible to claim twice

The new submit_rsvp() function takes an invitation, a reply, any survey answers, and an idempotency key. Inside one transaction it takes an advisory lock for that invitation, reloads the authoritative row, checks the close date and accepted count, validates every choice, then inserts the response and answers together. A retry with the same key returns the original result instead of duplicating it.

The one-seat race · both clicks happen, one commit wins
Guest A
Submit
Confirmed
Invitation lock1 seatserialize
Guest B
Submit
Full

The decision is serialized at the database. Guest B receives an explicit capacity message; no second accepted row is created.

Prove the behavior at three levels

A critical rule is not finished when the implementation looks right. The rescue checks the small decision helpers, the database contract itself, and the user-visible race through real browser contexts.

Browser scenarios6auth · management · edge states · mobile · accessibility · race
Database assertions95RLS · isolation · result states · capacity · idempotency · rollback
Unit + component tests24validation · RSVP states · retry · confirmation

The browser suite exercises protected routes, a real password-recovery email, login and logout, host creation and tenant isolation, required surveys, invalid, expired, full, and declined invitations, plus phone-width layout and keyboard navigation. Its final scenario opens two isolated guest contexts, makes them submit for one remaining seat together, and requires exactly one confirmation and a final owner count of 1/1. Zero-exclusion axe scans cover the landing, login, dashboard, and invitation experiences.

The measured result

Production advisories
41 before2 after
Main JS, gzip
1.08 MB before366 KB after
Automated checks
0 before125 after
Lighthouse accessibility
67 before100 after

A rescue is a handoff, not a magic trick

RSVQuick did not need its product idea replaced. It needed the invisible parts of the product to become explicit: who can access what, where the final-seat decision lives, how partial writes are prevented, how a clean environment is rebuilt, and which tests prove the promise still holds.

Book an AuditView the rescued repository →Credit the original project →