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>
6.3 KiB
Fase 1 — Autenticación y Colectivo
Duración estimada: 2–3 semanas
Objetivo: la base del dominio. Sin esto no existe nada más.
1.1 Base de datos
- Migración: tabla
users— espejo deauth.usersde Supabase, poblada por trigger al primer login OIDC:display_name,emaillanguage: enum(en | es)— defaulten, sobrescrito por preferencia de navegador en primer loginavatar_type: enum(initials | emoji | upload)— defaultinitialsavatar_emoji: text | nullavatar_url: text | null— URL firmada de Supabase Storage
- Migración: tabla
collectivescon camposid,name,emoji,created_by,created_at - Migración: tabla
collective_membersconrole: enum(admin | member | guest) - Migración: tabla
collective_invitationscon token, expiración y estado - RLS completo — políticas críticas:
- Un usuario solo puede leer colectivos a los que pertenece
- Solo administradores pueden insertar en
collective_invitations - Solo administradores pueden actualizar roles en
collective_members - Un usuario solo puede actualizar su propia fila en
users
- Trigger:
on_auth_user_created— sincroniza el perfil de Keycloak (display_name, email) ausersen cada login - Trigger: promoción automática al miembro más antiguo si el último admin elimina su cuenta (RN-10)
- Función SQL:
is_active_member(collective_id, user_id)— usada en todas las políticas RLS - Bucket Supabase Storage:
avatars— privado, acceso vía signed URLs, un fichero por usuario ({user_id}/avatar)
1.2 Integración Keycloak → Supabase
✅ DECISIÓN: Opción B — GoTrue como proxy OIDC. GoTrue recibe el token de Keycloak, lo valida y emite su propio JWT de Supabase.
auth.uid()resuelve al UUID de GoTrue (que mapea alsubde Keycloak). Esto mantieneauth.usersde Supabase poblado y permite usar todas las helpers de GoTrue.
- Configurar Keycloak como OIDC Identity Provider en GoTrue (
infra/docker-compose.dev.yml—GOTRUE_EXTERNAL_KEYCLOAK_*) - Flujo:
signInWithOAuth({ provider: 'keycloak' })→ Keycloak PKCE → GoTrue callback → Supabase session - Verificar que
auth.uid()en las políticas RLS resuelve correctamente — ✅ curl acceptance test: los 5 usuarios autenticados vía Keycloak→GoTrue tienensubcorrecto y las políticas RLS devuelven solo sus datos - Test de integración: token de Keycloak → exchange con GoTrue → query a Supabase con RLS → respuesta correcta — ✅ todos los 5 usuarios pasan
1.3 Invitaciones (sin email)
✅ DECISIÓN: sin email. Las invitaciones se crean como un link que el admin copia manualmente y comparte por cualquier canal (WhatsApp, etc.). No se integra Resend ni se envían emails.
collective_invitationstienetokenúnico (UUID),expires_at,role, yaccepted_at- Pantalla
/collective/managegenera el link de invitación y muestra un botón "Copy link" /invitation/[token]— el invitado abre el link, se autentica si no lo está, y acepta- Lógica de aceptación via función SQL
accept_invitation(p_token)SECURITY DEFINER (no Edge Function)
1.4 Internacionalización (i18n)
Librería: Paraglide JS (@inlang/paraglide-sveltekit). Compile-time, tree-shaken por idioma, sin overhead en runtime.
- Instalar y configurar
@inlang/paraglide-sveltekitcon el plugin de Vite - Crear ficheros de mensajes:
messages/en.jsonymessages/es.json - Configurar detección de idioma:
Accept-Languageheader en primer load → guardado enusers.languagetras login - Cambio de idioma en runtime: actualizar
users.languageen Supabase + llamar asetLanguageTag()de Paraglide — sin recarga de página - Traducir todas las cadenas UI de la Fase 1 (onboarding, gestión de colectivo, settings)
Las cadenas de UI van en
messages/. Nunca hardcodear texto visible al usuario directamente en componentes.
1.5 Frontend
- Integración de Supabase OAuth en SvelteKit (PKCE, no keycloak-js):
supabase.auth.signInWithOAuth({ provider: 'keycloak' })— compatible con Safari sin iframes- Store Svelte
auth— exponecurrentUser,authLoading,isAuthenticated,displayName login()redirige a Keycloak vía GoTrue; callback en/auth/callbackintercambia el code- Refresco automático de sesión por el cliente de Supabase (localStorage)
- Rutas protegidas:
(app)/+layout.tsconssr: falseque redirige si no hay sesión - Flujo de onboarding post-primer-login:
- Detectar si el usuario no pertenece a ningún colectivo → redirigir a
/onboarding - Opción A: Crear nuevo colectivo (nombre + emoji)
- Opción B: Pegar link de invitación recibido
- Detectar si el usuario no pertenece a ningún colectivo → redirigir a
- Pantalla
/collective/manage— miembros, roles, botón "Generate invite link" (copia al clipboard), expulsar - Pantalla
/invitation/[token]— aceptar invitación (el usuario debe estar autenticado) - Selector de colectivo activo en la barra lateral — un usuario puede pertenecer a varios colectivos y cambiar entre ellos
- Store Svelte
collective— colectivo activo, lista de miembros, rol del usuario actual, lista de todos los colectivos del usuario - Pantalla
/settings— configuración de usuario:- Campo display name (input de texto, guardado automático con debounce)
- Selector de avatar con tres modos:
- Initials — preview generado en tiempo real desde el display name
- Emoji — grid de 32 emoji curados, tap para seleccionar
- Upload — input de fichero; recorte en el navegador 1:1 con
cropperjs; subida a Supabase Storage bucketavatars
- Selector de idioma (English / Español) con efecto inmediato
- Enlace externo a Keycloak Account Console para cambio de email/contraseña
- Componente
Avatar.svelte— renderiza segúnavatar_type: initials (color determinista por hash del nombre), emoji, o<img>con fallback a initials
Auto-registro: Self-registration está habilitado en Keycloak. Un usuario sin cuenta que recibe un link de invitación puede registrarse en la pantalla de Keycloak y volver al callback de invitación automáticamente.
Criterio de aceptación: los 5 usuarios de prueba pueden autenticarse contra Keycloak, llegar a la app con sesión activa, y auth.uid() en Supabase resuelve correctamente para las políticas RLS.