ElmsPark · independent review
Private review for Don and Kenn. Not for publication.
SoundCheck (codename) · round 2 · the code audit

Good bones, honest builder, four things to fix, one question no code can answer.

ElmsPark Consultants · 15 July 2026 · reviews donjjjordan/soundcheck at commit c0f48b1 · follows round 1 (the brief review) and Don's response document
90-second version: read The verdict, then jump to The call sheet. Everything in between is the evidence. For your builder's AI (Claude Code): the machine-readable .md version of this review.

The verdict

This is a real, honest, demo-grade prototype whose hardest problems are not in the code. The code needs a day of fixes. The venture needs one number: will venues reply?

Round 1 reviewed the brief and Don's team took it seriously: every fix the response document claims was pushed to the repo is genuinely there, mostly done well. This round is the first look at the code itself. It holds up better than most one-day builds have any right to, and the DECISIONS.md discipline (28 logged assumptions, each with reasoning) is rarer than the code. But the audit also found five material defects the response could not have known about, and the flagship claim, "the 10 gig-killers are caught automatically", is today about half true on the live data path.

33/33unit tests pass, typecheck clean (run for this audit)
12response-doc claims traced to code: 8 verified, 3 partial, 1 not held
5material defects found, none fatal, all cheap to fix
4 / 10gig-killers solidly automated end to end today
~9 hrstotal build time (6 commits, one day). The code is not the moat.

Scorecard

Honesty of claims8/10
Data model quality9/10
Code correctness5/10
Security posture4/10
Differentiator coverage today5/10
Strategy (after round 1)8/10

Security and correctness scores are normal for a one-day prototype and are listed, not hidden. They matter only because the demo is about to be shown around and the wedge is about to touch real venues.

What is genuinely good (worth saying out loud on the call)

  • The venue-intake data model is the best artefact in the repo. The 11-section, 77-field schema with versioned setups and the sound limiter as a first-class object (prisma/schema.prisma) is exactly the right core, and it was built first, per the brief's own guardrail.
  • Round-1 fixes were real, same-day, and verifiable. The Congestion Charge correction became a review-dated config with a staleness test that fails the suite after 13 months (src/config/uk-rates.ts). That is the right pattern, generalised.
  • Money handling is careful. Integer pence everywhere, the all-in price fully decomposed and stored at booking so it cannot grow later, and the server recomputes the quote rather than trusting the browser.
  • The mock boundaries are honest. No Stripe key means an explicitly mock provider; a live key is refused at runtime until a deliberate flag is set after solicitor sign-off (src/server/payments/index.ts:14). "Don't fake automation" is mostly being honoured.
  • The contract reads like the brief promised. No non-circumvention, no hidden-contact clause, cooling-off correctly framed as goodwill (CCR 2013 exemption named), "payment protection" language throughout.
  • Three of the brief's acceptance criteria already pass: the 85 dB venue produces a "no live drums" verdict shown to both sides; a London venue flags ULEZ £12.50 and the £18 Congestion Charge to the act; and there is no "get a quote" wall anywhere in the client path.

The response document, claim by claim

Round 1's fixes were promised in writing ("what you clone is what this document claims"). Here is each checkable claim traced to the code.

ClaimWhere checkedVerdict
Congestion Charge £18, rates centralised, 13-month staleness tripwiresrc/config/uk-rates.ts, uk-rates.test.ts (3 tests)VERIFIED
"Escrow" gone from every user-facing surfaceUI says "payment protected" / "Deposits protected"; contract says "payment protection". Only internal enum names remain, as declared.VERIFIED
Runtime guard refuses live Stripe keys until legal sign-offsrc/server/payments/index.ts:14-18 throws on sk_liveVERIFIED
Cooling-off reframed: CCR 2013 exemption + 60-day guardsrc/domain/contract.ts:47, clause 6VERIFIED
Limiter norms restated as our policy, not a market statisticsrc/domain/limiter.ts:5-11VERIFIED
Stripe card-hold limits verified, warning at top of the payments codesrc/server/payments/stripe.ts:6-13VERIFIED
Venue form asks the core sixteen fields, including the two outdoor onessrc/app/venue/[token]/page.tsx: exactly 16 inputs, mobile-first, no loginVERIFIED
Readiness "never fakes a green" (unknowns go amber)src/domain/readiness.ts + testsVERIFIED
"The live demo already exists", 3-minute quickstartAll the described paths exist and the unit suite passes. The full app was not executed in this audit (no Postgres on the review machine), so the end-to-end demo claim is credible but unprobed by us. Run it on the M5 before the call if time allows.PARTLY CHECKED
"The slot state machine already holds dates during checkout"HELD status exists and is used, but the availability check races under concurrency (finding F1) and abandoned checkouts leak HELD dates forever (F7)PARTLY TRUE
Reviews are booking-linked onlyTrue structurally (1:1 with a booking in the schema). No live review-collection flow exists yet; today's reviews are seed data.PARTLY TRUE
Bookings move through an enforced state machineThe machine exists and is tested, but nothing calls its guard: assertTransition has zero production callers, and the one real transition in the app is illegal by the machine's own rules (finding F2)NOT HELD

The brief's own Definition of Done, scored today

Acceptance criterion (from Don's brief)Today
Client books unattended: search → all-in price → available act → pay → contract → venue goes greenPARTIAL works with mock money; venue link is never actually sent (email is mocked), so "green" needs a human to pass the link on
Each of the 10 gig-killers detected and surfaced, a test per itemPARTIAL see the coverage table: 4 solid, 3 partial, 3 weak; killers 6 and 7 have no tests
Act sees budget and venue up front, never pays per lead, is paid fastPARTIAL display is real; payout, settlement and refund flows do not exist yet (Phase 3, as planned)
85 dB venue auto-produces "no live drums, electronic-kit line-ups" shown to both sidesPASS
London ULEZ venue auto-flags the charges to the actPASS (correct 2026 rates, act-facing note, heuristic zones documented as demo-only)
No "get a quote" wall anywhere in the standard client pathPASS
Compliance badges reflect real, verified, in-date PLI/PAT/IDFAIL badges render seed data; there is no verification pipeline, expiry is never date-checked, and lapsed compliance does not block booking (F4)

New findings from the code audit

Things round 1 could not see because it only had the brief. Five material, nine hardening. Every one is cited to a file so Don's builder can fix them without guessing. Tap any row for the detail.

Material (fix before anyone outside the family sees the demo)

F1 · HIGH Two clients can book the same act on the same date

The availability check runs before the database transaction, and inside the transaction the slot is updated unconditionally (src/server/booking-actions.ts:54-59 and :177-183). Two overlapping checkouts for the same act and date both pass the check and both become CONFIRMED bookings; the second silently overwrites the slot. That is precisely the "double-booked wedding" the plan treats as a guarantee event, and the sequential case ("that date was just taken") is handled, so the gap is only visible under real concurrency, which is when it hurts.

Fix: make the slot update conditional inside the transaction (update where status is AVAILABLE, throw if nothing updated). Roughly ten lines.
F2 · HIGH The booking state machine is decorative: its guard is never called, and the first real transition is illegal

The machine's own header says "the API layer must call assertTransition before persisting any status change" (src/domain/booking-states.ts:5). Nothing does: outside its tests it has zero callers. Worse, the one real transition in the app jumps CONFIRMED straight to CONTRACT_SIGNED (src/server/booking-actions.ts:249-254), which the machine forbids (it requires CONTRACT_SENT in between), so wiring the guard up as written would break the flow. Ten of the sixteen states (VENUE_OUTREACH, VENUE_READY, PRE_EVENT, EVENT_DAY, PERFORMED, SETTLED and the exception states) are unreachable by any code path today.

Not a crash. It matters because the brief's whole automation story ("a booking moves itself forward") hangs off this machine, and right now it is a diagram, not a mechanism. Phase 3 builds on top of it; wire it before then, not after.

Fix: one helper (load booking, assert transition, write status) used by every status write, plus either add the CONTRACT_SENT step or amend the machine. Half a day.
F3 · HIGH The admin console is the fall-through: any account that is not a matched client or musician gets it

The dashboard branches "if client… if musician… otherwise render admin" (src/app/dashboard/page.tsx:145 onwards). There is no role === "ADMIN" check. A user with the VENUE role (the enum exists) or any future role lands on the admin view: booking totals, protected-deposit sums, recent bookings with client names, and live venue outreach links.

Fix: one explicit role guard. Two lines.
F4 · MEDIUM Vetting and fee floors are displayed, not enforced

The booking action fetches the act's compliance records and then never reads them (src/server/booking-actions.ts:45-48): lapsed or missing PLI/PAT does not block a booking, though the brief and the acceptance criteria say it must. The badge component never date-checks expiry, so an expired-but-once-verified record still renders as a green badge (src/components/Badges.tsx). meetsMinimumFee is written and unit-tested but never called, and serviceRadiusMiles is never read anywhere, so an act can be booked outside its radius at a mispriced travel fee (see F12).

Fix: three guards in the booking action plus a date check in the badge. An hour or two.
F5 · MEDIUM The PLI half of gig-killer 9 is missing, quietly

The readiness engine's input declares actPliVerified and the mapper dutifully computes it (src/lib/readiness-input.ts:62), but the engine never references it (src/domain/readiness.ts): only PAT is checked. The venue-side field for required PLI cover (requiredPliCoverPence) exists in the schema and is read by nothing. So "missing PAT/PLI paperwork on arrival" is half-covered, and the dead input suggests the rule was intended and lost.

Fix: one rule in the engine plus one field mapped through. An hour, including the test.

Hardening (fine for a laptop demo, required before the wedge touches real people)

F6 Sessions are signed with a hardcoded fallback secret; forged admin cookies are trivial if it ever runs unset

If SESSION_SECRET is not set, cookies are signed with the literal string "dev-only-secret" (src/lib/session.ts:13), and SESSION_SECRET is not even listed in .env.example, so every install runs on the public constant. The cookie also lacks the secure flag, and the signed value has no expiry or revocation (the 30-day limit is browser-side only). The crypto that exists is done properly (HMAC with timing-safe comparison; scrypt for passwords). Fix: require the secret at boot, add secure, embed an expiry.

F7 An abandoned Stripe checkout blocks the date forever

On the real-Stripe path the slot is set to HELD before redirecting to Checkout, but nothing handles checkout.session.expired and there is no reaper job, so a client who closes the tab leaves that act's date HELD indefinitely and invisible to every other client (src/server/booking-actions.ts:180, src/app/api/stripe/webhook/route.ts). The mock path is unaffected.

F8 Booking references are generated by counting rows

count() + 1 (src/server/booking-actions.ts:21-25) means two concurrent bookings can mint the same SC-2026-NNNN and the second dies on the unique constraint with a 500. Use a sequence or retry loop.

F9 A venue is identified by postcode alone, so different venues can merge and inherit each other's "confirmed" setup

Booking reuses any venue with the same postcode (src/server/booking-actions.ts:89-100) and applies its latest CONFIRMED setup instantly (the caching moat, D20). Rural and shared postcodes cover multiple properties, and the client's typed venue name is ignored on reuse: the hotel next door's limiter reading can be served as "confirmed" for a different building. Match on postcode plus normalised name, or ask the client to confirm the matched venue.

F10 The product copy claims outreach that never happens

The booking page tells the client "We have asked [venue] to confirm their setup" (src/app/bookings/[ref]/page.tsx:138-143). Nothing is ever sent: email is mocked (honestly disclosed in the README and D19, but the UI does not carry the caveat). The repo's own rule is "don't fake automation with hidden manual steps". Reword until Resend lands ("Here is the venue's two-minute link, share it with your contact"), which is also simply better UX for the demo.

F11 The signed contract is re-rendered live, never frozen

Contract text is regenerated from current booking data on every page view (src/app/bookings/[ref]/page.tsx:70-81); no copy or hash is stored at signature time (Contract.documentUrl is always null). If booking data changes after signing, the "signed" document silently changes with it. Persist the text (or a hash) when the first party signs.

F12 Unknown postcodes silently geocode to central London, locking in a wrong price

The demo geocoder falls back to a central-London centroid for any postcode not in its small table (src/lib/geo.ts:34,47). A Newcastle act quoted for a Cornwall wedding shows zero travel miles, and the all-in guarantee then locks the wrong figure in. The stub is documented, but the failure mode is silent mispricing; refusing to quote would honour the pricing promise better until the real lookup lands.

F13 Venue tokens never expire and stay live after confirmation

The tokenised form is the right pattern (crypto-random, no login), but a leaked link can rewrite a venue's "confirmed" setup at any time, months later, and flip bookings' readiness (src/server/venue-actions.ts). Add expiry and lock-after-confirm with an explicit re-open.

F14 Smaller loose ends, listed for completeness

The guest-count search input is collected and ignored; zod is a dependency that is never imported; the VenuePack, Thread and Message tables are never written (the pack is a live page, messaging has no UI); the admin queue's "escalate if within 21 days" is a label with no date logic; no rate limiting on login; no CI workflow on the repo, so "tests pass" is only ever a local claim. Ask the builder to add a ten-line GitHub Action so the suite gates every push.

The 10 gig-killers, honestly scored

The README says the readiness engine catches the 10 gig-killers. The engine is real and well-tested where it is wired. The honest picture has three columns: does the rule exist, can real data actually reach it in the live product, and is it tested.

#Gig-killer (from the brief)Rule in engineLive data pathTest
1Unannounced sound limiterREDON FORMYES
2Limiter set too low / C-weighted / mic too closePARTIAL threshold yes; C-weighting only warns in the readout and never affects the RAG levelPARTIAL limiter type and mic proximity are in the schema but not on the formPARTIAL
3Performance space too smallREDON FORMYES
4No power near stage / shared kitchen circuitPARTIAL socket count yes; the shared-circuit field (dedicatedCircuit) is read by nothingPARTIALYES
5Curfew vs booked finish-time mismatchREDUNREACHABLE no live surface collects a finish time (not the venue form, not the booking form), and neither seeded venue sets one, so this rule can only fire in unit testsYES
6Guests already in the room at soundcheckWEAK ambers only if a window is missing entirely; never compares the actual timesNOT COLLECTEDNO
7Brutal load-in (stairs, tiny doors, gravel)WEAK "stairs" ambers; door sizes, distance and surface are schema-onlyPARTIALNO
8No parking / unbudgeted ULEZ + Congestion ChargeAMBERYES correct 2026 rates; zone lookup is a documented demo heuristic, barred from production by D6YES
9Missing PAT / PLI paperwork on arrivalHALF PAT reds correctly; PLI never checked (F5)HALFPAT ONLY
10Outdoor / marquee: no rain cover, unlevel groundRED / AMBERON FORMYES

Net: 4 solid (1, 3, 8, 10), 3 partial (2, 4, 9), 3 weak or unreachable (5, 6, 7). None of this is hard to finish, and the core insight survives contact with the code: the limiter flow, the one that matters most, works end to end. But "the 10 gig-killers are caught automatically (DONE)" should read "5 or 6 of 10 today", and the fastest wins are wiring finish-time collection (killer 5 is fully built and fed by nothing) and the one-line PLI rule.

The business, round 2

Round 1's five structural criticisms were accepted and the plan was reordered around the wedge (a free venue-check for musicians' existing gigs) with written kill/go gates. That was the right response, and the gates are the right instrument. These five points are what round 2 adds; none reverses the wedge decision, all of them shape it.

The VAT trap inside the headline commission

The plan recommends publishing 15% against "Encore's 20%". But Encore's 20% is VAT-inclusive (roughly 16.7% before VAT), and if SoundCheck's 15% commission bears VAT, as it normally will once the platform is VAT-registered, the act's effective cost is 18%. The proud headline gap of five points is really about one, and for some acts it could invert. D5 (platform VAT unmodelled) and D25 (the 15% recommendation) each hold half of this problem. Get the accountant's answer before any comparison copy is written, and make every published comparison VAT-explicit.

The venue-data publication plan needs a consent line, now

The demand hypothesis of programmatic venue pages ("can we have a live band at [venue]?") is genuinely clever, and it is also a purpose-limitation problem: a venue manager who spends two minutes helping one couple's booking has not agreed to a public profile of their limiter, curfew and quirks. Publishing it anyway is the fastest way to burn the exact goodwill the wedge depends on. The fix costs one field: add "may we show a public band-ready profile of your venue?" to the intake form before the first real outreach goes out. Consent turns a legal risk into an owned asset.

Clawback is the unpriced corner of the guarantee

The refund ladder is now properly capped (good), but the money flow pays the act's deposit promptly and then, if the act cancels, refunds the client in full (plus £100) and chases the act for recovery. The 2% reserve only balances if recovery mostly succeeds and cancellations arrive at the average rate, and they will not: they cluster on peak Saturdays, in the exact weeks a replacement is hardest. The response already names the mitigation as a question for the solicitor (hold the payout inside the Stripe balance until nearer the event where the 90-day window allows). Make that the default position, not an option.

The wedge has unlisted prerequisites

Before the first real venue is emailed: ICO registration and a privacy notice; a warmed sending domain; a recorded letter-of-authority pattern so contacting venues "in the musician's name" is both lawful and deliverable (From: the platform, Reply-To: the act, consent stored); and the named human's actual name and hours. The G1 gate also needs denominator discipline: 50 outreaches across genuinely distinct venues, time-boxed, not 50 sends into one friendly chain. And instrument who completes each form (venue staff vs the act on their behalf), or the venue-response number the whole venture pivots on will be quietly polluted.

Still unanswered anywhere, and it is the ops question

Who answers the phone at 21:40 on a June Saturday when a limiter trips mid-first-dance? Peak support hours for this business are precisely the hours a solo founder with a life is off. Related: what does "vetted" actually mean at launch (who watches the videos, who checks references), and who is the named human the response promises for venue liaison "from day one"? These are headcount and rota questions, not code questions, and the £38 per-booking ops line in the unit economics is a guess until someone owns them.

The call sheet

Seven decisions, in the order they unblock each other. The recommendation is in each one.

  1. Wedge: go. The response's own plan (free venue-check for existing gigs, instrumented from the first send, G1/G2/G3 gates in writing) is the right next move, and this audit confirms the organs it reuses (tokenised intake, readiness engine, venue pack) genuinely exist. Add the prerequisites above to Sprint 0. Agree who is the named human.
  2. Fix-four before anyone outside the family sees the demo. F1 (double-booking race), F2 (wire the state machine), F3 (admin fall-through), F6 (session secret), plus the F10 copy fix. Roughly a day of work, listed file by file above.
  3. Commission: park the public number until the accountant answers the VAT question. Keep 15% as the working assumption; write every comparison VAT-explicit. Do not print "keep more than at Encore" until the effective rates have been done properly.
  4. Add the venue-consent field to the intake form now, before the first outreach, so the venue-pages demand play stays legal and friendly.
  5. Approve the solicitor question pack and a budget for it. The pack in the response doc is good. Add clawback/payout timing (hold payout toward T-14 inside Stripe's window) as a default position to test, not an afterthought.
  6. Book the ten conversations this week. The three venue coordinators matter most; the plan already says so. Nothing else in Phase 3 or 4 starts until G1 and G2 have data. Their 3-to-5-day wedge build estimate is plausible given what already exists, provided the fix-four lands first.
  7. Name: decide nothing today. Brand tokens are genuinely swappable in the code (checked), so naming is not load-bearing. GigSure is the strongest candidate on the shortlist; check domains and trademarks when the wedge shows signal.
If only one thing gets agreed on the call: the wedge goes ahead, and its success gate is the venue response rate, measured exactly as written in the response document's section 5. Everything else in this review is fixable with ordinary engineering. That number is the venture.

Method and evidence

  • What was reviewed: the full repository donjjjordan/soundcheck (private; reviewed via the ElmsPark fork) at commit c0f48b1, "Response doc for Kenn's review + critic-pass fixes", 14 July 2026. Every source file was read (66 files: schema, migrations, all domain modules, server actions, payments, every page and component, seed, config). Six commits, all 14 July 2026, 13:43 to 22:50.
  • What was executed: npm ci (clean), the full unit suite (vitest run: 4 files, 33 tests, 33 passed) and the TypeScript check (tsc --noEmit: no errors), on 15 July 2026.
  • What was not executed: the running application (the audit machine has no Postgres; the README quickstart needs one) and the real-Stripe path (no keys, matching the repo's own declared status). Findings about runtime behaviour (F1, F7) are from code analysis, not live reproduction.
  • Taken on trust: the market research in Don's brief (competitor commissions, Encore/Poptop positioning, limiter prevalence). The response document says the TfL, Stripe, legislation and FCA claims were re-verified against primary sources; the code matches those claims everywhere it encodes them, and the load-bearing ones are consistent with our own knowledge, but this audit did not re-run that research.
  • Prior rounds: round 1 (14 July) reviewed the build brief only, before repo access. The response document and DECISIONS.md D22-D28 record what changed as a result. This document is the first review of the code itself.