Allay es el proyecto con el que terminé el Grado Superior de Desarrollo de Aplicaciones Web: el Proyecto Intermodular, que en otros sitios llamarían TFG. Lo desarrollamos entre dos, Helen Cano y yo, y la idea de partida es sencilla de explicar: llevar un servidor de Minecraft más allá del juego.

Si has jugado en un servidor de Minecraft, sabes que todo existe mientras estás conectado. Tus amigos, tus mensajes, tu perfil o tus monedas viven dentro del juego, y cuando lo cierras desaparecen. Allay es un sistema de gestión pensado para que todo eso esté disponible también desde una web, en cualquier dispositivo, y para darle al equipo de moderación herramientas en un único sitio.

Nos repartimos el trabajo según lo que mejor sabía hacer cada uno. Helen, que antes había hecho el Grado Superior de Diseño Gráfico, se encargó de la imagen de marca, el diseño de la interfaz y el frontend. Yo me encargué de todo el backend y de la capa de datos del frontend: las llamadas a la API, la caché, la sesión y los WebSockets.

En este post cuento de qué va el proyecto, cómo lo planteamos y cómo está construido por dentro. La parte técnica que explico con más detalle es la mía, el backend.

Un poco de contexto

Un servidor grande de Minecraft no es un único servidor, sino una network: varios servidores (un lobby, distintos modos de juego, uno de autenticación…) unidos por un proxy, que es el punto de entrada y decide a qué servidor va cada jugador. Las funcionalidades se añaden con plugins, y yo llevo años programándolos (si te interesa, en este blog hablo de CloudMultiProxy).

Hay plugins para casi todo, pero no hay ninguno abierto que haga de sistema central de usuarios compartido entre el juego y una web. Los servidores que lo tienen usan soluciones privadas hechas a medida. Allay busca cubrir ese hueco con algo modular y asequible para cualquier servidor.

El ecosistema de Allay: la network de Minecraft, el backend central y la web

Hay otro detalle del juego que condiciona el diseño: existen cuentas premium (el juego comprado, verificado por Microsoft) y no premium, que muchos servidores aceptan. Un jugador no premium no tiene una identidad verificada, así que el servidor necesita su propio sistema de cuentas con contraseña. Allay lo incluye desde el principio.

Un proyecto pensado por fases

Desde el análisis inicial vimos que el sistema completo era demasiado grande para el tiempo del proyecto: gestión de usuarios y roles, amigos, mensajería, staff, notificaciones, ayuda, reportes, sanciones, economía, tienda, buzón… Cada módulo lo definimos con sus requisitos (más de 60 en total), marcando si pertenecían a la web, al juego o a ambos.

Así que lo dividimos en cuatro fases y el proyecto de fin de ciclo se centró en la primera:

Las cuatro fases de Allay

La fase 1 tenía dos bloques en 8 semanas, organizadas en sprints de una semana: el diseño teórico de todo el sistema (requisitos, tecnologías, bases de datos, identidad de marca) y la primera versión web funcional, con usuarios y sesiones, amigos, notificaciones en tiempo real, administración y roles, zona de staff y mensajería privada.

Arquitectura

Arquitectura de Allay: frontend, backend en capas y tres bases de datos

Allay sigue un modelo cliente-servidor de tres capas, y el backend repite la misma idea por dentro:

  • Frontend en React 19 con TypeScript, TanStack Router (rutas por archivos y rutas protegidas), TanStack Query para la caché de datos y la sesión, TanStack Form y Table, Zod para validar cada respuesta de la API, shadcn/ui y Tailwind v4.
  • Backend en Go con GoFiber v3, organizado en handlers (solo HTTP), casos de uso (toda la lógica de negocio) y repositorios (acceso a datos detrás de interfaces). Los casos de uso no saben si un dato sale de PostgreSQL, de Redis o de ClickHouse.
  • Tres bases de datos, cada una donde mejor encaja.

Una base de datos para cada cosa

Usar tres sistemas en un proyecto de fin de ciclo puede parecer excesivo, pero cada uno tiene un papel claro:

  • PostgreSQL es la fuente de verdad: usuarios, solicitudes de amistad y amistades, con sus restricciones, claves foráneas y tipos ENUM para roles y estados.
  • Redis guarda lo que se consulta en cada petición y tiene caducidad: las sesiones y las verificaciones temporales (tickets, restablecimiento de contraseña…). Comprobar una sesión en memoria es mucho más barato que ir a disco en cada request.
  • ClickHouse guarda los mensajes, tanto los privados como los del chat de staff. Es una base de datos columnar pensada para volúmenes enormes, y en una network grande los mensajes crecen mucho más rápido que cualquier otra tabla.

Las migraciones de PostgreSQL y ClickHouse son archivos .sql versionados que el backend aplica solo al arrancar con golang-migrate.

Cuentas, sesiones y seguridad

Premium y no premium

Al registrarse, el jugador elige si su cuenta es premium. Por debajo pasa esto:

Registro de cuentas premium y no premium

El UUID offline se calcula siempre, igual que lo hace el propio servidor de Minecraft: un MD5 de "OfflinePlayer:" + nombre con los bits de versión 3 de un UUID. Si la cuenta es premium, el backend consulta la API de Mojang para obtener el UUID real (con PlayerDB como respaldo si Mojang no responde) y rechaza el registro si ese nombre no corresponde a una cuenta comprada. Guardar los dos UUID deja preparado el terreno para la fase 2, cuando el jugador llegue desde el juego.

Sesiones

  • Las contraseñas se guardan con bcrypt.
  • Al iniciar sesión se genera un token (un ULID) que viaja en una cookie HttpOnly, inaccesible desde JavaScript. La sesión dura 7 días.
  • La sesión vive en Redis con su TTL, junto con el navegador y el sistema operativo desde el que se abrió. Cada usuario tiene además una lista de sus sesiones, que se limpia de las caducadas al crear una nueva.
  • Desde los ajustes se pueden ver y cerrar sesiones en otros dispositivos. El frontend nunca ve el token de las demás sesiones: el backend las identifica con un fragmento de su SHA-256, de forma que lo que llega al navegador no sirve para suplantar a nadie.

Roles

Hay cuatro roles jerárquicos: USER, STAFF, ADMIN y OWNER, y cada uno tiene los permisos de los inferiores. Las rutas se agrupan por el nivel que necesitan y cada grupo pasa por su middleware: uno comprueba la sesión, otro que el usuario sea al menos STAFF (zona de staff) y otro que sea al menos ADMIN (panel de administración). En el frontend, las rutas protegidas de TanStack Router redirigen al login antes de cargar si no hay sesión.

Tiempo real con WebSockets

Las solicitudes de amistad, los mensajes privados y el chat de staff llegan en tiempo real. Para eso el backend mantiene un hub de WebSockets con salas: cada conexión se une a la sala de su usuario (user:<id>) y a la de su rol (role:staff, role:admin…). Cuando un caso de uso necesita avisar a alguien, publica en la sala correspondiente y todas las pestañas abiertas de esa persona reciben el evento. Los mensajes viajan en un sobre JSON sencillo, { type, data }, y el frontend registra manejadores por tipo.

El reto aquí es la autenticación. La API WebSocket del navegador no permite añadir cabeceras propias, y meter el token de sesión en la URL es mala idea, porque las URLs acaban en logs. La solución que usé es un ticket efímero:

Autenticación del WebSocket con un ticket efímero

  1. El navegador pide un ticket con una petición normal, autenticada con su cookie.
  2. El backend genera un ID aleatorio, lo guarda en Redis asociado al usuario y le da 10 segundos de vida.
  3. El navegador abre el WebSocket con ese ticket en la URL.
  4. El backend lo valida, carga el usuario y registra la conexión en sus salas.

El token de sesión real nunca sale de la cookie, y lo que aparece en la URL caduca antes de que sirva para nada. Si la conexión se cae, el cliente vuelve a pedir un ticket y reintenta con espera creciente.

Los mensajes en ClickHouse

La parte que más me gustó diseñar fue el almacenamiento de los mensajes privados. En ClickHouse la clave de ordenación (ORDER BY) decide cómo se colocan los datos en disco, y la elegí así:

Cómo guarda ClickHouse los mensajes privados

Usar least(sender_id, receiver_id) y greatest(sender_id, receiver_id) hace que los mensajes de A a B y de B a A compartan clave, así que toda una conversación queda contigua y ordenada por fecha. Abrir un chat es leer un rango corto, no recorrer la tabla.

La lista de chats (la conversación con cada amigo y su último mensaje) sale de una sola consulta: se agrupa por esa misma pareja y argMax se queda con el mensaje más reciente de cada una. Además, la tabla usa ReplacingMergeTree, que descarta filas duplicadas con la misma clave al fusionar partes, y se particiona por mes.

La interfaz

Aquí es donde se nota el trabajo de Helen. Antes de escribir una línea de interfaz diseñó la identidad de marca completa en Illustrator (el logo, inspirado en el propio allay del juego, y la paleta de colores) y los wireframes de cada pantalla en Figma. Después los llevó a React con shadcn/ui y Tailwind. Estas son algunas pantallas de la versión final de la fase 1:

Página principal de Allay

Inicio de sesión

Lista de amigos

Perfil de un amigo con el chat privado

Zona de staff con la lista de staff conectado y su chat

Panel de administración de usuarios

Las skins de los jugadores se obtienen a partir de su nombre de usuario a través de un endpoint del backend, y las notificaciones en tiempo real se muestran con Sileo.

Resultado

Completamos prácticamente toda la fase 1: el diseño completo del sistema, la identidad de marca y una web funcional con cuentas, sesiones, amigos, mensajería, notificaciones, staff y administración. Se puede levantar en local con Docker Compose para las bases de datos, make build para el backend y pnpm para el frontend.

El proyecto no llegó a desplegarse en producción, y las fases 2, 3 y 4 (la integración con el juego, los sistemas de staff y la economía) quedan como trabajo futuro.

Lo que me llevo

Allay fue, con diferencia, el proyecto más ambicioso que había planteado hasta entonces fuera de los plugins. Me obligó a pensar primero en el sistema completo y después recortar con criterio qué entraba en cada fase, en lugar de empezar a programar sin más.

También me hizo salir de lo que se ve en el ciclo: Go en lugar de los lenguajes del temario, una base de datos columnar para mensajes, WebSockets con autenticación propia y una arquitectura por capas pensada para crecer. Y, sobre todo, fue un proyecto en equipo de principio a fin. Tener a alguien con formación en diseño gráfico cambió el resultado: Allay no parece un proyecto de clase con una plantilla por defecto, tiene una marca y una interfaz cuidadas. Eso me dejó centrarme en la arquitectura sabiendo que lo que construía iba a verse bien.