🚀 Guía: Supabase Realtime en PuntoHack
📖 ¿Qué es Supabase Realtime?
Supabase Realtime es un sistema de sincronización en tiempo real que permite que tu aplicación reciba actualizaciones instantáneas de la base de datos PostgreSQL sin necesidad de hacer polling (consultas repetidas cada X segundos).
🔄 Comparación: Polling vs Realtime
❌ Sistema Actual (Polling)
// Cada 5 segundos hace una petición HTTP
setInterval(() => {
fetch('/api/hackathons/list')
.then(data => {
// Comparar si hay cambios
if (cambios) router.refresh();
});
}, 5000);
Problemas:
- ⚠️ Consume recursos innecesariamente (peticiones constantes)
- ⚠️ Hay delay (hasta 5 segundos para ver cambios)
- ⚠️ No es eficiente (muchas peticiones sin cambios)
- ⚠️ No escala bien con muchos usuarios
✅ Con Supabase Realtime
// Se suscribe a cambios y recibe notificaciones instantáneas
supabase
.channel('hackathons')
.on('postgres_changes', {
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'Hackathon'
}, (payload) => {
// Actualizar UI instantáneamente
updateUI(payload.new);
})
.subscribe();
Ventajas:
- ✅ Instantáneo: Cambios en <100ms
- ✅ Eficiente: Solo recibe datos cuando hay cambios reales
- ✅ Escalable: WebSockets son más eficientes que HTTP polling
- ✅ Menos carga en servidor: No hay peticiones constantes
🎯 Casos de Uso en PuntoHack
1. Lista de Hackathons (Actual)
Situación actual: Polling cada 5 segundos
Con Realtime: Actualización instantánea cuando:
- Se publica un nuevo hackathon
- Cambia el estado de un hackathon (DRAFT → REGISTRATION → RUNNING)
- Se actualiza información del hackathon
2. Leaderboard (Muy útil)
Situación actual: Se actualiza solo al recargar
Con Realtime: Actualización instantánea cuando:
- Un juez envía una evaluación
- Se actualiza un score
- Cambia el ranking
3. Dashboard de Juez (Muy útil)
Situación actual: Polling o recarga manual
Con Realtime: Actualización instantánea cuando:
- Se asigna una nueva submission
- Se completa una evaluación
- Cambia el progreso de evaluación
4. Notificaciones (Ideal)
Situación actual: Polling cada X segundos
Con Realtime: Notificaciones instantáneas cuando:
- Llega una invitación de equipo
- Se aprueba/rechaza una solicitud
- Se evalúa tu submission
5. Submissions en Evaluación (Muy útil)
Situación actual: Recarga manual
Con Realtime: Actualización instantánea cuando:
- Se envía una nueva submission
- Se actualiza una submission
- Se completa una evaluación
🛠️ Cómo Funciona Técnicamente
Arquitectura
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Cliente │◄──WS───►│ Supabase │◄──DB───►│ PostgreSQL │
│ (Next.js) │ │ Realtime │ │ Database │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ │ │
│ 1. Suscripción │ │
│────────────────────────►│ │
│ │ │
│ │ 2. Cambio en DB │
│ │◄────────────────────────│
│ │ │
│ 3. Notificación │ │
│◄────────────────────────│ │
│ │ │
Flujo de Datos
- Cliente se suscribe a cambios en una tabla específica
- PostgreSQL detecta cambio (INSERT/UPDATE/DELETE)
- Supabase Realtime captura el cambio
- WebSocket envía notificación al cliente
- Cliente actualiza UI instantáneamente
📦 Instalación y Configuración
Paso 1: Instalar Supabase Client
pnpm add @supabase/supabase-js
Paso 2: Configurar Variables de Entorno
Agregar a .env.local:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://tu-proyecto.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=tu-anon-key-aqui
Nota: Necesitas obtener estas credenciales de tu proyecto Supabase:
- Ve a tu proyecto en Supabase Dashboard
- Settings → API
- Copia
URLyanon publickey
Paso 3: Crear Cliente de Supabase
Crear src/lib/supabase-client.ts:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables');
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Paso 4: Habilitar Realtime en Supabase
En el Dashboard de Supabase:
- Ve a Database → Replication
- Habilita Realtime para las tablas que necesites:
- ✅
Hackathon(para cambios de estado) - ✅
Submission(para nuevas submissions) - ✅
Score(para actualizaciones de evaluación) - ✅
ChallengeEvaluation(para evaluaciones de challenges) - ✅
Notification(para notificaciones) - ✅
Team(para cambios en equipos)
- ✅
💻 Implementación en Next.js 16
Ejemplo 1: Lista de Hackathons en Tiempo Real
Reemplazar src/app/hackathons/hackathons-list-realtime-wrapper.tsx:
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { supabase } from '@/lib/supabase-client';
interface HackathonsListRealtimeWrapperProps {
children: React.ReactNode;
type?: 'public' | 'organizer';
}
export function HackathonsListRealtimeWrapper({
children,
type = 'public',
}: HackathonsListRealtimeWrapperProps) {
const router = useRouter();
useEffect(() => {
// Crear canal de Realtime
const channel = supabase
.channel('hackathons-list')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'Hackathon',
},
(payload) => {
console.log('Cambio detectado en Hackathon:', payload);
// Actualizar la página cuando hay cambios
router.refresh();
}
)
.subscribe();
// Limpiar suscripción al desmontar
return () => {
supabase.removeChannel(channel);
};
}, [router]);
return <>{children}</>;
}
Ejemplo 2: Leaderboard en Tiempo Real
Crear src/app/hackathons/[slug]/leaderboard/leaderboard-realtime-wrapper.tsx:
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { supabase } from '@/lib/supabase-client';
interface LeaderboardRealtimeWrapperProps {
children: React.ReactNode;
hackathonId: string;
}
export function LeaderboardRealtimeWrapper({
children,
hackathonId,
}: LeaderboardRealtimeWrapperProps) {
const router = useRouter();
useEffect(() => {
// Suscribirse a cambios en Scores (que afectan el leaderboard)
const scoresChannel = supabase
.channel(`leaderboard-${hackathonId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'Score',
filter: `hackathonId=eq.${hackathonId}`, // Solo scores de este hackathon
},
() => {
// Actualizar leaderboard cuando hay cambios en scores
router.refresh();
}
)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'ChallengeEvaluation',
// Filtrar por hackathon (necesitarías un join o filtro adicional)
},
() => {
// Actualizar cuando cambian evaluaciones de challenges
router.refresh();
}
)
.subscribe();
return () => {
supabase.removeChannel(scoresChannel);
};
}, [hackathonId, router]);
return <>{children}</>;
}
Ejemplo 3: Notificaciones en Tiempo Real
Crear src/components/notifications/realtime-notifications.tsx:
'use client';
import { useEffect } from 'react';
import { useUser } from '@clerk/nextjs';
import { supabase } from '@/lib/supabase-client';
import { useToast } from '@/components/toast/toast-provider';
export function RealtimeNotifications() {
const { user } = useUser();
const { showInfo } = useToast();
useEffect(() => {
if (!user?.id) return;
// Obtener profileId del usuario
// (necesitarías hacer una query inicial para obtener el profileId)
const channel = supabase
.channel(`notifications-${user.id}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'Notification',
filter: `profileId=eq.${profileId}`, // Solo notificaciones del usuario
},
(payload) => {
const notification = payload.new;
// Mostrar toast con la nueva notificación
showInfo(notification.message || notification.title);
// Disparar evento para actualizar contador en navbar
window.dispatchEvent(new CustomEvent('notification-updated'));
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user?.id, showInfo]);
return null; // Componente invisible, solo maneja suscripciones
}
Ejemplo 4: Dashboard de Juez en Tiempo Real
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { supabase } from '@/lib/supabase-client';
export function JudgeDashboardRealtime({ judgeId }: { judgeId: string }) {
const router = useRouter();
useEffect(() => {
const channel = supabase
.channel(`judge-${judgeId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'Score',
filter: `judgeId=eq.${judgeId}`, // Solo scores de este juez
},
() => {
router.refresh();
}
)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'ChallengeEvaluation',
filter: `judgeId=eq.${judgeId}`,
},
() => {
router.refresh();
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [judgeId, router]);
return null;
}
🔐 Seguridad y Row Level Security (RLS)
⚠️ IMPORTANTE: Configurar RLS en Supabase
Supabase Realtime respeta las políticas RLS de PostgreSQL. Debes configurar políticas de seguridad:
-- Ejemplo: Usuarios solo pueden ver sus propias notificaciones
CREATE POLICY "Users can view own notifications"
ON "Notification"
FOR SELECT
USING (auth.uid()::text = (SELECT "userId" FROM "Profile" WHERE id = "profileId"));
-- Ejemplo: Jueces solo pueden ver scores de sus evaluaciones
CREATE POLICY "Judges can view own scores"
ON "Score"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Profile" p
WHERE p.id = "judgeId"
AND p."userId" = auth.uid()::text
)
);
Sin RLS configurado, cualquier usuario podría suscribirse a TODOS los cambios de la base de datos. ⚠️
📊 Comparación: Antes vs Después
Antes (Polling)
// ❌ Petición HTTP cada 5 segundos
setInterval(() => {
fetch('/api/hackathons/list')
.then(res => res.json())
.then(data => {
// Comparar y actualizar si hay cambios
});
}, 5000);
// Problemas:
// - 12 peticiones/minuto por usuario
// - Delay de hasta 5 segundos
// - Muchas peticiones sin cambios
Después (Realtime)
// ✅ Suscripción WebSocket (una sola conexión)
supabase
.channel('hackathons')
.on('postgres_changes', { table: 'Hackathon' }, (payload) => {
// Actualización instantánea (<100ms)
updateUI(payload.new);
})
.subscribe();
// Ventajas:
// - 1 conexión WebSocket (más eficiente)
// - Actualización instantánea
// - Solo recibe datos cuando hay cambios reales
🎯 Plan de Implementación
Fase 1: Setup Básico (1 día)
- ✅ Instalar
@supabase/supabase-js - ✅ Configurar variables de entorno
- ✅ Crear cliente de Supabase
- ✅ Habilitar Realtime en tablas clave
Fase 2: Reemplazar Polling (2-3 días)
- ✅ Lista de hackathons
- ✅ Dashboard de organizador
- ✅ Leaderboard
- ✅ Dashboard de juez
Fase 3: Notificaciones Realtime (1 día)
- ✅ Notificaciones instantáneas
- ✅ Actualización de contador en navbar
- ✅ Toasts automáticos
Fase 4: Seguridad (1 día)
- ✅ Configurar RLS policies
- ✅ Validar que solo usuarios autorizados reciben datos
- ✅ Testing de seguridad
⚡ Ventajas para PuntoHack
1. Mejor Experiencia de Usuario
- ✅ Cambios instantáneos (sin recargar)
- ✅ Leaderboard se actualiza en vivo
- ✅ Notificaciones inmediatas
2. Mejor Performance
- ✅ Menos carga en servidor (no hay polling constante)
- ✅ Menos tráfico de red
- ✅ Más escalable
3. Más Profesional
- ✅ Experiencia moderna y fluida
- ✅ Similar a aplicaciones como GitHub, Slack, etc.
🚨 Consideraciones
1. RLS es Crítico
Sin políticas RLS, cualquier usuario podría ver todos los cambios. Configurar RLS antes de usar Realtime en producción.
2. Límites de Supabase
- Plan gratuito: Hasta 200 conexiones simultáneas
- Plan Pro: Hasta 500 conexiones
- Para más, necesitas plan Enterprise
3. Compatibilidad con Prisma
- Prisma sigue funcionando normalmente
- Realtime es independiente de Prisma
- Puedes usar ambos juntos sin problemas
📝 Próximos Pasos
- Decidir si implementar Realtime ahora o más adelante
- Si decides implementar:
- Empezar con Fase 1 (setup básico)
- Reemplazar polling en lista de hackathons primero
- Expandir a otras áreas gradualmente
🔗 Recursos
- Documentación oficial de Supabase Realtime
- Guía de RLS (Row Level Security)
- Ejemplos de Next.js + Supabase Realtime
¿Quieres que implemente Supabase Realtime en el proyecto? Puedo:
- Configurar el cliente de Supabase
- Reemplazar el polling actual por Realtime
- Configurar RLS policies
- Implementar en las áreas más críticas primero