Planificador de Features CULTIVA Platform Sprint 1–2 14 pasos · 4 fases

Plan de Implementación: Sistema de Notificaciones en Tiempo Real

Panel /admin de CULTIVA Platform — Supabase Realtime + Next.js 14 App Router

📅 Generado: 18 Jun 2026
👨‍💻 Equipo: 1 Developer
Duración estimada: 4 semanas (2 sprints)
Ready to code: Lunes siguiente
🔍
Resumen ejecutivo

Implementar un centro de notificaciones en tiempo real en el panel /admin de CULTIVA Platform usando Supabase Realtime (canal nativo, sin dependencias externas). Los eventos de negocio clave — registro de cliente, compra de skill, error de agente e inactividad — se persistirán en una nueva tabla notifications y se renderizarán en un panel deslizante (bell icon + badge) integrado en el layout del admin. Cada notificación es persistente, con estado leído/no leído recuperable tras recarga. Entregable en 4 fases independientes, cada una mergeable por separado, con una cobertura de tests del 80 %+.

Requisitos
  • 4 tipos de eventos: nuevo cliente, skill comprada (webhook LS), error de agente, cliente inactivo +7 días.
  • Bell icon en header del admin con badge de contador de no leídas.
  • Panel deslizante con lista, marcar como leída, filtrar por tipo.
  • Persistencia en Supabase: tabla notifications con estado read/unread.
  • No romper rutas ni layout existentes en src/app/admin/.
  • Sin librerías de terceros para el canal real-time (solo Supabase Realtime nativo).
  • Máximo 2 sprints × 2 semanas para 1 developer.
🏗
Cambios de arquitectura
Tipo Archivo / Recurso Descripción
Nuevo supabase/migrations/005_notifications.sql Tabla notifications con RLS, índices y políticas por rol admin.
Nuevo src/lib/notifications/types.ts Tipos TypeScript: NotificationType, Notification, NotificationPayload.
Nuevo src/lib/notifications/service.ts CRUD de notificaciones: create, markRead, markAllRead, getUnreadCount.
Nuevo src/hooks/useNotifications.ts Hook React: suscripción Supabase Realtime, estado local, hydration SSR.
Nuevo src/components/admin/NotificationBell.tsx Icono campana + badge animado de contador de no leídas.
Nuevo src/components/admin/NotificationPanel.tsx Panel deslizante (slide-over): lista, filtros por tipo, marcar leída/todas.
Nuevo src/app/api/notifications/route.ts API Route: GET (list/paginar), PATCH (marcar leída), POST (crear interna).
Nuevo src/app/api/webhooks/lemon-squeezy/route.ts Handler webhook LS: valida firma, crea notificación skill_purchased.
Nuevo src/app/api/cron/inactive-clients/route.ts Cron diario (Vercel Cron): detecta clientes +7 días inactivos y emite notificaciones.
Modifica src/app/admin/layout.tsx Inyectar NotificationBell + NotificationPanel en el header del admin.
Extiende vercel.json Añadir cron job 0 9 * * * apuntando a /api/cron/inactive-clients.
Pasos de implementación
1
Capa de datos — Base de la feature Sprint 1 — Días 1-3
1
Migración SQL: tabla notifications Riesgo bajo
Archivo supabase/migrations/005_notifications.sql
Acción CREATE TABLE notifications (id uuid PK, type text, title text, body text, metadata jsonb, read boolean DEFAULT false, created_at timestamptz). Añadir índice en (read, created_at). Políticas RLS: SELECT/UPDATE solo para rol service_role y admins autenticados.
Por qué Persistencia server-side; nunca confiar en el cliente para el estado leído/no leído.
Dependencias Ninguna — primer paso del plan.
2
Tipos TypeScript de notificación Riesgo bajo
Archivo src/lib/notifications/types.ts
Acción Definir enum NotificationType { NEW_CLIENT, SKILL_PURCHASED, AGENT_ERROR, CLIENT_INACTIVE }. Interfaz Notification con todos los campos. Tipo NotificationPayload para creación.
Por qué Contrato de tipos compartido entre API, hooks y componentes. Evita strings mágicos.
Dependencias Paso 1 (guía el schema).
3
Servicio CRUD de notificaciones Riesgo bajo
Archivo src/lib/notifications/service.ts
Acción Funciones puras con cliente Supabase server-side: createNotification(payload), markAsRead(id), markAllAsRead(), getNotifications({type?, limit?}), getUnreadCount(). Todas con manejo de errores tipado.
Por qué Centralizar acceso a datos; facilita tests unitarios sin mockear la UI.
Dependencias Pasos 1–2.
2
API Routes y eventos de negocio Sprint 1 — Días 4-7
4
API Route: GET/PATCH/POST notifications Riesgo medio
Archivo src/app/api/notifications/route.ts
Acción GET: lista paginada (?type=&limit=20&offset=0). PATCH: {id, read: true}. POST: protegido con INTERNAL_API_KEY para creación interna desde otros servicios. Validar autenticación admin en todos los métodos.
Por qué Interfaz HTTP limpia usada por el hook, el webhook LS y el cron.
Riesgo Verificar que la ruta admin-only no filtre notificaciones a usuarios no autorizados.
5
Webhook Lemon Squeezy: skill_purchased Riesgo alto
Archivo src/app/api/webhooks/lemon-squeezy/route.ts
Acción Validar firma HMAC-SHA256 con LEMON_SQUEEZY_WEBHOOK_SECRET. Para evento order_created: crear notificación tipo SKILL_PURCHASED con metadata {skillName, customerEmail, orderId, amount}. Retornar 200 rápido; procesar async.
Por qué Sin verificación de firma cualquiera puede falsificar compras.
Riesgo CRÍTICO: idempotencia — guardar event_id en DB para evitar duplicados si LS reintenta.
6
Endpoint interno: new_client y agent_error Riesgo medio
Archivo src/app/api/notifications/route.ts (POST)
Acción Extender la ruta POST existente para aceptar payloads de NEW_CLIENT (invocado desde el hook onAuthStateChange de Supabase o desde el signup handler) y AGENT_ERROR (invocado por el agente vía API con INTERNAL_API_KEY).
Por qué Reutiliza la ruta existente; evita proliferación de endpoints.
Dependencias Paso 4.
7
Cron Vercel: detección de clientes inactivos Riesgo medio
Archivo src/app/api/cron/inactive-clients/route.ts
Acción SELECT users WHERE last_activity < now() - interval '7 days' AND active = true. Por cada usuario: crear notificación CLIENT_INACTIVE si no existe ya una no leída del mismo tipo. Proteger ruta con CRON_SECRET (Vercel header).
Por qué Consulta batch diaria más barata que triggers en tiempo real para inactividad.
Riesgo Evitar spam: chequear que ya no hay notificación del mismo cliente en las últimas 24h.
3
Frontend: Hook + Componentes UI Sprint 2 — Días 8-12
8
Hook useNotifications con Supabase Realtime Riesgo medio
Archivo src/hooks/useNotifications.ts
Acción useEffect: suscribir al canal Supabase Realtime 'notifications' (INSERT). Estado local: notifications[], unreadCount. Métodos: markRead(id), markAllRead(). Hydrate inicial desde GET /api/notifications. Cleanup de canal en unmount.
Por qué Separar lógica de negocio del componente permite testear el hook de forma aislada.
Riesgo Manejar reconexión del canal si la suscripción cae (onError + retry con backoff).
9
Componente NotificationBell Riesgo bajo
Archivo src/components/admin/NotificationBell.tsx
Acción Icono campana SVG + badge rojo con unreadCount (ocultar si 0). Animación shake cuando llega notificación nueva. Click: toggle del panel. Aria-label accesible.
Por qué Punto de entrada visual único para el usuario admin.
Dependencias Paso 8 (useNotifications).
10
Componente NotificationPanel (slide-over) Riesgo medio
Archivo src/components/admin/NotificationPanel.tsx
Acción Panel deslizante derecha con Tailwind transition. Filtros por tipo (tabs). Lista de notificaciones con icono por tipo, timestamp relativo, estado leído/no leído. Botón "marcar todas como leídas". Click en item: markRead(id) + opcional deep link.
Por qué UX inline evita navegación; el admin puede revisar sin perder contexto.
Riesgo Estado vacío explícito (no notificaciones), estado de carga y manejo de error de fetch.
11
Integrar en AdminLayout sin romper rutas Riesgo medio
Archivo src/app/admin/layout.tsx
Acción Envolver el layout con NotificationsProvider (Context). Añadir NotificationBell en el header existente (al lado del avatar de usuario). Incluir NotificationPanel fuera del flujo principal (portal o position:fixed). NO tocar rutas ni breadcrumbs.
Por qué Inyección mínima; el layout existente no se refactoriza.
Riesgo Verificar que el Context Provider no re-renderiza rutas hijas innecesariamente (memoizar).
4
Pulido, tests y despliegue Sprint 2 — Días 13-14
12
Tests unitarios del servicio y API Riesgo bajo
Archivo src/lib/notifications/__tests__/service.test.ts
Acción Mock del cliente Supabase. Tests: createNotification persiste correctamente, markAsRead actualiza solo el registro correcto, getUnreadCount retorna 0 cuando todas son leídas. Test del webhook: firma inválida retorna 401.
Dependencias Pasos 2–5.
13
Tests del hook useNotifications Riesgo bajo
Archivo src/hooks/__tests__/useNotifications.test.ts
Acción Usar @testing-library/react-hooks. Mock del canal Supabase Realtime. Verificar: unreadCount se incrementa al recibir INSERT, markRead actualiza estado local optimistamente, cleanup del canal al desmontar.
14
Configurar vercel.json y variables de entorno Riesgo bajo
Archivo vercel.json + Vercel Dashboard
Acción Añadir cron: {"path": "/api/cron/inactive-clients", "schedule": "0 9 * * *"}. Variables nuevas: LEMON_SQUEEZY_WEBHOOK_SECRET, CRON_SECRET, INTERNAL_API_KEY. Documentar en .env.example.
Por qué Sin el cron configurado la detección de inactivos no se ejecuta automáticamente.
🧪
Estrategia de tests
Unitarios
  • service.createNotification
  • service.markAsRead
  • service.getUnreadCount
  • Webhook HMAC validation
  • Cron deduplicación
Integración
  • Hook: Realtime INSERT
  • API GET paginación
  • API PATCH marcar leída
  • Webhook LS end-to-end
  • Cron → DB inactive
E2E (Playwright)
  • Bell badge aparece en admin
  • Panel se abre al click
  • Filtro por tipo funciona
  • Marcar todas leídas → badge 0
  • Persiste tras recarga
Riesgos y mitigaciones
Webhook Lemon Squeezy sin verificación de firma
Mitigación: Verificar HMAC-SHA256 con rawBody antes de cualquier procesamiento. Si falla → 401 inmediato. Tests con firma inválida obligatorios.
Duplicados en webhook (LS reintenta en timeout)
Mitigación: Guardar lemon_squeezy_event_id en la tabla notifications. CHECK UNIQUE antes de insertar.
Supabase Realtime se desconecta sin reconectar
Mitigación: Implementar onError + reconnect con exponential backoff en useNotifications. Fallback: polling cada 30s si canal muerto.
Context Provider re-renderiza todo el admin
Mitigación: Separar Context en NotificationsStateContext + NotificationsActionsContext. Memoizar con useMemo/useCallback.
Cron genera spam de notificaciones inactivas
Mitigación: Antes de crear, consultar si ya existe una notificación CLIENT_INACTIVE no leída para el mismo usuario en las últimas 24h.
🏆
Criterios de éxito
  • Bell badge muestra el contador correcto de no leídas al entrar al admin.
  • Una compra en Lemon Squeezy genera una notificación visible en <3 segundos.
  • Las notificaciones persisten y se recuperan correctamente al recargar la página.
  • Filtros por tipo (NEW_CLIENT / SKILL_PURCHASED / AGENT_ERROR / CLIENT_INACTIVE) funcionan sin recarga.
  • El cron diario detecta clientes inactivos sin generar duplicados.
  • Las rutas existentes en /admin no se ven afectadas (smoke test de todas las páginas).
  • Cobertura de tests ≥ 80 % en service.ts y useNotifications.ts.
  • Un developer puede empezar a codificar el lunes sin reuniones adicionales.