Skip to content
REVIEW-2026-10

Current-state architecture review (October 2026)

Scope. server/database/schema.ts (38 tables), every tRPC router (server/trpc/routers/*), every REST handler (server/api/**), the migrations folder and journal, the migration and sync scripts, and the 180 stories in docs/architecture/stories.json (127 built, 33 partial, 19 missing, 1 unknown). Production was not queried; row counts come from docs/research/2026-10-12-city-canonicalisation.md (read-only production survey, 2026-10-12) and the repo CLAUDE.md. Line numbers refer to this branch.

The short version. The product surface is broad and mostly real, and the code is readable and tested (34 test files, 379 passing tests). The problems are below the surface: the database enforces almost nothing, the schema has no reliable change process, auth stores its secrets in plaintext and trusts unverified email for paid tickets, and three core concepts — an event, who is going, who owns a profile — are each modelled two or three times. The target design fixes these with eleven decisions (ADR-001 to ADR-011) and a 28-step migration that ships the security work first.

What is already good

  • Prices are resolved server-side — the client never sends an amount (festivalSignup.ts:182-202).
  • TicketTailor webhooks are HMAC-verified in constant time, fail closed (api/webhooks/tickettailor.post.ts:32-50, tickettailor.lib.ts:44-52), and Stripe signatures are verified (api/stripe/webhook.post.ts:22-28).
  • The v3 sync is idempotent, dry-run by default and reversible by source (repo CLAUDE.md "wedance.vip → 2026 event sync").
  • Privacy defaults lean conservative where they were thought about: share_going defaults to false (migrations/0020_opt_in_attendees.sql), public profiles never return email (profile.ts:8-17), election voter identity is shown to signed-in members only (election.ts:111-121).
  • Password continuity with v4 works (2,339 migrated hashes verify unchanged).
  • The generated /architecture docs cannot drift from the code (ADR-000).

Findings, ranked

Severity: security · data integrity · product-blocking · hygiene. Rank orders by urgency × impact across severities.

#SeverityFindingEvidenceConsequenceStoriesFix
1securityEmail verification gap in ticket claiming flow(redacted until fix ships — see ADR-003, step 3)Ticket ownership can be misattributedRAZ-83, RAZ-110ADR-003, step 3
2securitySecret storage and environment isolation gaps(redacted until fix ships — see ADR-003, steps 1–2)Credential exposure risk on database leakmap:meetup-2.1-sign-up, RAZ-155ADR-001, ADR-003, steps 1–2
3product-blockingThe daily wedance.vip sync has failed since 2026-10-10: it reads two migration files that PR #202 deletedscripts/sync-v3-events.ts:65; commit 4728250; ~/.config/wedance/v3-sync.log (FAILED 10-10, 10-11, 10-12)City feeds and event pages stop receiving new or changed v3 events; ~1,450 upcoming events go staleRAZ-251, RAZ-253, RAZ-162, RAZ-28, RAZ-71ADR-001, step 0
4data integrityA ticket bought after taking a free spot is never recorded: the Stripe webhook treats the unique conflict as "already processed"api/stripe/webhook.post.ts:65-88; festivalSignup.ts:96-101; unique at schema.ts:96Payment taken, no verified ticket, no confirmation emailRAZ-110, RAZ-130, RAZ-247, RAZ-248ADR-007, step 6
5data integrityAccount erasure fails for any dancer who followed an artist or waved: deleteAccount deletes by hand and misses the two newer tables; no FK has a delete rulerouters/auth.ts:497-612 (no artistFollows/festivalWaves); schema.ts:707, 722-723; all 51 FKs no action in migrations/meta/0018_snapshot.jsonGDPR erasure requests return an error; the account staysRAZ-155ADR-002, step 5
6securityMagic-link request can modify profile data before ownership is proven(redacted until fix ships — see ADR-003, step 3)Unauthenticated profile tamperingRAZ-241, map:meetup-2.1-sign-upADR-003, step 3
7data integrityNo trustworthy migration path: journal stops at 0017; 0018 unjournaled and not applicable to prod; two 0020s, no 0019; 0021–0024 hand-applied; DDL inside scripts; db:push would drop the prod-only follows table and dancers.source_refmigrations/meta/_journal.json; 0018_catch_up_and_report_dedupe.sql:31; 0023_profile_languages.sql:1-3; scripts/migrate/00-schema.mjs:5, 32; package.json:20Every schema change is a manual production operation; a wrong command loses migrated data or the v4 rollback keyallADR-001, step 0
8securityMissing upload validation and rate limiting on public write endpoints(redacted until fix ships — see ADR-003, step 4)Abuse of storage and public mutation endpointsRAZ-275, RAZ-272, RAZ-241ADR-003, step 4
9securitySeveral public API responses include data beyond what the caller should see(redacted until fix ships — see ADR-003, step 4)PII exposure in public endpointsRAZ-41, RAZ-116, RAZ-121, RAZ-127, RAZ-115, RAZ-83ADR-003, step 4
10product-blocking"Who is going" lives in two tables with opposite privacy defaults; public faces come from unverified clicks; listings and organiser insights count different peopleschema.ts:568-573 (share_going default false) vs 74-97 (roster_visibility default public_minimal); api/festivals.get.ts:11, 53-57; festivalInsights.ts:68-79"No fake friends" cannot be true; two attendee numbers per festivalRAZ-83, RAZ-62, RAZ-86, RAZ-115, RAZ-79, RAZ-150, RAZ-77ADR-008, step 24
11product-blockingProfiles have no owner: venues cannot see or answer bookings, nobody is notified, organisers cannot manage listings; festival ownership is guessed from a submission slugschema.ts:304-340 (claimed only, 334); booking.ts:222-243; festivalInsights.ts:30-39, 54-66The booking engine and organiser tools stop at "request saved"RAZ-129, RAZ-135, RAZ-142, RAZ-158, RAZ-159, RAZ-79, RAZ-150ADR-004, steps 13, 16
12product-blockingPending, unapproved booking requests (a public, unauthenticated form) appear immediately as events on public feedsbooking.ts:149 (public), 79, 111 (only declined filtered)Anyone can publish an event on a city pageRAZ-162, RAZ-158, RAZ-304ADR-006, step 20
13product-blockingFestival programme, teachers and ticket prices exist only in codeapp/pages/festivals/[slug].vue:27-34; app/data/mock-*.ts; server/utils/ticket-prices.ts:25-60Database-only festivals have no schedule or tickets; organisers need a deploy to sellRAZ-33, RAZ-36, RAZ-54, RAZ-69, RAZ-97, RAZ-149ADR-007, step 21
14product-blockingAn event lives in three places (events, booking requests, festivals) plus an is_festival flag, merged at read time; /events/<id> guesses the tableschema.ts:604-648, 369-370, 54-72, 613; app/pages/events/[id].vue:41-45; app/pages/cities/[city]/index.vue:83Every feature that references "an event" must pick a table; no FK possibleRAZ-162, RAZ-304, RAZ-105, RAZ-126ADR-006, steps 17–20
15data integrityNo transactions are possible (neon-http driver); booking.setAvailability calls transaction() and can never succeed; capacity checks and ELO votes raceutils/db.ts:2; booking.ts:292; node_modules/drizzle-orm/neon-http/session.js:151-152; festivalSignup.ts:84-101; dinner.ts:107-118; cityVideo.ts:262-281Venue availability cannot be saved; free spots and dinners can be oversold; lost votesRAZ-158, map:meetup-2.2-free-spot-counter, map:meetup-5.1-dinner-sign-upADR-002, step 7
16data integrityTicketTailor buyers never see their ticket in My Plan: rows are written with paid_amount = 0, myTickets requires > 0tickettailor.lib.ts:150; festivalSignup.ts:553-565"Ticket bought" stays off for verified buyersRAZ-99ADR-007, step 6
17data integrityReferral credits are spent when a checkout starts; an expired checkout never gives them backfestivalSignup.ts:384-397; api/stripe/webhook.lib.ts:39-47Referrers silently lose earned creditRAZ-23ADR-007, step 6
18data integrityLinks by text instead of foreign keys: festival slug on waves, review targets, event venue/organiser usernames, artist handle lists, plan item ids, election candidate ids, every city_slugschema.ts:721, 241-242, 617-618, 638, 376, 569, 449, 489-490, 636Renames break links; deletes leave orphans; no join is guaranteedRAZ-65, RAZ-89, RAZ-253, RAZ-161, RAZ-55ADR-002, steps 9, 17–19, 24
19product-blockingCity is free text in six columns and an unreferenced slug in ten; the cities table is referenced by nothing and absent from productionschema.ts:739-759; city-canonicalisation.md:18-45; plan.ts:483-484, 533585 spellings; audience geography impossible; per-city counts wrongRAZ-85, RAZ-64, RAZ-47, RAZ-127ADR-005, step 11
20product-blockingplan_items is polymorphic text: festival slugs, event or booking ids, mock workshop ids, goals; courses copied into JSON; workshop picks in DB and localStorageschema.ts:568-569; plan.ts:76-97, 232-250; app/pages/my-plan.vue:466, 1047-1048Dangling plan rows, frozen courses, two workshop storesRAZ-97, RAZ-108, RAZ-91, RAZ-287, RAZ-36ADR-009, step 25
21product-blockingEnquiries, problem reports and gig applications are never stored: Linear issues or mailto: onlyfeedback.ts:119-160, 162-248; RAZ-41 notesLost requests when Linear is down; customer PII only in a dev tool; posters never get applicationsRAZ-57, RAZ-107, RAZ-114, RAZ-41, RAZ-272ADR-010, step 16
22data integrity/@handle is shared by dancers and profiles with no cross-table uniqueness; dancer handles nullable until onboardingschema.ts:21, 306; entity.ts:6-11; auth.ts:377-386A dancer can shadow a venue's URL or the reverseRAZ-154, RAZ-253ADR-004, step 12
23securitySession management gaps: cookie flags, revocation on credential change(redacted until fix ships — see ADR-003, step 2)Sessions persist beyond intended lifetimemap:meetup-2.1-sign-up, RAZ-241ADR-003, step 2
24data integritySpace stewardship is authorised by comparing a free-text moderator_handle with the caller's username; the same fact is stored flat and in the election tableselection.ts:46-60; booking.ts:16-35; schema.ts:330-332; election.ts:346-354Authorisation tied to a mutable stringRAZ-152, RAZ-166ADR-004, step 13
25product-blockingTwo artist models: static app/data/artists.ts for /artists/[id] and lineups, profiles for /@handle and follows; no artist role, no schoolapp/pages/artists/[id].vue:8; app/pages/festivals/[slug].vue:5; schema.ts:307Following an artist does not show their festivals; teacher/school search has no dataRAZ-55, RAZ-66, RAZ-303ADR-004, step 14
26securityPassword salt generation uses insufficient entropy(redacted until fix ships — see ADR-003, step 3)Low-entropy saltsmap:meetup-2.1-sign-upADR-003, step 3
27data integrityTimes stored three ways (timestamp holding UTC, 'HH:MM' text, free text); server formats event times in its own zoneschema.ts:599-601, 378-379, 403-404, 550, 527; plan.ts:713-714Wrong times in the discover deck; no time arithmetic on slotsRAZ-127, RAZ-118, RAZ-158ADR-002, steps 8, 17
28data integrityCounters stored next to their facts drift: people_count never updated; vote_count/elo_score read-modify-write; report_count duplicates reportshangouts.ts:97, 132-178; cityVideo.ts:262-281; schema.ts:273Wrong "N in" and leaderboardsRAZ-118, RAZ-156ADR-002, step 7
29data integrityfestival_signups is unique on (festival_id, dancer_id) with a nullable dancer_id, so unclaimed stubs are never deduplicatedschema.ts:79, 96Duplicate stub rows per buyerRAZ-83ADR-008, step 24
30hygieneEnum-like columns are TypeScript-only and already disagree (review.ts accepts event, the schema does not)schema.ts:93, 159, 241, 385, 568; review.ts:6The database accepts any stringmap:meetup-8.1-rate-festivalADR-002, step 8
31hygienejson and jsonb mixed; created_at nullable on 34 tables and missing on 3; is_admin, events.slug/name nullable; events.archived defaults to trueschema.ts:9, 12, 16, 606-607, 620No indexing on json; ?? false scattered through code; a native event inserted without archived is hidden—ADR-002, steps 8, 17
32hygieneFK columns without indexessessions.dancer_id (schema.ts:48), booking_requests.space_id/profile_id/organizer_id/requester_id (360-366), dinner_signups.dancer_id, giveaway_entries.dancer_id, festival_signups.dancer_idSequential scans on deletes and owner lookupsRAZ-155ADR-002, step 8
33hygieneAPI conventions missing: bare Error → 500, unique violations found by message text, no pagination, N+1 lists, whole-row outputs, unbounded inputs, togglesfestivalSignup.ts:82, 93; dinner.ts:121-126; entity.ts:14-68; dinner.ts:34-90; hangouts.ts:41-60; gigs.ts:20-41; plan.ts:80Confusing errors, slow pages as data grows, retries that undo themselvesRAZ-58, RAZ-64, RAZ-118ADR-011, steps 10, 26
34hygieneCode guesses the schema at runtime (catches "column does not exist")events.ts:36-54; plan.ts:51-52Masks missing migrationsRAZ-251ADR-001, step 0
35product-blockingTracker drift: Pro and Network plans are Done/In Review with no billing in the model; several fully built features sit in BacklogRAZ-100, RAZ-106 notes; RAZ-152, RAZ-163–166, RAZ-157, RAZ-170 notesThe board misstates what existsRAZ-100, RAZ-106, RAZ-39open question (pricing)
36hygieneLeftovers: flat moderator fields next to elections; hangouts.people_count; a roommate request is only (festival, dancer); repo CLAUDE.md documents a 0021_events_v3_sync.sql and a follows table that schema.ts does not haveschema.ts:330-332, 554, 663-670; repo CLAUDE.md:63, 95Misleading docs and dead columnsRAZ-121, RAZ-118steps 13, 7, 24, 0

Decisions only the Commander can make

These block or shape steps of the migration plan; none of them is technical.

  1. Default visibility of a new "Going": hidden until the dancer opts in (today's share_going, recommended) or listed by name and city (today's ticket roster)? Step 24.
  2. "No fake friends": may unverified "Going" faces be shown at all, if they are labelled, or only verified ticket holders? Step 24.
  3. Pending booking requests: private until a venue or moderator accepts them (recommended), or shown as "requested" on the community calendar? Step 20.
  4. Which hardcoded festivals are real and current, and who confirms their programme and prices before they are seeded into the database? Step 21.
  5. City battles (3 votes ever) and giveaways (0 ever): remove, or keep and write the stories? Step 26.
  6. Is a school an organiser profile with the role school (recommended), or a profile type of its own? Step 14.
  7. Dancer-to-dancer follows / friends / crews: build them (the "add as friend" flow and map:meetup-8.4) or leave the legacy dancer edges parked? Step 15.
  8. Organiser plans and billing (Pro, Network): what is sold and at what price, before any subscription model is designed? RAZ-100, RAZ-106.
  9. Profile claims: what evidence is enough to hand someone a venue or artist profile, and who reviews it? Step 13.
  10. Handle collisions: profile handles keep their URL and colliding dancers get a new one (recommended)? Step 12.
  11. Verified email before a ticket attaches to an account: accept the extra step for some buyers? Step 3.
  12. Publication timing: /architecture is public. Publish this review on the live site only after Phase 0 ships?