Phase 15: Production Port — SPA Rebuild + Capacitor Ready
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-appDepends 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 withadapter-static+keycloak-js. Delete@auth/sveltekit,adapter-node, all+page.server.jsfiles. - 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.cssfrom playground — literal copy, CSS source of truth stays in playground - Port all 13 playground pages (HTML →
+page.sveltewith 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.tsuniversal load oronMountwithkeycloak.tokenas 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 -kto 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.netserves 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-spaKeycloak client created (public, PKCE). Role fix:member→playerto 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-deploymentsPR #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)
Related
- Plan: Westside Kings & Queens
- Phase 10: Playground — source designs
- Plan: mcd-tracker — validated the SPA + keycloak-js + Capacitor pattern