Architecture: Jersey Intake Systems (A, B, C)

arch-jersey-intake Doc

architecture active arch:jersey-intake

Architecture: Jersey Intake Systems

Westside has three distinct jersey intake flows. They coexist intentionally — each serves a different audience with different identity guarantees. This note exists to prevent future agents from conflating them and trying to unify them prematurely.

Three Systems at a Glance

System Audience Identity Frontend Backend Storage
<strong>A — Roster</strong> Known roster players Parent registration token <code>westside-playground/jersey.html</code> → future SvelteKit route <code>basketball-api/routes/jersey.py</code> <code>players</code> table jersey fields
<strong>B — Public Intake</strong> Any player Marcus shares the link with Self-declared (name + email) <code>westside-landing/src/routes/jersey-public</code> <code>basketball-api/routes/jersey_public.py</code> <code>jersey_public_orders</code> table (new, migration 014)
<strong>C — Generic Checkout</strong> Registered parents buying jerseys/fees/tournaments Parent <code>registration_token</code> <code>westside-landing/src/routes/checkout</code> <code>basketball-api/routes/checkout.py</code> <code>products</code> + <code>orders</code> tables (migration 013)

Why Three Systems (And Not One)

  • Identity constraints differ. System A assumes Marcus knows who he's sending to. System B assumes he doesn't. System C assumes the parent is already a registered customer. Merging these forces one of the three assumptions onto the others and breaks the audience.
  • Auth models differ. A and C use parent registration_token lookups. B has no token at all. Trying to make B use a token leads to either a sentinel fake-parent hack or a schema change (nullable orders.parent_id) that ripples through C's production code path where real parents flow.
  • Risk surfaces differ. C touches real payment orders. Any schema change to C carries financial risk. B can iterate freely without touching C.
  • Verification in production code (2026-04-10): routes/checkout.py line 119 does Parent.registration_token == token and 404s if no match. Orders are tied to parents in the schema, not to ephemeral tokens. This is not a theoretical coupling — it's load-bearing.

System B Data Flow (Keycloak-Gated Intake)

  • Marcus shares westsidekingsandqueens.tail5b443a.ts.net/jersey-public with a player via text, GroupMe, or email
  • Player opens the link. The route lives under src/routes/(app)/jersey-public/ and is NOT on the PUBLIC_APP_ROUTES allowlist, so the existing (app)/+layout.svelte reactive guard auto-redirects unauthenticated visitors to /signin
  • Player signs in (or self-registers) via the westside-basketball Keycloak realm using the existing westside-spa client and $lib/keycloak.js initKeycloak() PKCE flow
  • After successful auth, Keycloak returns the player to /jersey-public. The page reads preferred_username and email from the JWT (via $lib/keycloak.js helpers) and prefills Player Name + Email, editable so a parent can submit on behalf of a child
  • Player fills remaining fields (Team, K/Q, Preferred Numbers 1-3, Top Size, Short Size, Tier) and submits
  • Frontend POSTs to /api/jersey-public-orders with Authorization Bearer JWT header
  • basketball-api validates the JWT via the existing keycloak_user dependency (same one used by routes/checkout.py), extracts the sub claim, inserts a row into jersey_public_orders with submitter_keycloak_sub, status=pending, and all submitted fields
  • basketball-api fires a Gmail notification to westsidebasketball@gmail.com via gmail-sdk in a FastAPI BackgroundTasks queue (non-blocking) per feedback_gmail_oauth_not_smtp.md. Notification includes the submitter_keycloak_sub so Marcus can cross-reference in Keycloak Admin Console if needed
  • Marcus opens /admin/jersey-orders (Keycloak admin-gated via the existing hasRole admin guard in (app)/+layout.svelte), reviews submissions, reconciles against parents.email and players.email, updates status plus linked_parent_id/linked_player_id
  • Marcus contacts the player out-of-band (text/call/GroupMe) to confirm and collect payment. Payment is not wired into System B yet, tracked as a future ticket

Email as Reconciliation Key

Since System B has no pre-existing identity, the single most reliable reconciliation signal is the email field. The admin review flow is:
This same query becomes the migration script the day System B flips to Keycloak-gated auth: rows where keycloak_sub IS NULL get backfilled by looking up Keycloak users by email.

Future: Keycloak Gating

The long-term vision is to replace System B's self-declared identity with Keycloak login. All outreach forms (jersey, tryout, waiver, fundraising, tournament RSVP) would require auth. Identity becomes the JWT claim, not a form field. Tracked in spike forgejo_admin/westside-playground#59. Email-as-key is forward-compatible with this change.

Production Chain for System B

Zero kustomize overlay changes. Both westside-landing and basketball-api already have production deployments. Merges to main trigger Woodpecker → Harbor image → kustomize newTag bump → ArgoCD sync.

The No-Merge Decision

Do not try to merge System B into System A or System C. Every previous attempt to unify these has crashed on the identity model. If a future agent proposes consolidation, point them at this note and require a written migration plan covering: (1) payment atomicity for C, (2) token lookup preservation for A, (3) backfill for B's anonymous rows. Absent that plan, the answer is no.
  • project-westside-basketball — project context, story:WS-S31
  • arch-generic-checkout — System C architecture
  • forgejo_admin/westside-playground#57 — System B playground prototype
  • forgejo_admin/westside-playground#59 — Keycloak-gated forms spike
  • feedback_never_stomp_archbox.md — the "do not merge systems casually" principle applied to code too