SHOTid auditfeat/v2 · 38549fa · 2026-05-16
Engineering · §04

Engineering critique + counter-proposals

Branchfeat/v2 Commit38549fa AudienceJonny, Liam, eng
04

Critical findings

Distinguished Engineer verdict · confidence 9/10

Reconsider — bordering on Reject for the OIDC plan

A pretty avatar picker dressed up as an identity platform, with the actually-hard parts deferred to "phase 2" and underestimated by an order of magnitude. The "1–2 week Cloudflare Worker OIDC issuer" is the single most dangerous decision in the codebase — and the only good news is that it is not yet in code.

P0 · security

pending_profiles RLS is USING (true)

The public anon key is in your shipped JS bundle. Anyone can SELECT * and live-watch every registration in flight: handles, traits, geo, locales. Calling this "v0 known gap" in a migration comment is professional malpractice.

P0 · security

lookup_signin_email_by_handle is a phishing oracle

SECURITY DEFINER RPC maps public handle → real recovery email. Enumerate handles, build (handle, email) pairs, run against haveibeenpwned. Tuesday for any attacker.

High · product

The defining product does not exist

Zero OIDC code. Zero VC issuance. Federation router returns 501. Federation portal and admin are empty. @sporthead/identity empty. You built the gift wrapping before the gift.

High · architecture

Next.js for a static identity surface is a category error

output:"export" turns off every feature Next.js exists to provide. Vite + React or Astro would give the same output in a third of the bundle and seconds-not-minutes builds.

High · auth

Supabase Auth is being abused, not used

PIN-as-password, synthetic @anon.sporthead.id emails, mailer_autoconfirm=true, RPC to leak email by handle. You pay the cost of both Supabase Auth and a bespoke flow with the benefit of neither.

High · ops

Founder-number race + Turnstile day-of risk

No SERIALIZABLE txn or advisory lock around founder-number assignment. Turnstile secret is unset because both wrangler configs have <TODO> placeholders. A 90-second bot run destroys the cohort meaning.

Medium · code

Zero tests · zero PR CI · zero observability

No ESLint config, Prettier, Husky, lint-staged, commitlint. Changesets configured in package.json but .changeset/ never bootstrapped. Sentry/PostHog/OTel all absent.

Medium · architecture

Runtime federation theming is a single failure domain

One Next.js codebase serving all federations means one bad deploy nukes SLFA + WAB + everything. Per-tenant SLA, analytics, audit, rate-limit are impossible without re-inventing per-tenant deploys badly. ADR-0005 is wrong.

Medium · data

pin_hash exists, nothing writes to it

Signature of a half-finished mental model. Either populate it server-side or drop the column. Dead schema becomes a future midnight incident.

06

Evaluation criteria

Six criteria, weighted toward the December 2026 launch reality. Each option scores 1–5 against each.

Closes critical security gapsRLS + email leak fixed before launch
0.25
Match to product visionOAuth provider · federation · VC issuance
0.22
Likelihood of Dec 2026 launchSierra Leone fight-night cohort cut
0.20
Total engineering costperson-weeks + vendor £
0.13
Operational simplicityvendors, runbooks, blast radius
0.12
Reversibilitycan we change direction in 6 months?
0.08
07

Options

Four shapes, ordered by size of change. A is the implicit baseline; B/C/D are the counter-proposals.

Option A · status quo

Keep going, build OIDC + VC + federation in-house as planned

Bespoke OIDC Cloudflare Worker in 1–2 weeks, VC signing keys on Cloudflare with joint control, runtime-themed federation portal, single Next.js static-export app.

Security1
Vision2
Launch1
Cost4
Ops2
Reversible3
weighted1.85 / 5
Option B · small (this sprint)

Patch the holes, ship registration-only for Dec

Close pending_profiles RLS · kill the email-leak RPC · CI gates on PRs · provision workers + secrets · founder-number lock · schedule cleanup · Sentry · drop pin_hash. No OIDC, no VC, no federation portal at launch.

Security5
Vision2
Launch5
Cost5
Ops4
Reversible5
weighted4.04 / 5
Option C · medium (next quarter)

Adopt Ory Hydra · drop Next.js for Vite · move pending_profiles out

Self-host Ory Hydra (OIDC-certified) on Fly.io, keep the PIN-first UI as Hydra's login/consent app. Migrate sporthead-id to Vite + React. Move staging registration to Worker + D1 entirely. Real OIDC, real key custody, no synthetic emails.

Security5
Vision4
Launch3
Cost3
Ops3
Reversible4
weighted3.80 / 5
Option D · big (greenfield)

Re-architect: Hydra + Kratos · Hono Workers · per-federation Pages

Ory Hydra (OIDC) + Ory Kratos (identity). Hono workers per bounded context. Astro for sporthead.com, Vite+React for sporthead.id, one Pages project per federation. Postgres only · KMS-backed VC keys · Sentry + Axiom + Grafana · full CI from day one.

Security5
Vision5
Launch2
Cost2
Ops3
Reversible2
weighted3.57 / 5
08

Comparison matrix

Darker green = stronger fit · rust = weak. Bottom row shows weighted totals.

A · status quo B · small C · medium D · big
Closes critical security (0.25)1555
Match to product vision (0.22)2245
Likelihood of Dec 2026 launch (0.20)1532
Engineering cost (0.13)4532
Operational simplicity (0.12)2433
Reversibility (0.08)3542
Weighted total1.854.043.803.57
09

Trade-off radar

Same six criteria projected as a hexagon. Larger area is stronger overall fit. B dominates the launch-critical axes; D wins on vision but pays for it elsewhere.

security vision launch cost ops reversible
  • A · status quo
  • B · small (leader)
  • C · medium
  • D · big
10

Tentative ranking

Weighted-total order. Not a decision — surfaced for the architecture review on 2026-05-20.

1stB · small (this sprint)4.04
2ndC · medium (next quarter)3.80
3rdD · big (greenfield)3.57
4thA · status quo1.85

Recommended path: B now → C next quarter. B ships registration + founder cohort + numbered VC by December without compromising security; C earns the "single sport identity" billing in Q1 2027 with Hydra in front of the PIN-first UX. D is the right shape if you were starting tomorrow but is too disruptive given Sierra Leone fight-night is six months out.

11

Open questions for the review

Has the anon key been rotated since pending_profiles open RLS shipped?

If not, anything pulled while the policy was open should be treated as already-exposed. Rotate + audit logs from the Supabase project before we close the policy.

@liam · before next commit

Do we accept "registration + founder cohort only" for December?

Option B explicitly defers OIDC, VC issuance, and federation portal. The launch story becomes "claim your Sport Head + your number" — not "Sign in with SHOT". Is that the right narrative for Sierra Leone?

@jonny · architecture review

Hydra hosting: Fly.io vs Railway vs CF Containers?

Option C and D both depend on this. Need a binding ops decision — who runs it, what's the SLO, how is the postgres backup story coupled to the Supabase one?

@liam · spike

SLFA seed row: copy-paste residue or real plan?

The seed in migration 08 has slug='slfa' + vanity_domain='westafricaboxing.com' + sport='Boxing'. SLFA is the Sierra Leone Football Association. Either the seed is wrong or the federation model conflates two distinct partners.

@jonny · clarify