fix(auth+pwa): resolve stuck-loading race condition and PWA build error
Auth: `getSession()` in a SvelteKit load function races with Supabase's async localStorage init and can return null for a valid session, triggering a spurious login() redirect. Moved auth redirect to (app)/+layout.svelte via $effect, which correctly waits for onAuthStateChange to fire. Also fixed collective data never loading on page refresh (INITIAL_SESSION event, not SIGNED_IN). PWA: SvelteKit blocks Workbox imports in src/service-worker.ts, preventing @vite-pwa/sveltekit from finding the compiled SW output. Switched to generateSW strategy (plugin auto-generates the SW). Custom SW deferred to Fase 2b using src/sw.ts (a filename SvelteKit does not intercept). Docs: added gotchas 6–8 to CLAUDE.md covering both root causes so they are not reintroduced. Updated plan/fase-2b with the sw.ts workaround note. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
CLAUDE.md
84
CLAUDE.md
@@ -8,12 +8,37 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Status
|
||||
|
||||
**Fase 0 complete. Fase 1 in progress.**
|
||||
**Fase 0 complete. Fase 1 complete. ✅ Acceptance test passed (all 5 users authenticate; `auth.uid()` resolves correctly in RLS).**
|
||||
|
||||
- `README.md` — full development plan, confirmed tech stack, Justfile reference, and technical warnings
|
||||
- `analysis/analisis-funcional.md` — complete functional specification (domain model, use cases, data models, business rules)
|
||||
- `plan/fase-*.md` — phase-by-phase implementation plans (Fase 0 through Fase 4)
|
||||
|
||||
### Fase 1 — what has been built
|
||||
|
||||
- `supabase/migrations/001_users.sql` — `users` table, `handle_new_user` trigger
|
||||
- `supabase/migrations/002_collectives.sql` — `collectives`, `collective_members`, `collective_invitations`; auto-promote-oldest-admin trigger
|
||||
- `supabase/migrations/003_rls.sql` — full RLS policies, `is_active_member`/`is_member`/`is_admin` helpers, `accept_invitation()` function
|
||||
- `supabase/migrations/004_storage.sql` — `avatars` bucket (private, signed URLs)
|
||||
- `apps/web/src/lib/supabase.ts` — Supabase singleton with PKCE flow
|
||||
- `apps/web/src/lib/auth.ts` — `login()` / `logout()` via `supabase.auth.signInWithOAuth`
|
||||
- `apps/web/src/lib/stores/auth.ts` — `currentUser`, `authLoading`, `isAuthenticated`, `displayName`
|
||||
- `apps/web/src/lib/stores/collective.ts` — `currentCollective`, `userCollectives`, `collectiveMembers`
|
||||
- `apps/web/src/lib/components/Avatar.svelte` — initials / emoji / upload with deterministic colour hash
|
||||
- `apps/web/src/lib/components/ImageCropper.svelte` — lazy-loaded cropperjs, 1:1, 256×256 WebP output
|
||||
- `apps/web/src/routes/+layout.svelte` — auth state listener, collective bootstrap
|
||||
- `apps/web/src/routes/(app)/+layout.ts` — SSR disabled (`ssr: false`); no auth check here (see gotcha below)
|
||||
- `apps/web/src/routes/(app)/+layout.svelte` — sidebar with collective switcher; auth redirect via `$effect`
|
||||
- `apps/web/src/routes/auth/callback/+page.svelte` — PKCE code exchange
|
||||
- `apps/web/src/routes/onboarding/+page.svelte` — create collective or join via link
|
||||
- `apps/web/src/routes/(app)/collective/manage/+page.svelte` — members, roles, invite link generator
|
||||
- `apps/web/src/routes/invitation/[token]/+page.svelte` — accept invitation (auth-aware)
|
||||
- `apps/web/src/routes/(app)/settings/+page.svelte` — display name, avatar, language, sign out
|
||||
- `apps/web/messages/en.json` + `es.json` — all Fase 1 strings
|
||||
- `packages/types/src/database.ts` — manually seeded from migrations (update with `just db-types` once CLI is wired)
|
||||
- `setup.sh` — idempotent local dev setup (prerequisites, .env, Docker, migrations, seed, Paraglide)
|
||||
- `infra/kong-start.sh` — Kong container entrypoint; substitutes `${VAR}` placeholders in `kong.yml` before Kong starts (Kong 2.8 does not do this natively)
|
||||
|
||||
## Local Dev: Required /etc/hosts Entry
|
||||
|
||||
GoTrue (Supabase Auth) and the browser both must resolve `keycloak` to reach the Keycloak container. Add this line to `/etc/hosts` on the development machine:
|
||||
@@ -41,8 +66,8 @@ Without this, the OAuth redirect URL (`http://keycloak:8080/...`) built by Keycl
|
||||
|
||||
```
|
||||
apps/web/src/lib/
|
||||
supabase.ts # Supabase singleton client
|
||||
auth.ts # keycloak-js OIDC client + session helpers
|
||||
supabase.ts # Supabase singleton client (PKCE, GoTrue auth)
|
||||
auth.ts # login() / logout() via supabase.auth.signInWithOAuth (Keycloak provider)
|
||||
stores/ # Svelte global stores
|
||||
sync/ # offline queue + conflict resolution
|
||||
components/ # reusable UI components
|
||||
@@ -66,8 +91,11 @@ infra/
|
||||
## Development Commands (Justfile)
|
||||
|
||||
```bash
|
||||
just setup # first-time (and idempotent) local environment setup
|
||||
just dev # start docker-compose.dev.yml + SvelteKit dev server
|
||||
just dev-stop # stop local environment
|
||||
just stop # stop all services (Docker stack + Vite dev server)
|
||||
just dev-stop # stop Docker stack only
|
||||
just dev-clean # stop Docker stack and remove all volumes (full reset)
|
||||
just db-reset # drop + migrate + seed (dev)
|
||||
just db-migrate # apply pending migrations (dev)
|
||||
just db-seed # apply supabase/seed.sql to the running dev db
|
||||
@@ -80,15 +108,24 @@ just restore supabase <file> # restore a specific dump
|
||||
just logs # docker compose logs -f (prod)
|
||||
```
|
||||
|
||||
## Local Dev Endpoints
|
||||
## Local Dev Endpoints and Credentials
|
||||
|
||||
| Service | URL | Notes |
|
||||
| Service | URL | Credentials (dev only) |
|
||||
|---|---|---|
|
||||
| SvelteKit app | http://localhost:5173 | |
|
||||
| Supabase Studio | http://localhost:54323 | |
|
||||
| Supabase API (Kong) | http://localhost:8001 | Port 8000 reserved by other services |
|
||||
| Keycloak Admin | http://localhost:8080/admin | admin / admin (dev only) |
|
||||
| PostgreSQL | localhost:5432 | postgres / postgres (dev only) |
|
||||
| SvelteKit app | http://localhost:5173 | — |
|
||||
| Supabase Studio | http://localhost:54323 | — |
|
||||
| Supabase API (Kong) | http://localhost:8001 | apikey: `PUBLIC_SUPABASE_ANON_KEY` from `.env` |
|
||||
| Keycloak Admin | http://localhost:8080/admin | `admin` / `admin` |
|
||||
| PostgreSQL | localhost:5432 | `postgres` / `postgres` |
|
||||
|
||||
**Dev secrets (set in `.env`, never commit to prod):**
|
||||
|
||||
| Variable | Dev value |
|
||||
|---|---|
|
||||
| `POSTGRES_PASSWORD` | `postgres` |
|
||||
| `SUPABASE_JWT_SECRET` | `super-secret-jwt-token-with-at-least-32-characters-long` |
|
||||
| `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` | `admin` / `admin` |
|
||||
| `KEYCLOAK_CLIENT_SECRET` | `gotrue-dev-secret` (client secret for `colectivo-web` in GoTrue) |
|
||||
|
||||
## Domain Model
|
||||
|
||||
@@ -116,7 +153,27 @@ Key decisions:
|
||||
- **Invitations are link-only** — no email sent. Admin generates a link and shares it manually.
|
||||
- **Multiple collectives per user** — a user can belong to several collectives and switch between them in the sidebar.
|
||||
|
||||
Logout must invalidate both sides: call `keycloak.logout()` (invalidates Keycloak session) AND clear the Supabase client session locally.
|
||||
Logout is a single call — `supabase.auth.signOut()` — which clears the GoTrue session. Keycloak's own SSO session persists (user won't be prompted for credentials on next login until the Keycloak session expires), which is acceptable for this app.
|
||||
|
||||
**Non-obvious integration requirements (all implemented; do not remove):**
|
||||
|
||||
1. **`colectivo-web` is a confidential client** — GoTrue needs a client secret to exchange codes with Keycloak. `GOTRUE_EXTERNAL_KEYCLOAK_SECRET` must match Keycloak's client secret (`gotrue-dev-secret` in dev).
|
||||
|
||||
2. **Custom `openid` client scope in Keycloak** — GoTrue v2.158.1 sends `scope=profile email` (no `openid`) to Keycloak, ignoring `GOTRUE_EXTERNAL_KEYCLOAK_SCOPES`. Without `openid` in the token scope, Keycloak's userinfo endpoint returns 401. Fix: a custom client scope named `openid` with `include.in.token.scope=true` is set as a default scope on `colectivo-web`, so Keycloak injects `openid` even when not requested.
|
||||
|
||||
3. **`GOTRUE_DISABLE_SIGNUP: "false"`** — GoTrue's signup flag blocks OIDC-based user creation too. Must be `false`; Keycloak is the gatekeeper for who can register.
|
||||
|
||||
4. **`auth.users` pre-seeded with Keycloak UUIDs** — GoTrue generates its own UUIDs on first login. `seed.sql` pre-inserts both `auth.users` (with `instance_id='00000000-0000-0000-0000-000000000000'` and empty-string token fields) and `auth.identities` (provider=keycloak, provider_id=Keycloak sub). This ensures `auth.uid()` = seed UUID = FK in all public tables.
|
||||
|
||||
5. **Kong env var substitution via `kong-start.sh`** — Kong 2.8 does not interpolate `${VAR}` in declarative config files. `infra/kong-start.sh` runs `perl -pe 's/\$\{(\w+)\}/$ENV{$1}/ge'` on `kong.yml` before starting Kong. Without this, Kong loads the literal string `${SUPABASE_ANON_KEY}` as the API key, rejecting all requests.
|
||||
|
||||
**SvelteKit + Supabase auth gotchas (do not remove these patterns):**
|
||||
|
||||
6. **Do not call `getSession()` in a SvelteKit `load` function to gate auth.** Supabase JS v2 initialises its session from localStorage asynchronously (`_recoverAndRefresh`). In a `load` function that runs immediately on mount, `getSession()` can return `null` for a valid session before initialisation completes — causing `login()` to fire and redirect to Keycloak unnecessarily. The correct pattern: `(app)/+layout.ts` only sets `ssr: false`, and `(app)/+layout.svelte` uses a `$effect` that redirects when `!$authLoading && !$isAuthenticated`. This waits for `onAuthStateChange` to fire (which is authoritative).
|
||||
|
||||
7. **`onAuthStateChange` fires `INITIAL_SESSION` on page load, not `SIGNED_IN`.** `SIGNED_IN` only fires immediately after a login flow. Load collective data whenever the session is present, regardless of event type — otherwise collectives are never loaded on page refresh. Pattern in root `+layout.svelte`: `if (session) { await loadUserCollectives(...) }` covering `INITIAL_SESSION`, `SIGNED_IN`, and `TOKEN_REFRESHED`.
|
||||
|
||||
8. **PWA service worker: do not use `injectManifest` with a file named `service-worker.ts`.** SvelteKit intercepts any file at `src/service-worker.[jt]s` and enforces a hard restriction: only `$service-worker` and `$env/static/public` may be imported — Workbox modules are blocked. With `injectManifest`, `@vite-pwa/sveltekit` tries to find the compiled SW output at `.svelte-kit/output/client/service-worker.js`, but SvelteKit never produces it (compilation fails on the blocked imports). Fix for Fase 1–2a: use `generateSW` strategy (plugin auto-generates the SW, no custom source needed). Fix for Fase 2b when a custom SW is needed: name the file `src/sw.ts` — SvelteKit does not recognise it as a service worker — and set `strategies: 'injectManifest'`, `filename: 'sw.ts'` in `vite.config.ts`.
|
||||
|
||||
## Sync Strategy
|
||||
|
||||
@@ -131,7 +188,8 @@ Logout must invalidate both sides: call `keycloak.logout()` (invalidates Keycloa
|
||||
## Critical Platform Gotchas
|
||||
|
||||
**iOS Safari / PWA:**
|
||||
- `keycloak-js` silent token refresh uses iframes (`checkLoginIframe`) — Safari blocks third-party cookies in iframes. **Set `checkLoginIframe: false`** and use `updateToken()` manually instead.
|
||||
- The app uses Supabase OAuth PKCE flow (`signInWithOAuth({ provider: 'keycloak' })`). No keycloak-js, no iframe-based silent refresh — Safari-compatible by design.
|
||||
- GoTrue session is persisted in `localStorage` and auto-refreshed by the Supabase client. No manual `updateToken()` needed.
|
||||
- Background Sync API is not supported in Safari. Implement a manual fallback using the `online` event.
|
||||
- Push notifications require iOS 16.4+ and the PWA must be installed to the home screen.
|
||||
- Test the shopping session mode on a real iPhone during development, not only in DevTools.
|
||||
|
||||
Reference in New Issue
Block a user