- CLAUDE.md: project guidance (stack, domain model, auth, i18n, gotchas) - DESIGN.md: full design system spec derived from Stitch mockups (slate monochrome, Inter, shadcn-svelte, screen patterns for all 5 views) - README.md: development plan with Justfile commands - analysis/: functional spec (domain model, use cases, business rules) - plan/: phase-by-phase implementation plans (Fase 0–4) - .stitch/designs/: generated mockups (lists, shopping session, notes, tasks, list detail with frequency chips) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.0 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
El JWT que emite Keycloak debe ser validable por Supabase RLS. Hay dos estrategias; elegir una antes de implementar:
Opción A — JWT de Keycloak directo en Supabase (recomendada para self-hosted):
- Configurar
JWT_SECRETde Supabase con la clave pública de Keycloak (RS256) - Supabase valida el token de Keycloak directamente en RLS via
auth.uid() - Sin intermediarios, latencia mínima
- Requiere que el claim
subde Keycloak sea el UUID que identifica al usuario en Supabase
Opción B — GoTrue como proxy OIDC:
- Supabase Auth recibe el token de Keycloak, lo valida y emite su propio JWT
- Más aislamiento, pero un salto extra en cada request
Decisión recomendada: Opción A. Más simple operacionalmente en self-hosted. El
subde Keycloak se convierte en elauth.uid()de Supabase.
- Configurar
supabase/config.tomlcon Keycloak como OIDC externo - Verificar que
auth.uid()en las políticas RLS resuelve alsubde Keycloak - Test de integración: token de Keycloak → query a Supabase con RLS activo → respuesta correcta
1.3 Edge Functions
invite-member: genera token único, inserta invitación, envía email vía Resendaccept-invitation: valida token + expiración, une al usuario al colectivo, invalida el token
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
keycloak-jsen SvelteKit:- Inicialización en
+layout.tsconcheckLoginIframe: false(compatibilidad PWA/Safari) - Store Svelte
auth— exponeuser,token,isAuthenticated,login(),logout() login()redirige al login page de Keycloak; el callback vuelve a la app con el token- Refresco silencioso del token antes de expiración (
updateToken(60))
- Inicialización en
- Rutas protegidas: hook
+layout.server.tsque verifica sesión y redirige si no hay token válido - Flujo de onboarding post-primer-login:
- Detectar si el usuario no tiene colectivo → redirigir a
/onboarding - Opción A: Crear nuevo colectivo (nombre + emoji)
- Opción B: Introducir link de invitación
- Detectar si el usuario no tiene colectivo → redirigir a
- Pantalla
/collective/manage— miembros, roles, botón invitar, expulsar - Pantalla
/invitation/[token]— aceptar invitación (el usuario debe estar autenticado en Keycloak) - Selector de colectivo activo en la barra de navegación principal
- Store Svelte
collective— colectivo activo, lista de miembros, rol del usuario actual - 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 ~40 emoji curados, tap para seleccionar
- Upload — input de fichero; preview antes de subir; subida a Supabase Storage bucket
avatars; soporte para recortar en proporción 1:1
- 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 (letra sobre fondo slate), emoji, o<img>con fallback a initials si la URL falla
Nota sobre registro de nuevos usuarios: Con Keycloak, el registro lo gestiona Keycloak (su propia pantalla o self-registration habilitado en el realm). No hay pantalla de registro en SvelteKit. Si un usuario llega desde un link de invitación sin cuenta, Keycloak gestiona el registro y luego redirige de vuelta al callback de invitación.
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.