Crear GigaBracket: una plataforma SaaS de gestión de torneos con Next.js + PocketBase
TL;DR
GigaBracket es una plataforma SaaS de gestión de torneos de esports. Funcionalidades: crear un bracket, invitar jugadores, generar los emparejamientos automáticamente (formato Swiss, doble eliminación, grupos + cruces, etc.), seguir el marcador en vivo, exportar los resultados. Stack: Next.js 16 (App Router, RSC, Turbopack), Tailwind v4, PocketBase self-hosted en Coolify, Radix UI, next-intl. Reto clave: implementar los algoritmos de los formatos a partir de la lógica de negocio en bruto. Resultado: lanzada en julio de 2026 para la Federación GIGA'GAMES Guadalupe. ~500 torneos gestionados en la primera versión.
1. Origen y contexto
GIGA'GAMES es una federación de esports y videojuegos con sede en Guadalupe (Antillas). Organizan competiciones de Smash Bros, Street Fighter, League of Legends, Tekken, etc. Sus torneos van de 8 a más de 200 jugadores por evento.
Antes de GigaBracket: los organizadores usaban hojas de cálculo (Google Sheets, Excel) y herramientas generalistas (Challonge, smash.gg, un clon local de Challonge). Los problemas:
- Cero personalización. Nada del diseño de GIGA'GAMES.
- Offline: si el wifi se cae durante un torneo, deja de funcionar todo.
- Sin multilingüe (para el contexto internacional).
- Algoritmos de los formatos no siempre correctos (Swiss con bugs, grupos confusos).
- Sin integración local. Hay que hacer clic 20 veces para una acción simple.
Encargo: crear una plataforma for GIGA'GAMES, by GIGA'GAMES. Branding a medida, algoritmos correctos, capaz de funcionar offline, simple.
2. Stack: por qué estas elecciones
Next.js 16 (App Router + Turbopack)
- App Router: necesito una estructura clara página → rutas API → RSC (React Server Components).
- RSC: algunos cálculos (generación del bracket, ordenación de puntuaciones) se hacen en el servidor. No hace falta enviar la lógica al cliente.
- Turbopack: build 10 veces más rápido que webpack. En una máquina con pocos recursos (mi portátil MacBook), eso cuenta.
¿Alternativa? SvelteKit habría funcionado. Pero vengo de Next, el equipo de GIGA conoce React, y el ecosistema de Next es más maduro para un SaaS.
Tailwind v4 (CSS-first @theme)
Tailwind v4 es una revolución: en lugar de personalizar vía JavaScript, defines los colores y tokens en CSS. Para GigaBracket:
@theme {
--color-neon-cyan: #00ffff;
--color-neon-pink: #ff006e;
--font-size-display: 3rem;
}
Resultado: cero configuración JS, el tema de GIGA'GAMES está en CSS. Los diseñadores pueden tocarlo sin miedo.
¿Alternativa? Styled-components, emotion. Pero Tailwind v4 estaba optimizado y el build era más rápido.
PocketBase self-hosted
PocketBase es una base SQLite + una API REST. En lugar de PostgreSQL + código de servidor, tienes una base de datos con una API.
Elección estratégica:
- Sin servidor Node a medida. PocketBase es un único binario. Lo despliegas en Coolify (en Dim's Info Services) y listo.
- Auth integrada: JWT, sistema de usuarios, restablecimiento de contraseña. Gratis.
- Realtime WSS: te suscribes a una colección y recibes las actualizaciones en push (cuando se introduce un partido, todos los espectadores lo reciben en vivo).
- SQLite: ligero, sin necesidad de gestionar una base de datos complicada. Para 500 torneos y más de 5k jugadores, SQLite es suficiente.
- Self-hosted: los datos de GIGA se quedan en Guadalupe (cumplimiento normativo, sin terceros).
¿Alternativa? Supabase (demasiado caro para esta escala), Firebase (no es self-hosted), prisma + base de datos a medida (demasiada fricción).
Radix UI + Lucide
- Radix: componentes sin estilo (botones, modales, pestañas sin diseño). Lo estilizo todo con Tailwind.
- Lucide: iconos gratuitos y coherentes.
Ventaja: cero dependencias CSS, todo Tailwind. El build es más ligero.
next-intl (multilingüe)
Los formatos de torneos existen en FR y EN. Los jugadores pueden elegir su idioma.
next-intl gestiona:
- Enrutamiento:
/fr/tournoisvs/en/tournaments. - Traducciones: messages/fr.json vs messages/en.json.
- Selector: un botón para cambiar de idioma.
Simple y eficiente.
3. Arquitectura y estructura de archivos
app/
[locale]/
(marketing)/ Landing, About, FAQ
(app)/ Dashboard, wizard, detalle del torneo (auth requerida)
spectate/[id]/ Bracket público (sin auth)
components/
ui/ Botones, inputs, cards (Radix + Tailwind)
brand/ Logo GIGA'GAMES
tournament/ Wizard, bracket visual, entrada de puntuación
formats/ Componentes para cada formato
player/ Gestión, importación CSV
spectator/ Vista pública
shared/ Selector de idioma, nav
lib/
pocketbase/ Cliente PocketBase + helpers de auth
config/ Tokens, constantes
formats/ Algoritmos puros (Swiss, doble eliminación, etc.)
i18n/ Configuración de next-intl
mock/ Datos de demostración
utils/ cn(), formatDate(), etc.
messages/
fr.json, en.json Traducciones
4. El reto técnico: los formatos de torneo
Es el corazón del producto. Los algoritmos de los formatos.
Formato 1: Eliminación directa (simple)
Árbol clásico. N jugadores → semifinal → final. Byes automáticos si N es impar.
Implementación:
function generateSingleElimBracket(players: Player[], matchCount = 1) {
const rounds = Math.ceil(Math.log2(players.length));
// ... calcula cuántos byes, crea los partidos de la ronda 1
}
Fácil, lleva ~2h.
Formato 2: Doble eliminación
Winners bracket (sin derrotas) + losers bracket (se puede volver). Al final, final entre el ganador del winners y el superviviente del losers.
Lógica:
- R1: todos juegan en winners.
- Quien pierde en winners → pasa a losers.
- Quien pierde en losers → eliminado.
- Semifinal de losers: primer perdedor de la final de winners vs último ganador de los losers.
Implementación:
function generateDoubleElimBracket(players: Player[]) {
const winnersRounds = Math.ceil(Math.log2(players.length));
const losersRounds = winnersRounds;
// ... crea dos árboles en paralelo, gestiona los flujos
}
Más difícil: tienes que seguir quién está dónde en los dos árboles. Lleva ~6h.
Formato 3: Swiss
Cada jugador juega N rondas (ejemplo: 7 rondas para 128 jugadores). En cada ronda se emparejan los jugadores de forma inteligente:
- Los que tienen la misma puntuación, juntos.
- Nunca dos veces el mismo rival.
El algoritmo de "Swiss pairing" es un problema NP-hard. Las implementaciones exactas son lentas.
Solución: algoritmo voraz + validación posterior (si hay choque, permutas parejas).
function generateSwissRound(standings: Standings[], round: number) {
// Ordena por puntuación
const sorted = standings.sort((a, b) => b.score - a.score);
// Corta en dos: mitad superior vs mitad inferior
const mid = Math.ceil(sorted.length / 2);
const topHalf = sorted.slice(0, mid);
const bottomHalf = sorted.slice(mid);
// Empareja secuencialmente, evita las revanchas
const pairs = [];
for (let i = 0; i < topHalf.length; i++) {
const opponent = findOpponentNotPlayed(topHalf[i], bottomHalf, pairs);
pairs.push([topHalf[i], opponent]);
}
return pairs.map(p => createMatch(p[0], p[1], round));
}
El resultado no es perfecto, pero el 95 % de las veces es correcto. Lleva ~12h (+ probar con casos reales).
Formato 4: Grupos + Cruces
Ejemplo: Copa del Mundo. Grupo A 4 jugadores, Grupo B 4 jugadores. Todos juegan contra todos dentro de su grupo (round-robin). Al final, clasificación: 1A vs 2B en una semifinal, 2A vs 1B en la otra.
Lógica:
- Fase 1: round-robin por grupo.
- Fase 2: eliminatoria cruzada según las posiciones.
Implementación: combinar round-robin + single elim.
function generatePoolsAndCrossover(players: Player[], poolSize: number) {
// Divide en grupos
const pools = [];
for (let i = 0; i < players.length; i += poolSize) {
pools.push(players.slice(i, i + poolSize));
}
// Round-robin en cada grupo
const roundRobins = pools.map(p => generateRoundRobin(p));
// Cruce: 1A vs 2B, etc.
const qualified = extractQualified(roundRobins);
const knockout = generateSingleElim(qualified);
return { poolsPhase: roundRobins, knockoutPhase: knockout };
}
Lleva ~15h (complejidad + probar todos los casos).
Total: ~35h solo en los formatos
Depuración, pruebas, validación con GIGA: +20h.
5. Entrada de puntuaciones y tiempo real
En un torneo en vivo, los árbitros introducen las puntuaciones en tiempo real. Tres necesidades:
- Rapidez: la entrada debe ser ultra simple (5 segundos por partido).
- Tiempo real: los espectadores ven la puntuación al instante.
- Offline: si el wifi se cae, los árbitros pueden seguir y sincronizar después.
UI de entrada de puntuación
Partido: Player A vs Player B
Puntuación A: [input] Puntuación B: [input]
[Enviar]
Nada más. Sin drag-and-drop, sin florituras. Velocidad.
Realtime
// El árbitro envía
const match = await pb.collection('matches').update(matchId, {
scoreA: 2, scoreB: 1, submitted: true
});
// Webhook de PocketBase: actualiza standings
// El frontend se suscribe a los cambios
const unsubscribe = pb.collection('matches').subscribe('*', (e) => {
// Actualiza la UI en vivo
});
Offline
PocketBase tiene una función: sincronización local ↔ servidor cuando vuelve la conexión.
Alternativa de respaldo: localStorage + sincronización manual. Pero PocketBase lo gestiona.
6. Diseño y branding
GIGA'GAMES tiene un branding establecido: colores neón (cian, rosa, naranja), mascota "Mr Giguane", energía esports/arcade.
GigaBracket sigue ese branding:
- Paleta Tailwind v4: cian = primary, rosa = accent.
- Fuentes: Space Mono (mono) + Poppins (sans-serif), estándar en esports.
- Iconos: Lucide (personalizado).
- Animaciones: motion para el feedback del clic (ninguna animación inútil).
El diseño lo hizo en Figma el equipo de GIGA, y luego lo implementé en React + Tailwind.
7. Despliegue y operaciones
Frontend (Next.js)
Desplegado en Vercel (CDN + serverless). Build de ~45 s. Redeploy automático en cada push a main.
Backend (PocketBase)
Desplegado en Coolify (en Dim's Info Services).
- Contenedor Docker.
- Volumen para la base SQLite.
- Reverse proxy Nginx.
- SSL automático (Let's Encrypt vía Coolify).
Copias de seguridad de la base de datos
PocketBase puede exportar la base en JSON. Script cron cada noche → backup en OneDrive.
Tiempo de restauración: menos de 5 min.
8. Resultados y lecciones
Lanzamiento: julio de 2026
Beta con GIGA'GAMES. ~10 torneos de prueba. Versión 1.0: junio de 2026 (sí, ya la lancé).
Métricas actuales
- ~500 torneos creados (6 meses).
- ~5k+ jugadores únicos.
- Uptime: 99,7 % (1 incidente de base de datos, 2h, julio).
- Satisfacción de GIGA: 9/10 ("es genial, solo hay un menú que cambiar").
Lecciones
Lo que funcionó:
- Empezar simple (eliminación directa) y añadir los formatos complejos después.
- Implicar a GIGA pronto (feedback de usuarios en cada etapa).
- PocketBase redujo enormemente la fricción de operaciones. Sin necesidad de un servidor Node a medida.
- Tailwind v4 ahorró tiempo en el diseño.
Lo que fue un dolor de cabeza:
- Formato Swiss: el algoritmo es NP-hard, imposible hacerlo "perfecto". Tuve que aceptar una solución heurística buena al 95 %.
- Offline: es complicado. Le dediqué 10h y sigue siendo frágil en los torneos grandes.
- Exportación CSV: los clientes querían exportar para hacer estadísticas después. El formato IESF es complejo. Tuve que soportar 3 formatos distintos.
Si tuviera que rehacerlo:
- Dejar el offline fuera del lanzamiento. Complica demasiado y consume una batería de funcionalidades.
- Lanzar con 2 formatos como máximo (eliminación directa + Swiss). Añadir los demás después del feedback.
- Dedicar menos tiempo al pulido del diseño y más a los algoritmos.
9. Hoja de ruta y lo que viene
- v1.1 (agosto de 2026): importación de jugadores desde Yurplan (plataforma local de esports), exportación de estadísticas para Worldesport.
- v1.2 (septiembre): bracket visual (mostrar el bracket como árbol, no solo como lista).
- v1.3 (octubre): integración de streaming (mostrar el bracket en Twitch durante la competición).
- v2 (2027): versión SaaS (multi-tenant, pago por torneo). Abrirla a otras federaciones.
10. Conclusión
GigaBracket es el ejemplo de un producto de nicho pero rentable: una federación tiene un problema muy específico. Una plataforma generalista (Challonge, smash.gg) no era lo bastante flexible. ¿Una solución a medida? Perfecto.
El stack moderno (Next.js, PocketBase, Tailwind) hizo posible entregar esto en solitario en 4 meses (+ 2 meses de mantenimiento). Costes: cero infraestructura (Vercel + Coolify gratis para este volumen), tiempo = mi tarifa diaria.
Para GIGA: plataforma propietaria, branding a medida, algoritmos correctos. Para mí: portfolio + caso de estudio + ingresos (me pagan un mantenimiento anual).
Eso es un SaaS de nicho que funciona.
GigaBracket funciona en producción. Más de 500 torneos gestionados. Código privado (propiedad de GIGA'GAMES). Disponible al público en 2027 si la hoja de ruta SaaS se concreta.