⚡ Supabase Realtime
Visión General
PuntoHack utiliza Supabase Realtime para proporcionar actualizaciones instantáneas (menos de 100ms) sin necesidad de polling. Esto mejora significativamente la UX y reduce la carga en el servidor.
Arquitectura Realtime
Configuración
Paso 1: Variables de Entorno
Agrega estas variables a tu archivo .env.local:
# Supabase Realtime
NEXT_PUBLIC_SUPABASE_URL=https://tu-proyecto.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
⚠️ IMPORTANTE:
- Usa
NEXT_PUBLIC_para que estén disponibles en el cliente - NO commits estas variables a Git (ya están en
.gitignore)
Paso 2: Habilitar Realtime en Supabase
Opción A: Desde el Dashboard (Recomendado)
- Ve a Database → Replication
- Habilita Realtime para las siguientes tablas:
- ✅
Hackathon(para cambios de estado y nuevos hackathons) - ✅
Submission(para nuevas submissions) - ✅
Score(para actualizaciones de evaluación) - ✅
ChallengeEvaluation(para evaluaciones de challenges) - ✅
Notification(para notificaciones instantáneas) - ✅
Team(para cambios en equipos)
- ✅
Opción B: Desde SQL Editor
Ejecuta este SQL en el SQL Editor de Supabase:
-- Habilitar Realtime para tablas específicas
ALTER PUBLICATION supabase_realtime ADD TABLE "Hackathon";
ALTER PUBLICATION supabase_realtime ADD TABLE "Submission";
ALTER PUBLICATION supabase_realtime ADD TABLE "Score";
ALTER PUBLICATION supabase_realtime ADD TABLE "ChallengeEvaluation";
ALTER PUBLICATION supabase_realtime ADD TABLE "Notification";
ALTER PUBLICATION supabase_realtime ADD TABLE "Team";
Paso 3: Configurar Row Level Security (RLS)
⚠️ CRÍTICO: Sin RLS, cualquier usuario podría ver TODOS los cambios de la base de datos.
Habilitar RLS
-- Habilitar RLS
ALTER TABLE "Hackathon" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Submission" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Score" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "ChallengeEvaluation" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Notification" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Team" ENABLE ROW LEVEL SECURITY;
Políticas de Seguridad
1. Hackathon (Lectura Pública)
-- Cualquiera puede ver hackathons publicados
CREATE POLICY "Hackathons are viewable by everyone"
ON "Hackathon"
FOR SELECT
USING (status != 'DRAFT');
-- Organizadores pueden ver sus hackathons (incluso DRAFT)
CREATE POLICY "Organizers can view own hackathons"
ON "Hackathon"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Profile" p
WHERE p.id = "Hackathon"."organizerId"
AND p."userId" = auth.uid()::text
)
);
2. Submission (Solo miembros del equipo y jueces)
-- Miembros del equipo pueden ver su submission
CREATE POLICY "Team members can view own submission"
ON "Submission"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "TeamMember" tm
JOIN "Profile" p ON p.id = tm."profileId"
WHERE tm."teamId" = "Submission"."teamId"
AND p."userId" = auth.uid()::text
)
);
-- Jueces asignados pueden ver submissions
CREATE POLICY "Assigned judges can view submissions"
ON "Submission"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "HackathonJudge" hj
JOIN "Profile" p ON p.id = hj."profileId"
WHERE hj."hackathonId" = "Submission"."hackathonId"
AND p."userId" = auth.uid()::text
)
);
3. Score (Solo jueces asignados)
-- Jueces pueden ver sus propios scores
CREATE POLICY "Judges can view own scores"
ON "Score"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Profile" p
WHERE p.id = "Score"."judgeId"
AND p."userId" = auth.uid()::text
)
);
4. Notification (Solo el usuario propietario)
-- Usuarios solo pueden ver sus propias notificaciones
CREATE POLICY "Users can view own notifications"
ON "Notification"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Profile" p
WHERE p.id = "Notification"."profileId"
AND p."userId" = auth.uid()::text
)
);
5. ChallengeEvaluation (Jueces y sponsors)
-- Jueces pueden ver sus evaluaciones
CREATE POLICY "Judges can view own evaluations"
ON "ChallengeEvaluation"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Profile" p
WHERE p.id = "ChallengeEvaluation"."judgeId"
AND p."userId" = auth.uid()::text
)
);
-- Sponsors pueden ver evaluaciones de sus challenges
CREATE POLICY "Sponsors can view challenge evaluations"
ON "ChallengeEvaluation"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "Challenge" c
JOIN "Sponsorship" s ON s.id = c."sponsorshipId"
JOIN "OrganizationMember" om ON om."organizationId" = s."organizationId"
JOIN "Profile" p ON p.id = om."profileId"
WHERE c.id = "ChallengeEvaluation"."challengeId"
AND p."userId" = auth.uid()::text
)
);
6. Team (Miembros del equipo)
-- Miembros del equipo pueden ver su equipo
CREATE POLICY "Team members can view own team"
ON "Team"
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM "TeamMember" tm
JOIN "Profile" p ON p.id = tm."profileId"
WHERE tm."teamId" = "Team".id
AND p."userId" = auth.uid()::text
)
);
Implementación
Cliente Supabase
Ubicación: 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);
Hooks Realtime
Ubicación: src/lib/realtime/hooks/
useHackathonsRealtime
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase/client';
import type { Hackathon } from '@prisma/client';
export function useHackathonsRealtime() {
const [hackathons, setHackathons] = useState<Hackathon[]>([]);
useEffect(() => {
// Suscripción inicial
const fetchHackathons = async () => {
// ... fetch inicial
};
fetchHackathons();
// Suscripción a cambios
const channel = supabase
.channel('hackathons-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'Hackathon',
},
(payload) => {
// Manejar cambios
if (payload.eventType === 'INSERT') {
setHackathons((prev) => [...prev, payload.new as Hackathon]);
} else if (payload.eventType === 'UPDATE') {
setHackathons((prev) =>
prev.map((h) =>
h.id === payload.new.id ? (payload.new as Hackathon) : h
)
);
} else if (payload.eventType === 'DELETE') {
setHackathons((prev) =>
prev.filter((h) => h.id !== payload.old.id)
);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
return hackathons;
}
Hooks Disponibles
1. useHackathonsRealtime
Suscribe a cambios en la tabla Hackathon.
Uso:
import { useHackathonsRealtime } from '@/lib/realtime/hooks/use-hackathons-realtime';
function HackathonsList() {
const hackathons = useHackathonsRealtime();
return (
<div>
{hackathons.map((hackathon) => (
<div key={hackathon.id}>{hackathon.name}</div>
))}
</div>
);
}
2. useHackathonStatusRealtime
Suscribe a cambios de estado de un hackathon específico.
Uso:
import { useHackathonStatusRealtime } from '@/lib/realtime/hooks/use-hackathon-status-realtime';
function HackathonDetail({ hackathonId }: { hackathonId: string }) {
const status = useHackathonStatusRealtime(hackathonId);
return <div>Estado: {status}</div>;
}
3. useLeaderboardRealtime
Suscribe a cambios en el leaderboard (scores).
Uso:
import { useLeaderboardRealtime } from '@/lib/realtime/hooks/use-leaderboard-realtime';
function Leaderboard({ hackathonId }: { hackathonId: string }) {
const leaderboard = useLeaderboardRealtime(hackathonId);
return (
<div>
{leaderboard.map((entry) => (
<div key={entry.team.id}>
{entry.team.name}: {entry.weightedScore}
</div>
))}
</div>
);
}
4. useNotificationsRealtime
Suscribe a nuevas notificaciones para el usuario actual.
Uso:
import { useNotificationsRealtime } from '@/lib/realtime/hooks/use-notifications-realtime';
function NotificationsList() {
const notifications = useNotificationsRealtime();
return (
<div>
{notifications.map((notif) => (
<div key={notif.id}>{notif.title}</div>
))}
</div>
);
}
Flujo de Actualización
Ventajas vs Polling
Antes (Polling)
Problemas:
- ❌ Latencia alta (hasta 5 segundos)
- ❌ Muchas requests HTTP innecesarias
- ❌ Mayor carga en servidor
- ❌ Mayor consumo de ancho de banda
Ahora (Realtime)
Ventajas:
- ✅ Latencia baja (menos de 100ms)
- ✅ Una sola conexión WebSocket
- ✅ Menor carga en servidor
- ✅ Menor consumo de ancho de banda
- ✅ Mejor UX
Verificación de Configuración
Test Rápido
-
Inicia el servidor de desarrollo:
pnpm dev -
Abre la consola del navegador (F12)
-
Deberías ver mensajes como:
[Realtime] Suscrito a Hackathon (realtime-Hackathon-xxxxx) -
Si ves warnings como:
⚠️ Supabase Realtime no configurado...Verifica que las variables de entorno estén correctas.
Test Manual
- Abre la lista de hackathons en el navegador
- En otra pestaña, crea o actualiza un hackathon (desde el dashboard)
- La lista debería actualizarse instantáneamente sin recargar
Test con SQL
- Abre el SQL Editor en Supabase
- Ejecuta:
UPDATE "Hackathon"
SET "status" = 'REGISTRATION'
WHERE "slug" = 'tu-hackathon-slug'; - La página debería actualizarse automáticamente
Troubleshooting
Error: "Supabase Realtime no configurado"
Causa: Variables de entorno faltantes o incorrectas
Solución:
- Verifica que
.env.localexiste - Verifica que las variables tienen
NEXT_PUBLIC_prefix - Reinicia el servidor de desarrollo (
pnpm dev)
Error: "No se puede suscribir al canal"
Causa: RLS bloqueando el acceso o Realtime no habilitado
Solución:
- Verifica que Realtime está habilitado en la tabla (Database → Replication)
- Verifica que las políticas RLS permiten el acceso
- Revisa los logs en Supabase Dashboard → Logs → Realtime
- Verifica que
auth.uid()está disponible (usuario autenticado con Clerk)
No se actualiza automáticamente
Causa: Realtime no está habilitado o hay un problema de conexión
Solución:
- Verifica la conexión WebSocket en DevTools → Network → WS
- Verifica que Realtime está habilitado en Supabase
- Revisa la consola del navegador para errores
- Verifica que las políticas RLS están correctamente configuradas
Mejores Prácticas
1. Limpiar Suscripciones
Siempre limpia las suscripciones en el cleanup de useEffect:
useEffect(() => {
const channel = supabase.channel('...').subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
2. Manejar Errores
Implementa manejo de errores en los hooks:
useEffect(() => {
const channel = supabase
.channel('...')
.on('postgres_changes', { ... }, (payload) => {
// Manejar cambios
})
.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('Subscribed to channel');
} else if (status === 'CHANNEL_ERROR') {
console.error('Channel error');
}
});
}, []);
3. Optimizar Queries
Solo suscribe a las tablas y eventos que necesitas:
// ✅ Correcto: Solo suscribe a INSERT y UPDATE
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'Hackathon',
}, handler)
// ❌ Incorrecto: Suscribe a todos los eventos
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'Hackathon',
}, handler)
Próximos Pasos
- Clerk Authentication - Configuración de autenticación
- Prisma ORM - Uso de Prisma
- Development Guide - Setup completo
Siguiente: Clerk Authentication