Phase 15: Production Port — SPA Rebuild + Capacitor Ready

phase-wkq-15-production-port Phase

phase
Goal: Replace the current SSR westside-app entirely. Rebuild as SPA (adapter-static) with keycloak-js auth. Port all 11 playground pages. Capacitor-ready from day one — one codebase serves web and future iOS app.
Owner: Dev agent
Repo: forgejo_admin/westside-app
Depends on: phase-wkq-10-playground (design approved), phase-wkq-12-unified-registration (registration flow ready)

Architecture Change

This is a full architecture migration, not an incremental port.
Concern Current (SSR — replaced) New (SPA)
SvelteKit adapter adapter-node adapter-static
Auth library Auth.js (server-side OIDC) keycloak-js (client-side PKCE)
Keycloak client type Confidential (client secret) Public (no secret, PKCE)
Token location Server-side session In-memory (never localStorage)
Data loading +page.server.ts +page.ts / onMount fetch with Bearer token
Deployment Node.js container on k8s Static files served by nginx (or k8s static)
Capacitor Not possible Ready — static files wrap into native WebView

Why SPA

Westside is a phone-first app — parents, coaches, and Marcus all use it on mobile. Capacitor requires static files (no Node.js server in the WebView). Going SPA now means one codebase serves web + future iOS app without a rewrite. The mcd-tracker project validated this pattern: SvelteKit adapter-static + keycloak-js + Capacitor.

Scope

  • Same repo, clean break: nuke westside-app/src/, fresh SvelteKit scaffold with adapter-static + keycloak-js. Delete @auth/sveltekit, adapter-node, all +page.server.js files.
  • Wire keycloak-js auth in +layout.svelte — init, login, token refresh, role detection (3 roles: admin, coach, member). Point at prod Keycloak (keycloak.tail5b443a.ts.net).
  • Create new Keycloak client: public, PKCE enabled, redirect URIs for https://westsidekingsandqueens.tail5b443a.ts.net/* + capacitor://localhost/* + http://localhost/*
  • Connect to prod API directly — no Docker Compose for iteration 1. Bearer token from keycloak-js → basketball-api.tail5b443a.ts.net. Missing endpoints show placeholder UI.
  • Copy shared/app.css from playground — literal copy, CSS source of truth stays in playground
  • Port all 13 playground pages (HTML → +page.svelte with Svelte bindings from @-comment data contracts)
  • Port order by complexity: Low (index, tryouts, signin, coach-profile, team) → Medium (parent, coach, admin, admin-players) → High (register, player-profile, billing, admin-teams)
  • Data fetching: +page.ts universal load or onMount with keycloak.token as Bearer header
  • Role-based routing: unauthenticated → landing, admin → /admin, coach → /coach, member → /my-players
  • Role-based visibility: read @variants in each HTML file for {#if} guards per section
  • NO scoped Svelte <style> blocks — all CSS stays in global app.css
  • Dev overlay pattern: kustomize dev overlay in pal-e-deployments/overlays/westside-app/dev/ — node:22 image, hostPath mount to ~/westside-app, npm run dev --host. Same production URL (westsidekingsandqueens.tail5b443a.ts.net), same Keycloak redirect URIs, Vite hot reload. kubectl apply -k to flip between dev/prod. No new Tailscale hostname, no CORS changes, no capacitor dev hub needed.
  • Prod overlay: existing pal-e-deployments/overlays/westside-app/prod/ — nginx serving built SPA. ArgoCD auto-syncs when pushed.
  • Validate on phone via same production URL in dev mode
  • Retire current SSR westside-app deployment (replaced in-place by SPA)
  • Retire GitHub Pages site (ldraney/west-side-basketball) (after prod cutover)

Route Table (from playground README)

Playground File Production Route Role Complexity
index.html / Public low
tryouts.html /tryouts Public low
register.html /register Public high
signin.html /signin Public (keycloak.login()) low
parent.html /my-players Member medium
player-profile.html /players/[id] All (role-based sections) high
billing.html /players/[id]/billing Member (owner) + Admin high
team.html /teams/[id] All authenticated low
coach.html /coach Coach medium
coach-profile.html /coaches/[id] All authenticated low
admin.html /admin Admin medium
admin-players.html /admin/players Admin medium
admin-teams.html /admin/teams Admin high

Keycloak Client Config

  • Client authentication: OFF (public client)
  • Valid Redirect URIs: https://westsidekingsandqueens.tail5b443a.ts.net/*, capacitor://localhost/*, http://localhost/*
  • Web Origins: https://westsidekingsandqueens.tail5b443a.ts.net, capacitor://localhost, http://localhost

Deliverables

  • westsidekingsandqueens.tail5b443a.ts.net serves SPA with all 13 routes — MERGED. PR #37 squash-merged. SPA scaffold + keycloak-js + all 13 routes ported. QA approved.
  • keycloak-js auth working — login, logout, role redirect, token refresh — MERGED. westside-spa Keycloak client created (public, PKCE). Role fix: memberplayer to match realm.
  • API endpoint compatibility — MERGED. basketball-api PR #96. Prefix fix (dual registration), 6 new endpoints (/account/players, /admin/dashboard, /admin/players, /coaches/me, /coaches/{id}, PUT /players/{id}), CORS middleware. 35 new tests, 319 total pass.
  • All playground designs ported with data bindings to basketball-api
  • Capacitor-compatible — static output, no server dependency
  • Dev overlay — DONE. pal-e-deployments PR #27 merged. Dev (westside-dev.tail5b443a.ts.net) + prod overlay. Auth.js stripped.
  • Old SSR deployment torn down

Not In Scope

  • Capacitor iOS build / App Store submission (future phase)
  • Push notifications (future phase)
  • Offline mode (future phase)