Skip to content

Implementation Roadmap — 2026-07 Audit Remediation

Status: Archived

The master execution plan for the whole-app audit. It sequences the slices defined across the twelve problem-space solution docs (solutions/) into dependency-ordered waves, each slice carrying its own E2E verification gate.

  • Findings (what's wrong): ../history/ (16 blocks, now archived alongside everything else)
  • Solutions (how each is fixed, with research): solutions/psN-*.md
  • This doc (in what order, with what gates): the sequencing authority.

Supersedes the earlier triage in implementation-plan-2026-07-audit.md; cites wiring-plans-2026-07-backend.md and design-specs-2026-07-ui.md for object-level detail rather than repeating.

Standing rules (apply to every slice)

  1. E2E-verify at the end of each slice (owner directive). Beyond yarn check: prove the flagged problem is actually fixed end-to-end — a seeded scenario exercising the real RPC+RLS+client path (scripts/run-scenario.mjs), an OTA on-device check for UI, or a Maestro flow where device proof is required. Record the evidence in the slice.
  2. Backend slice = migration → local db reset + psql/synthetic probe → yarn check → commit → OWNER prod push. Client slice = yarn check → commit → OTA → baseline advance. The prod-push step is owner-gated (the permission classifier blocks agent prod deploys).
  3. Migration numbers are assigned at implementation time from the then-next-free number. The 00388+ numbers in the solution docs are provisional and overlap across docs (parallel authorship) — this roadmap fixes the order, not the integers. Assign sequentially as each slice lands.
  4. One migration per slice, CREATE OR REPLACE + DROP … IF EXISTS, SECDEF hygiene (search_path='' + REVOKE … FROM PUBLIC, anon + explicit GRANT), explicit per-table GRANT for new tables, signals_emit for events, no PII in JSONB.
  5. Docs updated per slice (screen-blueprint / canon / rebuild-log as touched).

Wave 0 — Unblock (owner action, gates everything)

#ActionWhy first
0.1npx supabase db push — the already-shipped 00385 (alert wiring + TOCTOU restore), 00386 (PortOne anon-revoke), 00387 (venue RPC lockdown + avatar ownership)Two are live unauthenticated prod holes. Nothing else should ship on top of an unpushed security baseline.
0.2DONE — PS11 Slice 1: fixed 3 of the 19 pgTAP files that had drifted from schema/RPC changes (the "already pass locally" claim was never actually run — clubs.tags dropped in 00278 but referenced in 00016_rls_club_media_growth.test.sql's fixture AND live in get_club_growth_analytics itself, fixed via migration 00392; submit_match_score's split tiebreak-score params in 00002_rpcs.test.sql; 00010_rls_clubs.test.sql test 13 rewritten to call join_open_club after 00145/00146 replaced direct self-join INSERTs), then wired supabase test db into CI as the pgtap job (.github/workflows/ci.yml) + yarn db:testTurns on the verification harness that every Wave-1 security slice's guard plugs into. All 19 files green, 210 assertions.

Wave 1 — P0 Security (live exposure)

Order within the wave = exploitability. Each ships with the pgTAP guard that prevents its regression (PS11).

SliceSpaceWhatGate
1.1PS1profiles_select PII leak → route reads through the visibility-enforcing RPC pattern (public/private split); verified blast radius = 7 read methodsprobe: unrelated user can't read phone/kakao_id/push_token
1.2PS1profiles_update column-forge → extend the field-protection trigger to trust_tier/singles_elo/doubles_elo/rating_confidence/kakao_id/nationality_codeprobe: self-PATCH of trust_tier reverts
1.3PS1 + PS9venue_corrections GRANT lockdown (RPC-only, 00329 template) + resolve_venue_facts per-row exception isolation (malformed row no longer aborts a 400-venue batch)probe: raw insert blocked; malformed correction isolates
1.4PS1 + PS6guest_applications RPC-only lockdown + capacity/status gate in decide_guest_applicationprobe: raw PATCH approve blocked; over-capacity rejected
1.5PS1app_config explicit REVOKE + deny-all RLS (holds service-role key)probe: anon/authenticated select denied
1.6PS1 + PS11Structural SECDEF sweep — REVOKE … FROM anon on the ~35 remaining anon-executable SECDEF fns; corrected ALTER DEFAULT PRIVILEGES; fix the false canon claim (data-and-hooks.md:89); pgTAP ACL-hygiene guard (G1) + REVOKE-idiom migration linter (G2)pgTAP: no state-mutating SECDEF fn is anon-exec without a guard
1.7PS1 + PS11Explicit per-table GRANT sweep (7% → 100%) + table_grant_hygiene pgTAP (G3)pgTAP passes

Wave 2 — P1 Correctness (money & data integrity)

No live exploit, but wrong. Grouped by the shared trigger/table families the research identified so co-located changes land together.

2A — Match & settlement integrity (PS2 + PS4 share the matches trigger family — land as a coordinated set)

  • 2A.1 PS4: submit_match_score status guard (reject resubmit on completed → route via 00308 correction) + agree_to_correction writes score_status (else corrected-disputed sticks).
  • 2A.2 PS4: complete the matches protection surface — RLS status predicate on team-slot columns + fix protect_matches_scoring's dead tiebreak_score reference (team1/team2_tiebreak_score silently unprotected since 00147).
  • 2A.3 PS2: widen enforce_settled_match_participants trigger to the four team-slot columns (settle-gate bypassed by swaps) — same trigger family as 2A.2, verify ordering.
  • 2A.4 PS4: participant-slot validation (members / confirmed RSVPs).
  • 2A.5 PS4: disputed state machine — reject_call writes score_status='disputed' + resolution path → makes the dead HomeDisputedAlertBanner/ScoreStatusBadge live.
  • Gate: seeded match scenario — completed-match resubmit rejected; unsettled swap rejected; disputed round surfaces + resolves.

2B — Payments lifecycle (PS2)

  • 2B.1 hold-lifecycle signal gaps: 00384 inline completion-cancel emits hold_expired; payer-cancel resolves hold_created.
  • 2B.2 session_payments UPDATE WITH CHECK (protect amount/user_id) via the protect_columns idiom.
  • 2B.3 client: use-live-session-data invalidates sessionPaymentKeys (stale-hold UI, U3).
  • Gate: seeded fee-session scenario — hold states + signals correct; UI reactive.

2C — Dues & attendance (PS5)

  • 2C.1 canonical dues_day-precise "overdue" — one function/view; route all 5 surfaces through it.
  • 2C.2 partial payment — wire the dead payment_history ledger (receipt rows) + dues.paid_amount trigger rollup + entity/mapper/adapter (entity-change checklist).
  • 2C.3 waive — audit trail + dues_waived resolution (unresolvable-critical-push fix).
  • 2C.4 strike-alert resurrection — edge-triggered emit + decay-resolve (the decay half is call-site-independent, ship first).
  • 2C.5 ex-member dues exclusion — active-membership INNER JOIN in cron + admin-snapshot.
  • Gate: seeded dues scenario — overdue agrees across surfaces; waive clears push; partial records amount; ex-member gets no push.

2D — Session & RSVP lifecycle (PS3)

  • 2D.1 canonical waitlist-promotion primitive (dequeue_next_waitlisted FOR UPDATE SKIP LOCKED + promote_next_waitlisted) — cancel/expiry/forfeit all call it; kill the dead path.
  • 2D.2 capacity TOCTOU — SELECT … FOR UPDATE on the session row before the count (00313 idiom).
  • 2D.3 allowlist status-transition guard (replaces the 2-rule blocklist) + RSVP-to-terminal-session block.
  • 2D.4 sessions_select_public RLS (visibility='public' AND public_invite_enabled) — unblocks the pickup preview and PS9's funnel leak.
  • Gate: concurrency probe (two last-seat RSVPs) + non-member preview loads + illegal transition rejected.

2E — Membership & account lifecycle (PS6 + PS7 — one canonical deletion path, two owners)

  • 2E.1 PS6: sole-owner succession (auto-transfer to longest-tenured admin, else auto-archive) + ghost-member deactivation on deletion + backfill.
  • 2E.2 PS7: delete_account_atomic completeness — session-domain tables + banned_until='infinity' (deleted user can currently re-auth and self-clear deleted_at) + release_departed_user_open_rsvps backstop.
  • 2E.3 PS7: marketing-consent withdrawal backfill (after the client label fix bakes).
  • Gate: seeded deletion scenario — no ghost membership, no orphan RSVPs/holds, club not locked, account can't re-auth.

Wave 3 — P2 Dead pipelines & surfacing (high user value, low risk, mostly OTA)

SliceSpaceWhat
3.1PS6Join-request approval UI (CRITICAL dead pipeline — server ready) — wire useClubJoinRequests/useDecideJoinRequest into the members screen (Wireframe A) + the missing admin-snapshot invalidation key
3.2PS8Push-tap routing (CRITICAL) — both producers converge on getSignalRoute via one {category,context} shape + tests; retire the duplicate local reminder scheduler; DM emitter writes context.threadId
3.3PS8Admin-count single source (sumAdminAttentionTotal) + signalMatchesScreen full category map
3.4PS7OAuth cancel/failure error-surfacing (one throw e) + login screen for when EXT-11 creds land (credential-gated parts flagged)
3.5PS7 + PS8알림 설정 screen (Wireframe C) — claim the dead /settings/signal-preferences route, edit signal_preferences (13 cat × in-app/push/severity/quiet) + retire/adopt the 6 dead notify booleans
3.6PS9Discovery UX — confidence-tier badge honesty, cause-aware empty states, single region-filter component, naver_place deep-data surfacing (hours + review-keyword chips; zero migration), reopen path
3.7PS10Records UX — unified zero-match empty state (Wireframe D, the most common launch state), sparkline direction, solo-region leaderboard, "내 순위", tap-to-profile on podium/partners, TierInfoSheet Tamagui rebuild, DEFAULT_ELO=1000, challenge bridge → app root
3.8PS6/PS9/PS10Membership/venue lifecycle signals (member-left/removed/settings/unarchive) + manner_tag_received

Wave 4 — P3 Guardrails (interleave; ratchets applied as files are touched)

PS11 remaining slices (G4 no-localecompare-sort ESLint rule + 14-site ratchet baseline; G5 signal completeness test; G6 knip dead-export detection; G7 rls_with_check_completeness pgTAP). Most are ratchets that also retire the ICU localeCompare sites the earlier waves touched (PS6/PS9/PS10 share the plainStringCompare module — land it once, in the first wave that needs it).

Wave 5 — PS12 Engineering streamline (LAST, gated on Waves 1-4 E2E-confirmed)

Only after every problem space ships AND E2E-confirms: the four conformance passes (SSoT / prefer-libraries + knip / hexagonal-SoC / test-suite backfill). Reserved as the final wave; slices defined against the measured residue. See solutions/ps12-engineering-streamline.md.

Cross-wave dependency graph (the load-bearing edges)

  • Wave 0.1 (prod push) → everything. No slice ships on an unpushed security baseline.
  • Wave 0.2 (CI pgTAP) → all Wave-1 guards.
  • 2A.2 ↔ 2A.3 (same matches trigger family — coordinate).
  • 2E.1 ↔ 2E.2 ↔ 2C.5 (one deletion truth: membership deactivation, session-domain cleanup, dues exclusion must agree).
  • 3.5 depends on 3.4's consent-label fix baking before 2E.3's backfill.
  • plainStringCompare / PlayerIdentityChip / region-filter component are shared primitives (PS6/PS9/PS10) — build once, first consumer owns it, others cite.
  • Wave 5 depends on Waves 1-4 complete + E2E-confirmed.

Scale

~55 slices across 12 spaces. Roughly: 7 P0-security, ~20 P1-correctness, ~12 P2-pipelines, ~7 P3-guardrails, ~4 P12-streamline, plus the shared primitives. Backend slices are owner-gated at the prod-push step; client slices ship on OTA autonomously.

Markdown remains the source of truth. Run yarn docs:check before handoff.