- Cliente Supabase singleton en src/lib/supabase.ts - Interfaces AuthUser / AuthService en src/auth/types.ts - SupabaseAuthService: signInWithGoogle, signOut, getCurrentUser, onAuthStateChange con upsert a tabla users (platform_admin para @inquality.com.py, guest resto) - AuthProvider (React Context) wrappea toda la app; expone user, loading, signIn, signOut - LoginPage /login: logo InQuality color, título, fade-in, botón Google naranja #F59845 - RootComponent del router guarda todas las rutas — sin sesión → /login, con sesión + /login → / - Script SQL: supabase/migrations/001_create_users.sql (tabla users + RLS) - AppHeader: <Link> → <a href="/"> para evitar dependencia de RouterContext en tests - 561/561 tests verdes, build sin errores TS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
19 KiB
Sprint 4 — BRIEF
Naturaleza del sprint: refactorización arquitectural mayor. No hay features de producto nuevas visibles. El objetivo es habilitar colaboración multi-usuario sobre una base correcta. Este sprint es una excepción justificada al rebalanceo web/PDF/infra — es 100% infraestructura.
Contexto y motivación
InQ ROI nació como herramienta single-user con persistencia local (IndexedDB/Dexie). El equipo de InQuality lo usa actualmente de forma individual y quiere pasar a un modelo colaborativo donde:
- Múltiples consultores pueden trabajar sobre procesos compartidos
- Existe identidad de usuario (quién creó, quién actualizó)
- Algunos elementos son privados del consultor, otros compartidos con el equipo
- Los recursos son un catálogo global del equipo, no duplicados por proceso
Para esto se requiere reemplazar IndexedDB con una base de datos centralizada, agregar autenticación, y rediseñar el modelo de datos de recursos.
Dato importante: no hay datos en producción que valgan la pena migrar. Se abandona IndexedDB limpiamente y se empieza de cero en la nueva base de datos.
Decisiones estratégicas — NO reabrir en esta sprint
Auth: Supabase ahora, Keycloak después
Se usa Supabase como BaaS para Sprint 4: auth + PostgreSQL + RLS. La razón es velocidad de implementación.
El código debe construir una interfaz AuthService que abstraiga el proveedor de autenticación. El objetivo es que la migración futura a Keycloak (servidor de identidad InQuality para múltiples productos) sea cambio de implementación, no reescritura de la app.
InQuality tiene infraestructura VPS (desa + prod) y la visión es Keycloak self-hosted como plataforma de identidad para todos sus productos. Eso es trabajo de un sprint futuro.
Modelo de plataforma: InQuality como operador (Modelo A)
InQ ROI es una plataforma de consultoría operada por InQuality, no un SaaS peer-to-peer. InQuality controla quién entra, a qué tiene acceso, y qué puede hacer. Los clientes y consultores externos son guests invitados por InQuality.
Esto se implementa con un campo platform_role en la tabla de usuarios:
platform_admin→ staff InQuality, puede ver todos los procesos de todos los usuarios/gruposmember→ usuario estándar (consultor InQuality sin acceso global, o reservado para futuro)guest→ cliente externo o consultor externo, solo ve lo que se le comparte explícitamente
En Sprint 4, todos los usuarios de InQuality reciben platform_admin. El rol guest se usa cuando se agregue el primer cliente externo.
Migración a Modelo B (SaaS peer-to-peer) — posible, no planificada.
Cuando InQuality decida que cada organización cliente debe ser autónoma (administrar sus propios usuarios, sin acceso implícito de InQuality), la migración consiste en: eliminar el bypass de platform_admin, agregar self-service de org, y cambiar las políticas RLS. El schema de Sprint 4 ya lo soporta sin reestructurar datos.
Auth: providers múltiples según rol
- Staff InQuality (
platform_admin): Google Workspace@inquality.com.py. Solo este dominio puede obtenerplatform_admin. - Guests (clientes, consultores externos): cualquier email. Magic link o email+contraseña. No requieren Google Workspace.
Supabase soporta múltiples providers simultáneamente. En Sprint 4 solo se implementa Google OAuth para staff. La infraestructura para guest auth (magic link) se deja preparada pero no activada hasta que haya un cliente real.
Online-only
Se elimina el requerimiento de offline. La herramienta requiere conectividad. Offline + sync se agrega al roadmap futuro.
Sin auditoría en Sprint 4
La auditoría de cambios (quién cambió qué campo y cuándo) se difiere a un sprint dedicado. En Sprint 4 solo se captura owner_id, created_by, updated_by, created_at, updated_at en las tablas principales — suficiente para mostrar identidad en la UI sin el costo de instrumentar un audit log completo.
Modelo de datos objetivo
Entidades centrales y sus relaciones
El modelo tiene tres entidades de identidad/acceso y las entidades de negocio del producto. La separación es importante: client en un proceso es un atributo de texto, no una entidad con acceso al sistema.
Usuarios (identidad)
↓ pertenecen a
Grupos de usuario (agrupación de acceso)
↓ se usan en
Acceso a procesos (tabla de permisos)
↓ determina quién ve
Procesos → Actividades → Simulaciones (entidades de negocio)
↑
Recursos (catálogo global, sin process_id)
Usuarios
La tabla de usuarios combina Supabase Auth (fuente de verdad para credenciales) con una tabla users en la DB de la app para atributos de la plataforma.
users
id uuid PK (mismo UUID que auth.users de Supabase)
email text NOT NULL
name text
avatar_url text
platform_role 'platform_admin' | 'member' | 'guest'
created_at timestamptz DEFAULT now()
Reglas de platform_role:
platform_admin→ staff InQuality. Bypass de todos los controles de acceso: ve y puede editar todos los procesos. Solo usuarios con dominio@inquality.com.pyobtienen este rol.member→ usuario estándar. Ve solo lo que se le comparte explícitamente. Reservado para consultores InQuality sin acceso global o para uso futuro.guest→ cliente externo o consultor externo. Idéntico amemberen permisos. Se usa para distinguir visualmente en la UI quién es externo.
En Sprint 4, todos los usuarios de InQuality reciben platform_admin. El rol guest entra cuando llegue el primer cliente externo.
El campo platform_role nunca hardcodea "inquality". Si en el futuro InQuality decide operar el Modelo B (cada org autónoma), el bypass se elimina cambiando las políticas RLS — el campo permanece genérico.
Grupos de usuario
Permiten compartir un proceso con un conjunto de personas sin listar a cada una individualmente. Útil para "todos los del cliente Acme" o "equipo de consultoría junior".
user_groups
id uuid PK
name text NOT NULL
created_by uuid → users.id
created_at timestamptz DEFAULT now()
group_memberships
user_id uuid → users.id
group_id uuid → user_groups.id
role 'owner' | 'member'
PRIMARY KEY (user_id, group_id)
Acceso a procesos
Reemplaza el campo visibility: 'private' | 'team' del modelo anterior. El acceso es explícito: el owner de un proceso elige con quién compartirlo (usuarios individuales o grupos).
process_access
process_id uuid → processes.id
grantee_type 'user' | 'group'
grantee_id uuid (→ users.id si 'user', → user_groups.id si 'group')
permission 'view' | 'edit'
PRIMARY KEY (process_id, grantee_type, grantee_id)
Un proceso sin entradas en process_access es privado del owner. Un proceso compartido con el grupo "Equipo InQuality" es efectivamente de visibilidad de equipo. La tabla process_access es suficientemente flexible para Modelo A y Modelo B sin reestructurar.
Procesos
processes
id uuid PK
name text
client text ← atributo descriptivo, NO entidad con acceso
currency text
frequency numeric
bpmn_xml text
owner_id uuid → users.id
created_by uuid → users.id
updated_by uuid → users.id
created_at timestamptz
updated_at timestamptz
Sin campo visibility. El acceso se determina completamente por process_access y por platform_role del usuario.
Recursos — catálogo global
Este es el cambio más significativo de modelo. Los recursos dejan de ser por proceso y pasan a ser un catálogo global del equipo.
resources
id uuid PK
name text
type 'human' | 'system' | 'tool'
cost_per_hour numeric
created_by uuid → users.id
updated_by uuid → users.id
created_at timestamptz
updated_at timestamptz
Sin process_id. Sin control de acceso propio — los recursos son siempre visibles para todos los usuarios autenticados. Cualquier consultor puede crear, ver y editar recursos del catálogo.
Las asignaciones de recursos a actividades (activity_resource_assignments) referencian el resource_id del catálogo global.
Actividades
Sin cambio de modelo respecto a Sprint 3. Se agrega created_by, updated_by, created_at, updated_at.
Simulaciones
No tienen control de acceso propio — la regla de acceso se deriva del proceso al que pertenecen.
Grupos de procesos y tags
Compartidos por todo el equipo. Sin campo de acceso. Sin owner_id.
Interfaz AuthService — contrato obligatorio
Claude Code debe definir y respetar esta interfaz. No llamar a Supabase directamente desde componentes o stores.
interface AuthUser {
id: string
email: string
name: string
avatarUrl?: string
}
interface AuthService {
signInWithGoogle(): Promise<void>
signOut(): Promise<void>
getCurrentUser(): Promise<AuthUser | null>
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void
}
La implementación SupabaseAuthService implementa esta interfaz y es la única que toca Supabase directamente. Cuando llegue Keycloak, se escribe KeycloakAuthService que implementa la misma interfaz. El resto de la app no cambia.
Capa de repositorios — contrato obligatorio
Actualmente existe src/persistence/repositories.ts con Dexie. Se reemplaza por implementaciones Supabase manteniendo el mismo patrón de repositorios.
// El contrato (interface) no cambia, la implementación sí
interface ProcessRepository {
getAll(): Promise<Process[]>
getById(id: string): Promise<Process | undefined>
save(process: Process): Promise<void>
delete(id: string): Promise<void>
// ...
}
Los stores de Zustand llaman al repositorio. Los repositorios llaman a Supabase. Ningún componente toca Supabase directamente.
Row-Level Security (RLS)
Las políticas de RLS de Supabase deben ser equivalentes a políticas PostgreSQL estándar. No usar features exclusivas de Supabase que no puedan recrearse en un PostgreSQL standalone cuando se migre a VPS.
Helper function (crear una vez)
-- Retorna true si el usuario actual es platform_admin
CREATE OR REPLACE FUNCTION is_platform_admin()
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid() AND platform_role = 'platform_admin'
);
$$ LANGUAGE sql SECURITY DEFINER;
Políticas para processes
-- Ver: platform_admin ve todo | owner ve los suyos | acceso explícito
CREATE POLICY "select_processes" ON processes FOR SELECT
USING (
is_platform_admin()
OR owner_id = auth.uid()
OR EXISTS (
SELECT 1 FROM process_access
WHERE process_id = processes.id
AND (
(grantee_type = 'user' AND grantee_id = auth.uid())
OR
(grantee_type = 'group' AND grantee_id IN (
SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
))
)
)
);
-- Insertar: cualquier usuario autenticado
CREATE POLICY "insert_processes" ON processes FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
-- Actualizar: platform_admin | owner | quien tenga permiso 'edit'
CREATE POLICY "update_processes" ON processes FOR UPDATE
USING (
is_platform_admin()
OR owner_id = auth.uid()
OR EXISTS (
SELECT 1 FROM process_access
WHERE process_id = processes.id
AND permission = 'edit'
AND (
(grantee_type = 'user' AND grantee_id = auth.uid())
OR
(grantee_type = 'group' AND grantee_id IN (
SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
))
)
)
);
-- Eliminar: solo el owner (platform_admin puede, por ser owner o por override)
CREATE POLICY "delete_processes" ON processes FOR DELETE
USING (is_platform_admin() OR owner_id = auth.uid());
Políticas para resources (catálogo global)
-- Todos los autenticados pueden leer y escribir
CREATE POLICY "all_resources" ON resources
USING (auth.uid() IS NOT NULL)
WITH CHECK (auth.uid() IS NOT NULL);
Políticas para users, user_groups, group_memberships, process_access
-- users: cualquier autenticado puede leer (para mostrar nombres), solo el propio usuario puede actualizar
CREATE POLICY "read_users" ON users FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "update_own_user" ON users FOR UPDATE USING (id = auth.uid());
-- user_groups: todos los autenticados leen; platform_admin o owner del grupo puede modificar
CREATE POLICY "read_groups" ON user_groups FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_groups" ON user_groups FOR ALL
USING (is_platform_admin() OR created_by = auth.uid());
-- group_memberships: todos los autenticados leen; platform_admin o owner del grupo gestiona
CREATE POLICY "read_memberships" ON group_memberships FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_memberships" ON group_memberships FOR ALL
USING (
is_platform_admin()
OR EXISTS (
SELECT 1 FROM user_groups
WHERE id = group_memberships.group_id AND created_by = auth.uid()
)
);
-- process_access: owner del proceso o platform_admin gestiona los accesos
CREATE POLICY "manage_process_access" ON process_access FOR ALL
USING (
is_platform_admin()
OR EXISTS (
SELECT 1 FROM processes WHERE id = process_access.process_id AND owner_id = auth.uid()
)
);
CREATE POLICY "read_process_access" ON process_access FOR SELECT
USING (auth.uid() IS NOT NULL);
Nota sobre migración a Modelo B
Para migrar de Modelo A a Modelo B (orgs autónomas), los únicos cambios son:
- Eliminar o condicionar el bypass de
is_platform_admin()en las políticas - Agregar tabla
organizationsyorg_memberships - Ajustar las políticas para que el scope sea
org_iden lugar de plataforma global
El schema de datos no cambia. Las tablas user_groups, group_memberships y process_access son compatibles con ambos modelos.
UX — cambios de superficie
Login
Pantalla de login antes de cualquier contenido. Dos flujos:
Staff InQuality → Botón "Iniciar sesión con Google". Solo cuentas @inquality.com.py obtienen acceso. Al autenticarse, la app verifica el dominio y asigna platform_role = 'platform_admin' si no existe registro previo en la tabla users.
Guests (clientes, consultores externos) → En Sprint 4 este flujo no está activado. La infraestructura (campo platform_role = 'guest', tabla users) se construye lista para habilitarlo. El botón de magic link / email no aparece en la UI de Sprint 4. Se activa cuando haya un cliente real que incorporar.
Al completar login, la app redirige al workspace. Sin login, cualquier ruta protegida redirige a /login.
Biblioteca (LibraryPage)
- Toggle de filtro: "Todos" / "Míos"
- Badge o ícono que indica visibilidad (privado 🔒 / compartido)
- En cada tarjeta de proceso: "Actualizado por [nombre]" + timestamp relativo
- Botón para cambiar visibilidad de un proceso propio
ResourcesPanel (en workspace)
El panel de recursos ya no es "crear recursos para este proceso". Pasa a ser "seleccionar desde catálogo global" + "agregar nuevo al catálogo". El catálogo vive en una nueva sección de la app (Settings o panel dedicado).
AppHeader
Mostrar avatar / nombre del usuario logueado. Botón de logout.
Stack — cambios
| Agregado | Propósito |
|---|---|
@supabase/supabase-js |
Cliente Supabase (auth + db) |
Variables de entorno VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY |
Configuración por entorno |
| Eliminado | Motivo |
|---|---|
dexie |
Reemplazado por Supabase PostgreSQL |
src/persistence/db.ts |
Reemplazado por cliente Supabase |
| Migraciones Dexie (v1-v4) | Ya no aplican |
Las variables de entorno deben existir en .env.local (ignorado por git) y documentarse en .env.example.
Etapas tentativas
El orden importa — cada etapa habilita la siguiente.
| Etapa | Descripción |
|---|---|
| 1 | Supabase setup + AuthService + login page + Google OAuth |
| 2 | Schema PostgreSQL + migración de repositorios (procesos + grupos + tags) |
| 3 | Migración de actividades + recursos globales (nuevo modelo) |
| 4 | Migración de simulaciones + limpieza de Dexie |
| 5 | Modelo de privacidad: RLS + UI de visibilidad en biblioteca |
| 6 | Identidad en UI: header con usuario + "actualizado por" en biblioteca y workspace |
| 7 | ResourcesPanel: UX de catálogo global |
| 8 | Tests de integración + polish + validación E2E de auth |
Cada etapa tiene su propio ETAPA_N_PROMPT.md antes de arrancar. No improvisar orden ni scope entre etapas.
Criterios de aceptación del sprint
- Login con Google Workspace (
@inquality.com.py) funciona en desa y prod - Ruta protegida — sin login no se accede a nada de la app
- Procesos se guardan y recuperan de Supabase (no de IndexedDB)
- Control de acceso funciona — un proceso sin
process_accessno lo ve otro usuario (salvoplatform_admin) - Recursos son globales — visibles para todos los usuarios autenticados
- En la biblioteca aparece "Actualizado por [nombre]" en cada proceso
AuthServicees una interface con implementaciónSupabaseAuthService- No hay llamadas directas a Supabase fuera de repositorios y
AuthService - Dexie eliminado del proyecto (no solo sin uso — removido del package.json)
- Build limpio sin errores TypeScript
- Tests de dominio previos siguen verdes (simulación, ROI, PDF)
- Validación visual del director en cada etapa con output visual
Riesgos y mitigaciones
Google Workspace OAuth requiere configuración en Google Cloud Console.
Acción: crear el proyecto OAuth antes de que Claude Code escriba una línea de código. El client_id y client_secret tienen que estar disponibles. Se configura también en Supabase Dashboard → Auth → Providers.
El cambio de recursos globales rompe el UX actual completamente. Acción: el prompt de Etapa 7 (ResourcesPanel) debe incluir un mockup del nuevo UX aprobado antes de implementar. Aplicar Patrón C.5 (mockup como contrato visual).
Múltiples etapas tienen riesgo de regresión en simulación/PDF.
Acción: correr npm run test al final de cada etapa y verificar que los 561 tests previos siguen verdes. Si baja el count, parar y reportar antes de continuar.
Fuera del scope de Sprint 4 — explícito
- Auditoría de cambios (audit log) — sprint dedicado futuro
- Keycloak / migración de auth provider — sprint futuro
- Offline mode / sync — roadmap
- Acceso de clientes externos — Fase 2
- Versionado de procesos — Fase 2
- Export a Excel — BACKLOG sin sprint
- Cualquier feature de PDF o simulación nueva
Referencias
| Recurso | Propósito |
|---|---|
simulador-web/CLAUDE.md |
Reglas de implementación para Claude Code |
simulador-web/src/persistence/ |
Código Dexie actual a reemplazar |
simulador-web/src/store/ |
Stores Zustand que llaman a repositorios |
simulador-web/src/domain/types.ts |
Tipos de dominio (base del schema PostgreSQL) |
methodology/PATTERNS.md |
C.5 (mockup como contrato), C.7 (git diff preventivo) |
methodology/ANTI_PATTERNS.md |
AP.8 (adelanto silencioso) |
| Supabase docs — RLS | https://supabase.com/docs/guides/auth/row-level-security |
| Supabase docs — Google OAuth | https://supabase.com/docs/guides/auth/social-login/auth-google |