Architecture: Jersey Intake Systems (A, B, C)
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_tokenlookups. 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 (nullableorders.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.pyline 119 doesParent.registration_token == tokenand 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.
Related
project-westside-basketball— project context,story:WS-S31arch-generic-checkout— System C architectureforgejo_admin/westside-playground#57— System B playground prototypeforgejo_admin/westside-playground#59— Keycloak-gated forms spikefeedback_never_stomp_archbox.md— the "do not merge systems casually" principle applied to code too