Saltar al contenido principal

⚡ 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)

  1. Ve a DatabaseReplication
  2. 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

  1. Inicia el servidor de desarrollo:

    pnpm dev
  2. Abre la consola del navegador (F12)

  3. Deberías ver mensajes como:

    [Realtime] Suscrito a Hackathon (realtime-Hackathon-xxxxx)
  4. Si ves warnings como:

    ⚠️ Supabase Realtime no configurado...

    Verifica que las variables de entorno estén correctas.

Test Manual

  1. Abre la lista de hackathons en el navegador
  2. En otra pestaña, crea o actualiza un hackathon (desde el dashboard)
  3. La lista debería actualizarse instantáneamente sin recargar

Test con SQL

  1. Abre el SQL Editor en Supabase
  2. Ejecuta:
    UPDATE "Hackathon" 
    SET "status" = 'REGISTRATION'
    WHERE "slug" = 'tu-hackathon-slug';
  3. La página debería actualizarse automáticamente

Troubleshooting

Error: "Supabase Realtime no configurado"

Causa: Variables de entorno faltantes o incorrectas

Solución:

  1. Verifica que .env.local existe
  2. Verifica que las variables tienen NEXT_PUBLIC_ prefix
  3. 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:

  1. Verifica que Realtime está habilitado en la tabla (Database → Replication)
  2. Verifica que las políticas RLS permiten el acceso
  3. Revisa los logs en Supabase Dashboard → Logs → Realtime
  4. 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:

  1. Verifica la conexión WebSocket en DevTools → Network → WS
  2. Verifica que Realtime está habilitado en Supabase
  3. Revisa la consola del navegador para errores
  4. 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


Siguiente: Clerk Authentication