- JWT strategy: Option B (GoTrue as OIDC proxy, not direct Keycloak JWT) - Self-registration enabled in Keycloak realm - Multiple collectives per user with sidebar switcher - Invitations are link-only (no email/Resend integration) - Avatar upload uses in-browser crop (cropperjs) at 1:1 ratio - Removed Edge Functions from plan; invitation logic handled in SvelteKit server actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.2 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 (
supabase/config.toml) - Flujo: app obtiene token de Keycloak → lo intercambia por token de GoTrue → usa token de GoTrue para Supabase
- Verificar que
auth.uid()en las políticas RLS resuelve correctamente - Test de integración: token de Keycloak → exchange con GoTrue → query a Supabase con RLS → respuesta correcta
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 en un server action de SvelteKit (no Edge Function): validar token + expiración, insertar en
collective_members, marcaraccepted_at
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 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 ~40 emoji curados, tap para seleccionar
- Upload — input de fichero; recorte en el navegador en proporción 1:1 (usar
cropperjso similar); 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 (letra sobre fondo slate), emoji, o<img>con fallback a initials si la URL falla
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.