Record Fase 1 design decisions before implementation
- 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>
This commit is contained in:
12
CLAUDE.md
12
CLAUDE.md
@@ -97,13 +97,17 @@ The central organizing unit is the **Collective** (household group), not the ind
|
|||||||
- Trash retains deleted items/notes for 7 days before permanent deletion.
|
- Trash retains deleted items/notes for 7 days before permanent deletion.
|
||||||
- If the sole admin deletes their account, the system auto-promotes the oldest member before allowing deletion.
|
- If the sole admin deletes their account, the system auto-promotes the oldest member before allowing deletion.
|
||||||
|
|
||||||
## Auth Architecture (Keycloak → Supabase)
|
## Auth Architecture (Keycloak → GoTrue → Supabase)
|
||||||
|
|
||||||
Auth flows through Keycloak as the OIDC IdP. Supabase validates Keycloak-issued JWTs. Supabase RLS uses `auth.uid()` which reads the `sub` claim from the JWT (must be a UUID v4 — Keycloak default).
|
**Strategy: Option B — GoTrue as OIDC proxy.**
|
||||||
|
The app authenticates with Keycloak (PKCE), then exchanges the Keycloak token with GoTrue for a Supabase JWT. Supabase RLS uses `auth.uid()` which resolves to the GoTrue user UUID (mapped from Keycloak `sub`). GoTrue maintains `auth.users`.
|
||||||
|
|
||||||
The Keycloak RS256 public key must be set as `JWT_SECRET` in Supabase. **Rotating Keycloak signing keys requires updating Supabase `JWT_SECRET` simultaneously.**
|
Key decisions:
|
||||||
|
- **Self-registration enabled** in Keycloak — users can create accounts on the Keycloak login screen.
|
||||||
|
- **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 locally.
|
Logout must invalidate both sides: call `keycloak.logout()` (invalidates Keycloak session) AND clear the Supabase client session locally.
|
||||||
|
|
||||||
## Sync Strategy
|
## Sync Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"displayName": "Colectivo",
|
"displayName": "Colectivo",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sslRequired": "external",
|
"sslRequired": "external",
|
||||||
"registrationAllowed": false,
|
"registrationAllowed": true,
|
||||||
"loginWithEmailAllowed": true,
|
"loginWithEmailAllowed": true,
|
||||||
"duplicateEmailsAllowed": false,
|
"duplicateEmailsAllowed": false,
|
||||||
"resetPasswordAllowed": true,
|
"resetPasswordAllowed": true,
|
||||||
|
|||||||
@@ -25,28 +25,24 @@
|
|||||||
|
|
||||||
#### 1.2 Integración Keycloak → Supabase
|
#### 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:
|
> **✅ 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 al `sub` de Keycloak).
|
||||||
|
> Esto mantiene `auth.users` de Supabase poblado y permite usar todas las helpers de GoTrue.
|
||||||
|
|
||||||
**Opción A — JWT de Keycloak directo en Supabase (recomendada para self-hosted):**
|
- [ ] Configurar Keycloak como OIDC Identity Provider en GoTrue (`supabase/config.toml`)
|
||||||
- Configurar `JWT_SECRET` de Supabase con la clave pública de Keycloak (RS256)
|
- [ ] Flujo: app obtiene token de Keycloak → lo intercambia por token de GoTrue → usa token de GoTrue para Supabase
|
||||||
- Supabase valida el token de Keycloak directamente en RLS via `auth.uid()`
|
- [ ] Verificar que `auth.uid()` en las políticas RLS resuelve correctamente
|
||||||
- Sin intermediarios, latencia mínima
|
- [ ] Test de integración: token de Keycloak → exchange con GoTrue → query a Supabase con RLS → respuesta correcta
|
||||||
- Requiere que el claim `sub` de Keycloak sea el UUID que identifica al usuario en Supabase
|
|
||||||
|
|
||||||
**Opción B — GoTrue como proxy OIDC:**
|
#### 1.3 Invitaciones (sin email)
|
||||||
- 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 `sub` de Keycloak se convierte en el `auth.uid()` de Supabase.
|
> **✅ 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.
|
||||||
|
|
||||||
- [ ] Configurar `supabase/config.toml` con Keycloak como OIDC externo
|
- [ ] `collective_invitations` tiene `token` único (UUID), `expires_at`, `role`, y `accepted_at`
|
||||||
- [ ] Verificar que `auth.uid()` en las políticas RLS resuelve al `sub` de Keycloak
|
- [ ] Pantalla `/collective/manage` genera el link de invitación y muestra un botón "Copy link"
|
||||||
- [ ] Test de integración: token de Keycloak → query a Supabase con RLS activo → respuesta correcta
|
- [ ] `/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`, marcar `accepted_at`
|
||||||
#### 1.3 Edge Functions
|
|
||||||
|
|
||||||
- [ ] `invite-member`: genera token único, inserta invitación, envía email vía Resend
|
|
||||||
- [ ] `accept-invitation`: valida token + expiración, une al usuario al colectivo, invalida el token
|
|
||||||
|
|
||||||
#### 1.4 Internacionalización (i18n)
|
#### 1.4 Internacionalización (i18n)
|
||||||
|
|
||||||
@@ -69,24 +65,24 @@ Librería: **Paraglide JS** (`@inlang/paraglide-sveltekit`). Compile-time, tree-
|
|||||||
- Refresco silencioso del token antes de expiración (`updateToken(60)`)
|
- Refresco silencioso del token antes de expiración (`updateToken(60)`)
|
||||||
- [ ] Rutas protegidas: hook `+layout.server.ts` que verifica sesión y redirige si no hay token válido
|
- [ ] Rutas protegidas: hook `+layout.server.ts` que verifica sesión y redirige si no hay token válido
|
||||||
- [ ] Flujo de onboarding post-primer-login:
|
- [ ] Flujo de onboarding post-primer-login:
|
||||||
- Detectar si el usuario no tiene colectivo → redirigir a `/onboarding`
|
- Detectar si el usuario no pertenece a ningún colectivo → redirigir a `/onboarding`
|
||||||
- Opción A: Crear nuevo colectivo (nombre + emoji)
|
- Opción A: Crear nuevo colectivo (nombre + emoji)
|
||||||
- Opción B: Introducir link de invitación
|
- Opción B: Pegar link de invitación recibido
|
||||||
- [ ] Pantalla `/collective/manage` — miembros, roles, botón invitar, expulsar
|
- [ ] 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 en Keycloak)
|
- [ ] Pantalla `/invitation/[token]` — aceptar invitación (el usuario debe estar autenticado)
|
||||||
- [ ] Selector de colectivo activo en la barra de navegación principal
|
- [ ] **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
|
- [ ] 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:
|
- [ ] Pantalla `/settings` — configuración de usuario:
|
||||||
- Campo display name (input de texto, guardado automático con debounce)
|
- Campo display name (input de texto, guardado automático con debounce)
|
||||||
- Selector de avatar con tres modos:
|
- Selector de avatar con tres modos:
|
||||||
- *Initials* — preview generado en tiempo real desde el display name
|
- *Initials* — preview generado en tiempo real desde el display name
|
||||||
- *Emoji* — grid de ~40 emoji curados, tap para seleccionar
|
- *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
|
- *Upload* — input de fichero; **recorte en el navegador en proporción 1:1** (usar `cropperjs` o similar); subida a Supabase Storage bucket `avatars`
|
||||||
- Selector de idioma (English / Español) con efecto inmediato
|
- Selector de idioma (English / Español) con efecto inmediato
|
||||||
- Enlace externo a Keycloak Account Console para cambio de email/contraseña
|
- Enlace externo a Keycloak Account Console para cambio de email/contraseña
|
||||||
- [ ] Componente `Avatar.svelte` — renderiza según `avatar_type`: initials (letra sobre fondo slate), emoji, o `<img>` con fallback a initials si la URL falla
|
- [ ] Componente `Avatar.svelte` — renderiza según `avatar_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.
|
> **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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user