Servidores Authoritative
Crea scenes multijugador de Decentraland con un server authoritative sin interfaz.
Descripción general
Decentraland ejecuta scenes localmente en la máquina de cada jugador. De forma predeterminada, los jugadores pueden verse entre sí e interactuar directamente, pero cada uno interactúa con el entorno de manera independiente. Los cambios en el entorno no se comparten entre los jugadores por defecto.
Permitir que todos los jugadores vean una scene como si tuviera el mismo contenido en el mismo estado es extremadamente importante para que los jugadores interactúen de maneras más significativas. Sin esto, si un jugador abre una puerta y entra en una casa, los demás jugadores verán esa puerta como todavía cerrada, y el primer jugador parecerá atravesar directamente la puerta cerrada para los demás.
Un servidor autoritativo es un proceso de servidor sin interfaz gráfica que ejecuta el código de tu scene, valida los cambios de estado y transmite el resultado a todos los jugadores conectados. En lugar de confiar en que cada client informe sus propias acciones, el servidor actúa como la única fuente de verdad. Esto lo convierte en el enfoque recomendado para sincronizar scenes multijugador.
Un servidor autoritativo es ideal siempre que la justicia sea importante para la mecánica del juego, ya que puedes implementar validaciones elaboradas anti-cheat que se ejecuten en el lado del servidor. También puedes almacenar claves privadas y otra información sensible en el servidor, evitando tener que exponerlas directamente al usuario.
Tener un servidor autoritativo también resuelve un problema real: en una configuración peer-to-peer, dos jugadores controlando algo como una plataforma flotante pueden producir resultados conflictivos. Cada client establece la plataforma a una altura diferente, y nadie tiene la autoridad para decidir cuál es la correcta. Un servidor autoritativo resuelve cada cambio en un solo lugar, de modo que todos los clients convergen en el mismo estado.
También te da un lugar para persistir datos entre sesiones: tablas de clasificación, progreso del jugador, logros desbloqueados o cambios en el entorno como puertas abiertas o items colocados. Cuando los jugadores regresan, el mundo refleja lo que ocurrió antes.
Decentraland aloja y despliega el servidor por ti. Publicar tu scene mediante el proceso normal también publica el servidor sin problemas, sin pasos extra ni necesidad de pagar por ningún hosting.
Configuración
1. Instala la versión del SDK de auth-server
Las APIs nativas del servidor autoritativo (isServer, registerMessages, Storage, EnvVar, etc.) están disponibles en una rama separada del SDK. Ejecuta los siguientes comandos para instalarla en tu proyecto en lugar de la rama estándar del SDK:
npm install @dcl/sdk@auth-server
npm install @dcl/js-runtime@auth-server2. Configura scene.json
Opcionalmente añade lo siguiente a tu scene.json a nivel raíz:
Añade logsPermissions para listar las direcciones de wallets que pueden ver console.log() del servidor. Los usuarios listados luego pueden ver los logs del servidor en producción ejecutando el siguiente comando:
npx sdk-commands sdk-server-logs
3. Ejecuta el preview
Usa el comando estándar de preview, no se necesitan pasos extra. Al usar la rama auth-server del SDK, el preview inicia automáticamente en segundo plano una versión local del servidor autoritativo.
La sesión local del servidor no está conectada con la de producción, así que puedes probar cosas sin afectar a los jugadores que están en tu scene publicada.
Branching de Server / Client
El código de la scene en la src carpeta se ejecuta tanto en el server como en el client. Usa la isServer() función para dividir las rutas de ejecución:
El servidor ejecuta tu scene sin interfaz y sin rendering. Tiene acceso verificado a todas las posiciones de los jugadores, wearables y otros datos mediante PlayerIdentityData y es la única autoridad sobre el estado del juego.
Components sincronizados y validación
Sincronizar Entities con todos los Clients
Usa syncEntity para transmitir cualquier cambio en los components indicados de esa entity:
La sintaxis es idéntica a la utilizada por la función de multijugador serverless feature, lo que hace trivial actualizar una scene desde el uso de esta arquitectura al servidor autoritativo. Cuando una scene usa el servidor autoritativo, las actualizaciones de estado ya no se envían entre todos los jugadores; en su lugar, todas las actualizaciones de estado ahora se enrutan y validan a través del servidor.
📔 Nota: Con el servidor autoritativo, el patrón ideal es que solo el servidor llame a syncEntity. De ese modo no tienes que preocuparte por la consistencia de entity-id. Más bien, la entity es instanciada y compartida por el servidor, y todos los clients se sincronizan sobre esa instancia. Protégelo siempre con isServer(). Esto es diferente de función de multijugador serverless, donde cada client llama a syncEntity por su cuenta.
Validar cambios
Usa validateBeforeChange() para restringir cualquier actualización de estado en un component específico de una entity. Te permite ejecutar una función de validación personalizada, y los cambios solo se completan cuando la prueba de validación se cumple.
Si la validación devuelve el valor true, entonces el cambio se acepta y se propaga a todos los jugadores. Si la validación devuelve el valor false, entonces el cambio se rechaza. Un cambio rechazado no se pasará a otros jugadores y se revertirá para el jugador que intentó hacerlo.
Validar valores
El caso más simple es validar que el nuevo valor que se está escribiendo esté dentro de ciertos parámetros. Por ejemplo, aceptar solo cambios en un Transform cuando la nueva posición Y esté por encima de 0:
Porque validateBeforeChange() solo tiene sentido en el server, protégelo siempre con isServer(). En el client la llamada no hace nada útil.
Puedes usar esto para evitar cambios que vayan en contra de la lógica de tu juego, como mecanismos anti cheat.
Validar proximidad al jugador
Puedes combinar validateBeforeChange() con posiciones de jugadores verificadas por el server para comprobar que un jugador está lo suficientemente cerca de un objeto antes de permitirle interactuar con él. Por ejemplo, cuando un jugador intenta recoger un objeto cambiando su Transform, puedes rechazar el cambio si el objeto está a más de 5 metros del jugador:
Este patrón es útil como mecanismo anti-cheat: evita que los jugadores se extiendan por la scene para agarrar objetos con los que no deberían poder interactuar.
Permitir cambios solo a admins
También puedes validar en función de quién está enviando el cambio. Cada valor entrante incluye un campo senderAddress con la dirección de wallet del remitente. Úsalo para permitir cambios solo de ciertos jugadores. Por ejemplo, para permitir que solo los admins de la scene modifiquen un component VideoPlayer component:
Consulta Scene Admin para más contexto sobre cómo los jugadores se convierten en admins de una scene.
Permitir cambios solo por el server
El caso más estricto es aceptar solo escrituras que se originen en el propio server, rechazando cualquier cambio que provenga de un client. Este es el patrón recomendado para estado que debe ser totalmente autoritativo: puntuaciones, fase del juego, entities generadas, etc.
Cada valor entrante incluye un campo senderAddress . Cuando el remitente es el server, este campo coincide con la constante AUTH_SERVER_PEER_ID, exportada desde @dcl/sdk/network/message-bus-sync.
El ejemplo de abajo define un pequeño helper protectServerEntity() que aplica esta comprobación a uno o más components de una entity dada. Es una forma conveniente de proteger múltiples components (como Transform y GltfContainer) en una sola llamada:
📔 Nota: Llama siempre a protectServerEntity() dentro de un bloque isServer() . Envuelve validateBeforeChange(), que solo tiene sentido en el server — llamarlo en un client produce errores.
Components personalizados
También puedes aplicar validateBeforeChange() en components personalizados definidos por la scene.
Mensajes
Los components sincronizados son excelentes para el estado que todos los jugadores deben ver continuamente: cosas como posiciones, puntuaciones o fase del juego. Pero no todo encaja en ese modelo. A veces un jugador solo necesita decirle al server "hice clic en este botón" o "quiero unirme al juego", y el server necesita responder con una respuesta de una sola vez como "la ronda comenzó" o "aquí están tus estadísticas". Eso son eventos, no estado continuo.
Para eso sirven los mensajes. Usa registerMessages() para comunicación tipada y validada por esquema entre clients y el server. Los mensajes son fire-and-forget: un client envía uno al server, el server lo procesa y opcionalmente envía uno de vuelta. No crean por sí solos ningún estado persistente.
Definir mensajes
Define todos los mensajes en un archivo compartido que importen tanto el server como el client. Así ambos lados siempre están de acuerdo sobre qué mensajes existen y qué datos transportan. Cada mensaje es un Schemas.Map que describe su payload:
Enviar mensajes
Los clients solo pueden enviar mensajes al server. No hay mensajería directa de client a client. El server puede transmitir a todos los clients o apuntar a jugadores específicos por dirección.
Recibir mensajes
En el server, cada mensaje recibido incluye un context objeto con la dirección de wallet verificada del remitente. Úsalo para saber qué jugador envió el mensaje (nunca confíes en la identidad declarada por el propio payload).
Espera a que se sincronice el estado antes de enviar
Los clients deben esperar hasta que el estado de la scene se sincronice antes de enviar su primer mensaje, para evitar condiciones de carrera al unirse:
Tipos de Schemas disponibles
Todos los payloads de mensajes y components personalizados usan Schemas para serialización binaria. Aquí tienes una referencia rápida de los tipos disponibles:
📔 Nota: Los Messages deben definirse usando Schemas.Map(...). No puedes enviar objetos JavaScript simples, fallarán en la serialización binaria.
El server leyendo posiciones de jugadores
El server puede leer las posiciones de jugadores verificadas ; los clients no pueden falsificarlas. Esta es la base del anti-cheat basado en posición:
📔 Nota: Usa siempre PlayerIdentityData + Transform en el server para obtener las posiciones de los jugadores. Nunca confíes en valores reportados por el propio client.
Almacenamiento de datos
Persistir datos entre reinicios del server. Storage es solo para el server, protege siempre las llamadas con isServer(). El server puede escribir y leer estos datos.
Los datos pueden almacenarse en dos niveles:
World: Úsalo para datos relevantes para todos los jugadores, como tablas de clasificación o cambios persistentes del entorno.
Player: Úsalo para datos específicos del jugador, como guardar progreso o preferencias de ese jugador.
💡 Consejo: Storage solo acepta strings. Usa JSON.stringify() / JSON.parse() para objetos y String() / parseInt() para números.
Durante el desarrollo local, Storage se escribe en node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json.
World Storage — Compartido entre todos los jugadores
También puedes gestionar el almacenamiento de la scene mediante la línea de comandos, usando npx sdk-commands storage scene:
Player Storage — Por dirección de wallet
También puedes gestionar el almacenamiento de jugadores mediante la línea de comandos, usando npx sdk-commands storage player:
Acceder a datos almacenados
Puedes ver y editar los datos almacenados en vivo en tu server mediante la UI de storage, entrando en este enlace:
También puedes llegar a esta página a través de Creator Hub. Abre la pestaña Manage , haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona View server data.
Allí puedes ver una lista de todos los worlds y land donde puedes publicar scenes.
Abre tu scene y luego la pestaña Scene o Player tab.
En la pestaña Scene verás una lista de todas las variables almacenadas. Desde allí puedes editar o eliminar cualquiera de estas variables haciendo clic en el icono del lápiz o de la papelera.

En la pestaña Player En la pestaña
Cambiar la estructura de los datos
Los datos almacenados en producción no se borran cuando publicas una nueva versión de tu scene. Esto es excelente para tablas de clasificación, progreso del jugador y cambios persistentes del entorno que los jugadores esperan que sobrevivan más allá de pequeñas actualizaciones de tu scene.
La otra cara es que los datos guardados en storage fueron escritos por una versión anterior de tu código. Si tu nuevo código espera una forma diferente, analizar o leer esos datos antiguos puede fallar de manera sutil. Un campo que renombraste faltará. Un campo que antes era una string y ahora es un objeto lanzará un error cuando intentes acceder a una propiedad en él. Un jugador que no haya iniciado sesión durante meses puede cargar datos que preceden a una estructura que tu código ya no sabe cómo manejar.
📔 Nota: Los cambios de schema no solo afectan a la primera lectura después de un deploy. Los datos almacenados permanecen hasta que se sobrescriben o eliminan, así que un valor de formato antiguo puede aparecer en cualquier momento, a menudo de un jugador que vuelve y del que ya te habías olvidado.
Buenas prácticas
Analiza siempre de forma defensiva. Trata cualquier cosa que salga de storage como entrada no confiable, aunque tú la hayas escrito. Envuelve
JSON.parse()en untry/catch, comprueba que los campos existan antes de leerlos y ten preparado un valor predeterminado razonable cuando no existan:Añade campos, no los renombres ni los elimines. El cambio de esquema más seguro es uno aditivo: introduce un nuevo campo con un valor predeterminado y deja intactos los campos existentes. Los datos antiguos simplemente no tendrán el nuevo campo, que tu análisis defensivo ya maneja. Renombrar un campo obliga a que se rompa cada registro antiguo.
Versiona los objetos almacenados. Incluye un campo
versiondesde el primer día. Cuando leas datos, ramifica según la versión y migra las estructuras antiguas a la actual antes de usarlas. Esto mantiene el resto de tu código trabajando con una única estructura actual:Vuelve a escribir el valor migrado. Una vez que hayas actualizado un registro en memoria, guárdalo de nuevo para que la próxima lectura ya esté en el nuevo formato. Con el tiempo, esto vacía el conjunto de registros de estructura antigua sin necesidad de un script de migración de una sola vez.
Para cambios incompatibles, usa una nueva clave. Si la nueva estructura es realmente incompatible y migrarla no vale la pena, escribe en una nueva clave de almacenamiento (por ejemplo
progress_v2) e ignora la antigua. La clave antigua permanece sin causar problemas en el almacenamiento y evitas cualquier ruta de lectura que tenga que interpretarla. Puedes limpiar las claves antiguas más tarde mediante la UI de almacenamiento o losnpx sdk-commands storagecomandos.Prueba con datos de producción reales. Antes de desplegar un cambio estructural, extrae algunos registros reales de la UI de almacenamiento y ejecuta tu nuevo código de análisis sobre ellos. Los casos límite que causan problemas suelen ser registros cuya existencia no conocías.
Deja una salida de emergencia. Ten en cuenta que puedes editar o eliminar registros individuales desde la UI de almacenamiento o mediante
npx sdk-commands storage. Si un solo jugador termina atascado en un estado incorrecto después de un cambio de esquema, puedes corregir su registro directamente sin volver a desplegar.
Variables de entorno
Configura tu scene sin codificar valores en el código. Las variables de entorno son útiles para datos sensibles, y también para indicadores de características o parámetros que pueden cambiarse fácilmente sin volver a publicar tu scene.
Las variables de entorno son solo para el server. Protégelas con isServer(). El server puede leer variables de entorno, pero no cambiar sus valores.
EnvVar.get() devuelve un Promise<string> y se resuelve a una cadena vacía cuando la variable no está definida, así que proporciona siempre un valor de reserva para los valores faltantes:
Datos sensibles
Las variables de entorno son especialmente útiles para almacenar claves privadas, códigos de reclamación de recompensas y otros datos sensibles que sería arriesgado exponer en el código compilado del scene público.
Puedes almacenar claves privadas en el almacenamiento del server, y hacer que solo el server las lea con isServer(). De ese modo, los datos sensibles nunca pasan por la máquina del jugador.
Desarrollo local
Para usar variables de entorno mientras ejecutas tu proyecto localmente, crea un archivo .env en la raíz de tu proyecto:
Importante: Añade .env a tu .gitignore, para que estos valores potencialmente sensibles nunca se suban a los servers de contenido públicos.
Cambiar variables de entorno
La forma más sencilla de cambiar los valores de tus variables de entorno es a través de la UI de almacenamiento.
Puedes acceder a los datos almacenados por el almacenamiento del scene introduciendo este enlace:
También puedes llegar a esta página a través de Creator Hub. Abre la pestaña Manage , haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona View server data.
Allí puedes ver una lista de todos los worlds y land donde puedes publicar scenes.
Abre tu scene y luego la pestaña Entorno pestaña. Deberías ver todas las variables de entorno del proyecto.

Ten en cuenta que no puedes leer los valores de ninguna de estas variables de entorno (eso es para proteger datos sensibles), pero sí puedes eliminarlas o sobrescribirlas. Solo haz clic en el icono del lápiz o de la papelera.
También puedes gestionar variables de entorno desde la línea de comandos, usando npx sdk-commands storage env:
También puedes dirigirlo a un entorno específico con la bandera --target :
Las variables de entorno desplegadas tienen prioridad sobre los valores .env .
Estructura de proyecto recomendada
Separar el código del server, del client y el compartido mantiene el código legible a medida que crece:
💡 Consejo: Mantén todas las llamadas a registerMessages() y las definiciones de componentes personalizados en shared/. Tanto el server como el client importan desde allí, garantizando que siempre estén de acuerdo sobre los esquemas de mensajes.
Buenas prácticas de rendimiento
Cada cambio de componente envía todo el dato del componente por la red. Esto es diferente de lo que hace Colyseus, que envía solo los diffs. Al diseñar componentes personalizados, ten esto en cuenta. La solución óptima puede ser almacenar datos en componentes separados, según la frecuencia de cambio.
❌ Evita componentes monolíticos
✅ Prefiere componentes atómicos
Regla general: agrupa los campos que cambian juntos y a una frecuencia similar. Separa los datos que cambian rápido (temporizadores, posiciones) de los que cambian lento (scores, configuración).
Limita la frecuencia de los mensajes
Evita enviar mensajes en cada frame. Agrupa o limita la frecuencia cuando sea posible:
Por ejemplo, si el server controla un temporizador de cuenta regresiva, no es necesario enviar actualizaciones a todos los jugadores cada segundo. Lo mejor es que cada client calcule el paso del tiempo por su cuenta, y que el server emita su estado actual aproximadamente cada 30 segundos, para garantizar la consistencia.
Errores comunes
Olvidar la validación en el estado exclusivo del server
Sin validateBeforeChange, los clients pueden escribir en cualquier componente:
Confiar en los valores proporcionados por el client
Nunca dejes que un client dicte sus propios valores para datos importantes como salud, score o posición:
Enviar mensajes antes de la sincronización del estado
Los clients deben esperar hasta que el estado esté sincronizado antes de interactuar:
Espera a que el server arranque
El server solo está activo si hay al menos un jugador presente en el scene. Si no hay nadie allí en ese momento, el server se apaga después de unos minutos.
Cuando un primer jugador entra en el scene después de un tiempo de inactividad, el server tarda unos segundos en arrancar. El código de tu scene debe estar preparado para tener que esperar a que el server esté en línea. Las solicitudes iniciales al server deberían tener mecanismos de captura y reintento para ofrecer resiliencia.
Ejemplo completo
Un contador multijugador mínimo: haz clic en un botón, el server incrementa un score sincronizado. El server persiste el contador en Storage para que el valor sobreviva a los reinicios del server. Recuerda que el server se apaga cuando no hay jugadores en el scene, así que sin almacenamiento el conteo se restablecería a cero cada vez que el scene quedara vacío de jugadores.
Pruebas locales
La vista previa estándar lo maneja todo. Cuando usas la rama auth-server del SDK, el server local se inicia automáticamente en segundo plano junto con la vista previa del client.
Para probar interacciones multijugador localmente, abre la vista previa en dos ventanas separadas; cada ventana se trata como un jugador distinto. Conecta cada ventana con una dirección diferente. Ambos clients se conectarán a la misma instancia local del server.
Usando Creator Hub, haz clic en el botón Preview una segunda vez, y eso abre una segunda ventana de explorer de Decentraland. Debes conectarte en ambas ventanas con direcciones diferentes. Las mismas sesiones permanecerán abiertas mientras el scene se recarga.
Como alternativa, puedes abrir una segunda ventana de explorer de Decentraland escribiendo lo siguiente en una URL del navegador:
decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&multi-instance=true
Consejos de depuración
Prefija tus logs con
[SERVER]o[CLIENT]para que puedas distinguirlos en la terminal:Verifica la sincronización de componentes en el client registrando los recuentos de entidades:
Depurar en producción
Para ver console.log() la salida de tu server publicado, la dirección de tu wallet debe estar incluida en el array logsPermissions en scene.json:
Sin esto, los logs del server se ocultan en producción, incluso para el propietario del scene.
Transmite en directo los logs del server desde la línea de comandos ejecutando esto en la carpeta de tu proyecto
También puedes especificar manualmente el nombre del world en los logs con:
Al ver logs de un scene en un world de varios scenes o parcelas en Genesis City, pasa también una position para las coordenadas:
Se te pedirá que firmes un mensaje con una de las wallets listadas en logsPermissions para autenticarte. Una vez conectado, verás la console.log() salida del server en tiempo real, lo que es útil para diagnosticar problemas sin necesidad de volver a desplegar.
Ver datos de almacenamiento
Puedes acceder a los datos almacenados por el almacenamiento del scene introduciendo este enlace:
También puedes llegar a esta página a través de Creator Hub. Abre la pestaña Manage , haz clic en los tres puntos junto al lugar donde has publicado contenido y selecciona View server data.
Allí puedes ver una lista de todos los worlds y land donde puedes publicar scenes.
Abre el world o los datos del jugador para ver la información almacenada de cada uno.
Por ejemplo, si un jugador concreto tiene un problema al jugar tu scene, podrías buscar a ese jugador por dirección y ver qué datos están almacenados para entender su situación. Quizá se topó con un caso límite en el que terminó con datos contradictorios. Incluso puedes borrar o editar los datos de ese jugador desde esta página, para devolverlo a un estado estable.
Control de versiones
Cada versión publicada de tu scene obtiene su propio hash ID único, y cada hash se empareja con su propia instancia de server. Esto significa que el código del client y el código del server siempre avanzan juntos; no hay ninguna ventana en la que un client que ejecuta lógica antigua hable con un server que ejecuta lógica nueva (o viceversa).
Cuando publicas una actualización:
Los jugadores que ya están en el scene siguen viendo la versión antigua del scene hasta que se van y vuelven. Sus clients permanecen conectados a la instancia del server que coincide con el hash antiguo.
Los nuevos jugadores que llegan después de la actualización cargan la nueva versión del scene y se conectan a la nueva instancia del server.
Esto garantiza que el estado del client y del server nunca se desincronicen debido a un cambio de esquema o a un componente renombrado. Una actualización nunca puede romper la sesión de un jugador que ya está en tu scene.
La contrapartida es que, durante una breve ventana justo después de un deploy, los jugadores pueden quedar divididos entre dos instancias de server distintas. Un jugador que ya estaba allí y otro que acaba de llegar quizá no se vean ni puedan interactuar a través del scene, aunque estén en el mismo scene, hasta que los jugadores antiguos se vayan y vuelvan a entrar.
💡 Consejo: Los datos almacenados mediante el Storage service (como tablas de clasificación, progreso del jugador o cambios persistentes del entorno) no se borran entre versiones. El almacenamiento se conserva a nivel de ubicación y se comparte entre todas las instancias de server que apuntan al mismo scene, así que las nuevas versiones continúan justo donde la anterior lo dejó.
Migrar desde Colyseus
Si tienes un scene existente construido sobre Colyseus, la tabla siguiente mapea patrones comunes de Colyseus a sus equivalentes en SDK7:
room.send(type, data)
room.send(type, data) — misma API
room.onMessage(type, cb)
room.onMessage(type, cb) — misma API
room.state.players (schema)
syncEntity + componentes personalizados
Serialización JSON
Serialización binaria (automática mediante Schemas)
Aplicación de server separada
Misma base de código — isServer() ramificación
Alojamiento personalizado del server
Integrado: la vista previa ejecuta el server automáticamente
Diferencias clave a tener en cuenta:
Serialización: Colyseus envía diffs JSON; el SDK envía el componente completo en cada cambio. Mantén los componentes pequeños (ver Buenas prácticas de rendimiento).
Modelo de estado: Colyseus usa un árbol de estado mutable con diffing automático. El SDK usa componentes ECS sincronizados mediante
syncEntityy protegidos convalidateBeforeChange.Alojamiento: No hay despliegue de server separado. El server autoritativo se despliega automáticamente junto con el scene.
Última actualización