> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-es/escenas-sdk7/redes/authoritative-servers.md).

# Multiplayer Server

**📔 Nota**: El Multiplayer Server se llamaba anteriormente el **Servidor Autoritativo**. Solo cambió el nombre; la funcionalidad es la misma. La rama del SDK que debes instalar sigue llamándose `auth-server`, consulta [Configuración](#setup).

## Resumen

Decentraland ejecuta las scenes localmente en la máquina de un player. Por defecto, los players pueden verse entre sí e interactuar directamente, pero cada uno interactúa con el entorno de forma independiente. Los cambios en el entorno no se comparten entre players por defecto.

Permitir que todos los players vean una scene como si tuviera el mismo contenido en el mismo estado es extremadamente importante para que los players puedan interactuar de formas más significativas. Sin esto, si un player abre una puerta y entra en una casa, los demás players verán esa puerta todavía cerrada, y el primer player parecerá atravesar directamente la puerta cerrada para los demás players.

El **Multiplayer Server** es un proceso de server sin interfaz que ejecuta el código de tu scene, valida los cambios de estado y difunde el resultado a todos los players conectados. Sigue una arquitectura de server autoritativo: en lugar de confiar en que cada client informe de sus propias acciones, el server actúa como la única fuente de verdad. Esto lo convierte en el enfoque recomendado para sincronizar scenes multiplayer.

El Multiplayer Server es ideal siempre que la equidad sea importante para la mecánica del juego, ya que puedes implementar validaciones anti-cheat elaboradas que se ejecutan en el server. También puedes almacenar claves privadas y otra información sensible en el server, evitando tener que exponerlas directamente al usuario.

Tener un Multiplayer Server también resuelve un problema real: en una configuración peer-to-peer, dos players controlando algo como una plataforma flotante pueden producir resultados contradictorios. Cada client establece la plataforma a una altura diferente, y nadie tiene autoridad para decidir cuál es la correcta. El Multiplayer Server 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 player, logros desbloqueados o cambios en el entorno como puertas abiertas o elementos colocados. Cuando los players regresan, el mundo refleja lo que sucedió antes.

Decentraland hospeda y despliega el server por ti. Publicar tu scene mediante el proceso normal también publica el server sin problemas, sin pasos adicionales ni necesidad de pagar por hosting.

## Configuración

### 1. Instala la versión auth-server del SDK

Las API nativas del Multiplayer Server (`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:

```bash
npm install @dcl/sdk@auth-server
npm install @dcl/js-runtime@auth-server
```

### 2. Configura scene.json

El Multiplayer Server se habilita mediante la marca `"authoritativeMultiplayer": true` en `scene.json`, en el nivel raíz. No necesitas agregarlo manualmente: al usar la rama auth-server del SDK, se agrega automáticamente la primera vez que haces build o preview de la scene. Solo asegúrate de no eliminarlo; sin esta marca, el server nunca se ejecuta y `isServer()` siempre devuelve *false*.

Opcionalmente, también puedes agregar lo siguiente a tu `scene.json` : en el nivel raíz:

```json
{
  "logsPermissions": ["0xYourWalletAddress"]
}
```

Añade `logsPermissions` para listar las direcciones de wallet que pueden ver `console.log()` del server. Los users enumerados pueden ver entonces los logs del server en producción ejecutando el siguiente comando:

`npx sdk-commands sdk-server-logs`

### 3. Ejecuta la vista previa

Usa el comando estándar de preview, no se necesitan pasos adicionales. Al usar la rama auth-server del SDK, la preview inicia automáticamente una versión local del Multiplayer Server en segundo plano.

La sesión local del server no está conectada a la de producción, así que puedes probar cosas libremente sin afectar a los players que estén en tu scene publicada.

## Rama Server / Client

El código de la scene en la `src` carpeta de tu proyecto se ejecuta tanto en el server como en el client. Usa la `isServer()` función para dividir las rutas de ejecución:

```typescript
import { isServer } from "@dcl/sdk/network"
import { initServer } from "./server/server"
import { initClient } from "./client/setup"
import { setupUi } from "./client/ui"

function main() {
  if (isServer()) {
    // Solo servidor: lógica del juego, validación, gestión del estado
    initServer()
    return
  } else {
    // Solo cliente: UI, manejo de input, envío de mensajes
    initClient()
    setupUi()
  }
}
```

El server ejecuta tu scene sin interfaz y sin rendering. Tiene acceso verificado a todas las posiciones de los players, wearables y otros datos a través de `PlayerIdentityData` y es la única autoridad sobre el estado del juego.

## Componentes sincronizados y validación

### Sincronizar Entities con todos los clients

Usa `syncEntity` para difundir cualquier cambio en los componentes indicados de esa Entity:

```typescript
import { isServer, syncEntity } from "@dcl/sdk/network"

if (isServer()) {
  syncEntity(
    entity,
    [Transform.componentId, GameState.componentId],
    /* enumId */ 1
  )
}
```

La sintaxis es idéntica a la que usa la [multiplayer Serverless](/creator/content-creator-es/escenas-sdk7/redes/serverless-multiplayer.md) feature, lo que hace trivial actualizar una scene desde el uso de esta arquitectura al Multiplayer Server. Cuando una scene usa el Multiplayer Server, las actualizaciones de estado ya no se envían entre todos los players; en su lugar, todas las actualizaciones de estado se enrutan y validan a través del server.

{% hint style="warning" %}
**📔 Nota**: Con el Multiplayer Server, el patrón ideal es que solo el server llame a `syncEntity`. De ese modo no necesitas preocuparte por la consistencia del entity-id. Más bien, la Entity se instancia y se comparte desde el server, y todos los clients se sincronizan con esa instancia. Protégerla siempre con `isServer()`. Esto es diferente de [multiplayer Serverless](/creator/content-creator-es/escenas-sdk7/redes/serverless-multiplayer.md), donde cada client llama a `syncEntity` por su cuenta.
{% endhint %}

### Validar cambios

Usa `validateBeforeChange()` para restringir cualquier actualización de estado en un componente específico de una Entity. Te permite ejecutar una función de validación personalizada, y los cambios solo se realizan correctamente cuando se cumple la prueba de validación.

Si la validación devuelve el valor *true*, entonces el cambio se acepta y se propaga a todos los players. Si la validación devuelve el valor *false*false

#### , entonces el cambio se rechaza. Un cambio rechazado no se pasará a otros players y se revertirá para el player que intentó realizarlo.

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 a un` Transform

```typescript
cuando la nueva posición Y sea mayor que 0:
import { engine, Transform } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"

import { Vector3 } from "@dcl/sdk/math"
const entity = engine.addEntity()

if (isServer()) {
  Transform.validateBeforeChange(entity, (value) => {
    // Rechaza cualquier actualización que colocaría a la Entity en Y = 0 o por debajo
    return value.newValue.position.y > 0
  })
}
```

Porque `validateBeforeChange()` solo tiene sentido en el server, siempre protégelo con `isServer()`. En el client, la llamada no hace nada útil.

Puedes usar esto para impedir cambios que vayan en contra de la lógica de tu juego, como mecanismos anti-cheat.

#### Validar proximidad al jugador

También puedes combinar `validateBeforeChange()` con las posiciones de los players verificadas por el server para comprobar que un player esté lo suficientemente cerca de un objeto antes de permitirle interactuar con él. Por ejemplo, cuando un player intenta recoger un objeto cambiando su `que se está escribiendo esté dentro de ciertos parámetros. Por ejemplo, aceptar solo cambios a un`, puedes rechazar el cambio si el objeto está a más de 5 metros del player:

```typescript
import { engine, Transform, PlayerIdentityData } from "@dcl/sdk/ecs"
import { engine, Transform } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"

const pickableEntity = engine.addEntity()
Transform.create(pickableEntity, { position: Vector3.create(8, 1, 8) })

if (isServer()) {
  Transform.validateBeforeChange(pickableEntity, (value) => {
    // Encuentra al player que envió este cambio
    for (const [playerEntity, identity] of engine.getEntitiesWith(
      PlayerIdentityData
    )) {
      if (identity.address.toLowerCase() !== value.senderAddress.toLowerCase())
        continue

      const playerTransform = Transform.getOrNull(playerEntity)
      if (!playerTransform) return false

      // Obtén la posición actual del objeto, antes del cambio
      const objectTransform = Transform.getOrNull(pickableEntity)
      if (!objectTransform) return false

      const distance = Vector3.distance(
        playerTransform.position,
        objectTransform.position
      )

      // Permite el cambio solo si el player está a menos de 5 metros
      return distance <= 5
    }

    // No se encontró al remitente entre los players conectados — rechazar
    return false
  })
}
```

Este patrón es útil como mecanismo anti-cheat: evita que los players puedan extenderse por la scene para coger objetos con los que no deberían poder interactuar.

#### Permitir cambios solo por admins

También puedes validar en función de *quién* está enviando el cambio. Cada valor entrante incluye un `senderAddress` campo con la dirección de wallet del remitente. Usa esto para permitir solo cambios de ciertos players. Por ejemplo, para permitir solo a los admins de la scene modificar un `VideoPlayer` componente:

```typescript
import { engine, VideoPlayer } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { isPreview } from "@dcl/asset-packs/dist/admin-toolkit-ui/fetch-utils"
import { getSceneAdmins } from "@dcl/asset-packs/dist/admin-toolkit-ui/ModerationControl/api"

// Caché de direcciones de wallet de admins, actualizada desde la lista de admins de la scene
let adminAddresses = new Set<string>()

async function updateAdminAddresses() {
  if (isPreview()) return
  try {
    const [error, response] = await getSceneAdmins()
    if (error) {
      console.error("[SERVER] Error al obtener la lista de admins:", error)
      adminAddresses = new Set()
      return
    }
    adminAddresses = new Set(
      (response ?? []).map((admin) => admin.admin.toLowerCase())
    )
    console.log(
      "[SERVER] Caché de direcciones de admins actualizada:",
      Array.from(adminAddresses)
    )
  } catch (error) {
    console.error("[SERVER] Error al actualizar las direcciones de admins:", error)
    adminAddresses = new Set()
  }
}

export async function main() {
  const videoEntity = engine.addEntity()
  VideoPlayer.create(videoEntity, { src: "videos/intro.mp4" })

  if (isServer()) {
    // Llena la caché antes de conectar la validación
    await updateAdminAddresses()

    VideoPlayer.validateBeforeChange(videoEntity, (value) => {
      // Permite siempre los cambios mientras se ejecuta en preview, para facilitar las pruebas locales
      if (isPreview()) return true

      const senderAddress = value.senderAddress.toLowerCase()
      if (!adminAddresses.has(senderAddress)) {
        console.log(
          "[SERVER] Cambio no autorizado de VideoPlayer bloqueado desde:",
          senderAddress
        )
        return false
      }
      return true
    })
  }
}
```

Consulta [Scene Admin](/creator/content-creator-es/scene-editor/operar-en-vivo/scene-admin.md) para más contexto sobre cómo los players se convierten en admins de una scene.

#### Permitir cambios solo desde el server

El caso más estricto es aceptar solo escrituras que provengan del propio server, rechazando cualquier cambio que venga de un client. Este es el patrón recomendado para el estado que debe ser totalmente autoritativo: scores, fase del juego, Entities generadas, etc.

Cada valor entrante incluye un `senderAddress` campo. 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 componentes de una Entity dada. Es una forma cómoda de proteger varios componentes (como `que se está escribiendo esté dentro de ciertos parámetros. Por ejemplo, aceptar solo cambios a un` y `GltfContainer`) en una sola llamada:

```typescript
import { engine, Entity, Transform, GltfContainer } from "@dcl/sdk/ecs"
import { engine, Transform } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

type ComponentWithValidation = {
  validateBeforeChange: (
    entity: Entity,
    cb: (value: { senderAddress: string }) => boolean
  ) => void
}

function protectServerEntity(
  entity: Entity,
  components: ComponentWithValidation[]
) {
  for (const component of components) {
    component.validateBeforeChange(entity, (value) => {
      return value.senderAddress === AUTH_SERVER_PEER_ID
    })
  }
}

if (isServer()) {
  // Después de crear una Entity administrada por el server:
  import { Vector3 } from "@dcl/sdk/math"
  Transform.create(entity, { position: Vector3.create(10, 5, 10) })
  GltfContainer.create(entity, { src: "assets/model.glb" })
  protectServerEntity(entity, [Transform, GltfContainer])
}
```

{% hint style="warning" %}
**📔 Nota**: Llama siempre a `protectServerEntity()` dentro de un `isServer()` bloque. Envuelve `validateBeforeChange()`\`, que solo tiene sentido en el server; llamarla en un client produce errores.
{% endhint %}

#### Componentes personalizados

También puedes aplicar `validateBeforeChange()` en componentes personalizados definidos por la scene.

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

export const GameState = engine.defineComponent("game:State", {
  phase: Schemas.String,
  score: Schemas.Int,
  timeRemaining: Schemas.Int,
})

// Solo el server puede modificar este componente
if (isServer()) {
  GameState.validateBeforeChange((value) => {
    return value.senderAddress === AUTH_SERVER_PEER_ID
  })
}
```

## Mensajes

Los componentes sincronizados son ideales para el estado que todos los players deberían ver continuamente: cosas como posiciones, scores o la fase del juego. Pero no todo encaja en ese modelo. A veces un player 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 events, no estado continuo.

Para eso sirven los messages. Usa `registerMessages()` para comunicación tipada y validada por schema entre los clients y el server. Los messages son fire-and-forget: un client envía uno al server, el server lo procesa y opcionalmente envía uno de vuelta. No crean ningún estado persistente por sí mismos.

### Definir Messages

Define todos los messages en un archivo compartido que importen tanto el server como el client. Así ambos lados siempre están de acuerdo sobre qué messages existen y qué datos transportan. Cada message es un `Schemas.Map` que describe su payload:

```typescript
import { Schemas } from "@dcl/sdk/ecs"
import { registerMessages } from "@dcl/sdk/network"

export const Messages = {
  // Client → Server
  playerReady: Schemas.Map({ displayName: Schemas.String }),
  playerAction: Schemas.Map({ action: Schemas.String, targetId: Schemas.Int }),

  // Server → Client
  gameStarted: Schemas.Map({ roundNumber: Schemas.Int }),
  gameEnded: Schemas.Map({ winnerId: Schemas.String }),
}

export const room = registerMessages(Messages)
```

{% hint style="warning" %}
**📔 Nota**: Llama a `registerMessages()` una vez, en el nivel superior de un módulo compartido, como en el ejemplo anterior. Así se ejecuta cuando el módulo se carga por primera vez, tanto en el server como en el client. No lo llames condicionalmente ni dentro de funciones; ambos lados deben registrar siempre los mismos messages.
{% endhint %}

### Enviar mensajes

Los clients solo pueden enviar messages al server. No existe mensajería directa de client a client. El server puede difundir a todos los clients o dirigirse a players específicos por dirección.

```typescript
import { room } from "./shared/messages"

// Client → Server (difusión, el server la recibe)
room.send("playerReady", { displayName: "Alice" })

// Server → todos los clients
room.send("gameStarted", { roundNumber: 1 })

// Server → un client específico (por dirección de wallet)
room.send("gameEnded", { winnerId: "Alice" }, { to: [playerAddress] })
```

{% hint style="warning" %}
**📔 Nota**: Los messages tienen un tamaño máximo de alrededor de 13 KB. Los messages más grandes se descartan silenciosamente por el transporte, sin producir ningún error. Mantén los payloads pequeños; si necesitas compartir datos más grandes, divídelos en varios messages o usa componentes sincronizados.
{% endhint %}

### Recibir mensajes

```typescript
import { room } from "./shared/messages"

// El Client recibe del server
room.onMessage("gameStarted", (data) => {
  console.log(`¡La ronda ${data.roundNumber} comenzó!`)
})

// El server recibe del client
room.onMessage("playerReady", (data, context) => {
  if (!context) return
  const senderAddress = context.from // dirección de wallet verificada
  console.log(`[Server] ${data.displayName} está listo (${senderAddress})`)
})
```

En el server, cada message recibido incluye un `context` objeto con la dirección de wallet verificada del remitente. Usa esto para saber qué player envió el message (nunca confíes en la identidad declarada por el propio payload).

### Espera a que se sincronice el State antes de enviar

Los clients deben esperar hasta que el estado de la scene se haya sincronizado antes de enviar su primer message, para evitar race conditions al unirse:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { isStateSyncronized } from "@dcl/sdk/network"
import { room } from "./shared/messages"

engine.addSystem(() => {
  if (!isStateSyncronized()) return

  // Ya es seguro enviar messages
  room.send("playerReady", { displayName: "Alice" })
})
```

{% hint style="info" %}
**💡 Consejo**: `isStateSyncronized()` solo te indica que el estado del client está sincronizado; no garantiza que el server haya terminado de inicializarse. Para una comprobación de disponibilidad más robusta, haz que el server difunda un message de "ready" o heartbeat, y que los clients lo esperen antes de enviar messages de gameplay.
{% endhint %}

### Tipos de Schema disponibles

Todos los payloads de messages y los componentes personalizados usan `Schemas` para la serialización binaria. Aquí tienes una referencia rápida de los tipos disponibles:

```typescript
import { Schemas } from "@dcl/sdk/ecs"

// Tipos básicos
Schemas.String // "hello"
Schemas.Int // 42
Schemas.Float // 3.14
Schemas.Boolean // true / false
Schemas.Int64 // Date.now()

// Tipos vectoriales
Schemas.Vector3 // { x: 1, y: 2, z: 3 }
Schemas.Quaternion // { x, y, z, w }

// Tipos complejos
Schemas.Array(Schemas.String) // ["a", "b", "c"]
Schemas.Entity // Referencia a Entity
Schemas.Optional(Schemas.String) // "hello" o undefined
Schemas.Optional(Schemas.Int) // 42 o undefined

// Objetos anidados
Schemas.Map({
  name: Schemas.String,
  health: Schemas.Int,
  position: Schemas.Vector3,
  playerId: Schemas.Optional(Schemas.String),
})
```

{% hint style="warning" %}
**📔 Nota**: Messages *debe* estar definido usando `Schemas.Map(...)`. No puedes enviar objetos JavaScript planos; fallarán en la serialización binaria.
{% endhint %}

## Lectura de posiciones de jugadores por parte del servidor

El servidor puede leer **verificadas** posiciones de jugadores; los clientes no pueden falsificarlas. Esta es la base del anti-cheat basado en posiciones:

```typescript
import { engine, PlayerIdentityData, Transform } from "@dcl/sdk/ecs"

engine.addSystem(() => {
  for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
    const transform = Transform.getOrNull(entity)
    if (!transform) continue

    const address = identity.address
    const position = transform.position
    // Esta posición está verificada por el servidor — nunca confíes en la posición reportada por el cliente
  }
})
```

{% hint style="warning" %}
**📔 Nota**: Usa siempre `PlayerIdentityData` + `que se está escribiendo esté dentro de ciertos parámetros. Por ejemplo, aceptar solo cambios a un` en el servidor para obtener las posiciones de los jugadores. Nunca confíes en los valores reportados por el propio cliente.
{% endhint %}

## Almacenamiento de datos

Conserva los datos entre reinicios del servidor. Storage es **solo para el servidor**, protege siempre las llamadas con `isServer()`. El servidor puede tanto escribir como leer estos datos.

```typescript
import { Storage } from "@dcl/sdk/server"
```

Los datos se pueden almacenar en dos niveles:

* **World**: Usa esto para datos relevantes para todos los jugadores, como tablas de clasificación o cambios persistentes en el entorno.
* **Player**: Usa esto para datos específicos de un jugador, como guardar el progreso o las preferencias de ese jugador.

{% hint style="info" %}
**💡 Consejo**: Storage solo acepta cadenas. Usa `JSON.stringify()` / `JSON.parse()` para objetos y `String()` / `parseInt()` para números.

Durante el desarrollo local, el almacenamiento se escribe en `node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json`.
{% endhint %}

{% hint style="warning" %}
**📔 Nota**: `Storage.set()`, `Storage.player.set()`, y las `delete` variantes devuelven un **boolean**. Nunca lanzan excepciones — en caso de fallo (un error de red, o demasiadas solicitudes concurrentes, ver abajo) registran el error y se resuelven en `false`, lo que significa que el valor fue **no** persistido. Comprueba siempre el resultado: una `false` desechada es un guardado perdido silenciosamente.
{% endhint %}

### Guarda en puntos de control, no en cada cambio

Storage es una persistencia duradera para datos que deben sobrevivir a reinicios y redeploys del servidor. No es un datastore en vivo. Mantén el estado de trabajo del juego en memoria en el servidor. Es más rápido y es el patrón correcto para un servidor. Escribe en Storage solo cuando realmente lo necesites, en puntos de control significativos.

{% hint style="warning" %}
**⚠️ Advertencia**: El runtime del servidor permite un máximo de **40 llamadas al host en vuelo** a la vez, compartidas entre *todo* la escena le pide al runtime hacer — cada solicitud de almacenamiento, `signedFetch`, y otras APIs del runtime cuentan para el mismo límite. Si tu escena dispara solicitudes de almacenamiento más rápido que eso (por ejemplo, un `Storage.set` en cada cambio de puntuación, cada evento o cada tick), las llamadas que superan el límite fallan de inmediato: el SDK registra un `demasiadas llamadas concurrentes al host` error y la `Storage.set` promesa se resuelve en `false`. Si tu código descarta ese booleano, el guardado fallido es invisible — tus datos persistidos terminan desactualizados o perdidos. Comprueba el resultado de cada escritura (consulta la Nota arriba).
{% endhint %}

Buenos momentos para persistir:

* Fin del juego, o el final de una ronda.
* Un jugador se va.
* Un guardado periódico con debounce (por ejemplo, una vez cada 30 segundos), no una vez por frame.

Persiste solo los datos que deben sobrevivir a un reinicio o redeploy. Todo lo demás puede vivir en memoria.

El ejemplo siguiente mantiene las puntuaciones en memoria y las escribe solo en un punto de control, en lugar de en cada punto anotado:

```typescript
import { Storage } from "@dcl/sdk/server"

// El estado de trabajo vive en memoria, actualizado en cada evento
const scores: Record<string, number> = {}

function onPointScored(address: string) {
  scores[address] = (scores[address] ?? 0) + 1
  // Aquí no hay llamada a Storage — solo actualiza la memoria
}

// Persiste solo en un punto de control, como cuando un jugador se va
async function onPlayerLeave(address: string) {
  const saved = await Storage.player.set(address, "score", String(scores[address] ?? 0))
  if (!saved) {
    // La escritura no se persistió — mantén el valor en memoria e inténtalo de nuevo más tarde
    console.error(`Failed to save score for ${address}`)
  }
}
```

### Almacenamiento de World — Compartido entre todos los jugadores

```typescript
import { Storage } from "@dcl/sdk/server"

// Escribe — set() se resuelve en false si el valor NO se persistió
const ok = await Storage.set(
  "leaderboard",
  JSON.stringify([
    { name: "Alice", score: 100 },
    { name: "Bob", score: 85 },
  ])
)
if (!ok) {
  console.error("No se pudo guardar la clasificación — inténtalo de nuevo más tarde")
}

// Leer
const raw = await Storage.get<string>("leaderboard")
const leaderboard = raw ? JSON.parse(raw) : []

// Eliminar
await Storage.delete("leaderboard")
```

También puedes gestionar el almacenamiento de la escena desde la línea de comandos, usando `npx sdk-commands storage scene`:

```bash
# Establecer un valor
npx sdk-commands storage scene set high_score --value 100

# Obtener un valor
npx sdk-commands storage scene get high_score

# Eliminar un valor
npx sdk-commands storage scene delete high_score

# Eliminar todos los datos de almacenamiento de la escena
npx sdk-commands storage scene clear --confirm
```

### Almacenamiento de Player — Por dirección de Wallet

```typescript
import { Storage } from "@dcl/sdk/server"

// Escribe — set() se resuelve en false si el valor NO se persistió
const ok = await Storage.player.set(
  playerAddress,
  "progress",
  JSON.stringify({
    level: 5,
    coins: 250,
  })
)
if (!ok) {
  console.error(`Failed to save progress for ${playerAddress} — retry later`)
}

// Leer
const saved = await Storage.player.get<string>(playerAddress, "progress")
const progress = saved ? JSON.parse(saved) : { level: 1, coins: 0 }

// Eliminar
await Storage.player.delete(playerAddress, "progress")
```

También puedes gestionar el almacenamiento de jugadores desde la línea de comandos, usando `npx sdk-commands storage player`:

```bash
# Establecer un valor para un jugador específico
npx sdk-commands storage player set level --value 10 --address 0x1234...

# Obtener un valor para un jugador específico
npx sdk-commands storage player get level --address 0x1234...

# Eliminar un valor para un jugador específico
npx sdk-commands storage player delete level --address 0x1234...

# Eliminar todos los datos de un jugador específico
npx sdk-commands storage player clear --address 0x1234... --confirm

# Eliminar todos los datos de jugadores (todos los jugadores)
npx sdk-commands storage player clear --confirm
```

### Acceder a datos almacenados

Puedes ver y editar los datos almacenados en vivo en tu servidor a través de la UI de storage, entrando en este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes llegar a esta página a través de Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **View Storage**.

Allí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Abre tu escena y luego la **Scene** o **Player** pestaña.

En la **Scene** pestaña verás una lista de todas las variables almacenadas. Desde aquí puedes editar o eliminar cualquiera de estas variables haciendo clic en el icono del lápiz o de la papelera.

![Activate stream](/files/57b7c31e0f012c4735f28bc4204516aa3ec879bc)

En la **Player** pestaña verás una lista de todos los jugadores que tienen datos almacenados en tu servidor. Puedes buscarlos por dirección o nombre, y luego ver todos sus datos asociados. También puedes editar o eliminar estos datos haciendo clic en el icono del lápiz o de la papelera.

### Cambiar la estructura de datos

Los datos almacenados en producción son **no se borran cuando publicas una nueva versión de tu escena**. Esto es ideal para tablas de clasificación, progreso de jugadores y cambios persistentes en el entorno que los jugadores esperan que permanezcan más allá de pequeñas actualizaciones de tu escena.

La otra cara es que los datos que están 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 maneras sutiles. Un campo que renombraste faltará. Un campo que antes era una cadena y ahora es un objeto lanzará un error cuando intentes acceder a una propiedad en él. Un jugador que no ha iniciado sesión durante meses puede cargar datos que preceden a una estructura que tu código ya no sabe manejar.

{% hint style="warning" %}
**📔 Nota**: Los cambios de Schema no solo afectan a la primera lectura después de un deploy. Los datos almacenados viven hasta que se sobrescriben o se eliminan, así que un valor en formato antiguo puede aparecer en cualquier momento, a menudo de un jugador que regresa y que habías olvidado.
{% endhint %}

#### Buenas prácticas

* *Analiza siempre de forma defensiva*. Trata todo lo que sale de storage como entrada no confiable, aunque lo hayas escrito tú. Encierra `JSON.parse()` en un `try/catch`, comprueba que los campos existan antes de leerlos y ten listo un valor predeterminado razonable cuando no existan:

  ```typescript
  import { Storage } from "@dcl/sdk/server"

  const raw = await Storage.player.get<string>(playerAddress, "progress")
  let progress = { level: 1, coins: 0 }
  if (raw) {
    try {
      const parsed = JSON.parse(raw)
      progress = {
        level: typeof parsed.level === "number" ? parsed.level : 1,
        coins: typeof parsed.coins === "number" ? parsed.coins : 0,
      }
    } catch {
      // Datos antiguos o corruptos — vuelve a los valores predeterminados
    }
  }
  ```
* *Añade campos, no los renombres ni los elimines*. El cambio de schema más seguro es uno aditivo: introduce un nuevo campo con un valor predeterminado y deja los campos existentes intactos. Los datos antiguos simplemente no tendrán el nuevo campo, lo que tu análisis defensivo ya maneja. Renombrar un campo obliga a que todos los registros antiguos fallen.
* *Versiona tus objetos almacenados*. Incluye un `version` desde el primer día. Cuando leas datos, bifurca según la versión y migra las formas antiguas a la actual antes de usarlas. Esto mantiene el resto de tu código funcionando con una única forma actual:

  ```typescript
  type ProgressV2 = { version: 2; level: number; coins: number; xp: number }

  function migrate(raw: any): ProgressV2 {
    const version = raw?.version ?? 1
    if (version === 1) {
      // v1 no tenía el campo xp — asígnale un valor predeterminado
      return {
        version: 2,
        level: raw.level ?? 1,
        coins: raw.coins ?? 0,
        xp: 0,
      }
    }
    return raw as ProgressV2
  }
  ```
* *Vuelve a escribir el valor migrado*. Una vez que hayas actualizado un registro en memoria, guárdalo de nuevo para que la siguiente lectura ya esté en el nuevo formato. Con el tiempo, esto vacía el conjunto de registros con forma antigua sin necesidad de un script de migración único.
* *Para cambios incompatibles, usa una nueva clave*. Si la nueva estructura es realmente incompatible y no vale la pena migrar, escribe en una nueva clave de storage (por ejemplo `progress_v2`) e ignora la anterior. La clave antigua permanece sin problemas en storage y evitas cualquier ruta de lectura que tenga que interpretarla. Puedes limpiar las claves antiguas más tarde a través de la [storage UI](https://decentraland.org/storage) o la `npx sdk-commands storage` comandos.
* *Prueba con datos reales de producción*. Antes de desplegar un cambio estructural, extrae algunos registros reales desde la storage UI 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 vía de escape*. Ten en cuenta que puedes editar o eliminar registros individuales desde la storage UI o mediante `npx sdk-commands storage`. Si un solo jugador queda atascado en un mal estado después de un cambio de schema, puedes arreglar su registro directamente sin redeploy.

## Variables de entorno

Configura tu escena sin codificar valores directamente en el código. Las variables de entorno son útiles para datos sensibles, y también para banderas de funciones o parámetros que se pueden cambiar fácilmente sin volver a publicar tu escena.

Las variables de entorno son **solo para el servidor**. Protégelas con `isServer()`. El servidor puede leer variables de entorno, pero no cambiar sus valores.

`EnvVar.get()` devuelve un `Promise<string>` y se resuelve en una cadena vacía cuando la variable no está definida, así que proporciona siempre un valor alternativo para los valores faltantes:

```typescript
import { EnvVar } from "@dcl/sdk/server"
import { isServer } from "@dcl/sdk/network"

export async function main() {
  if (!isServer()) return

  const maxPlayers = parseInt((await EnvVar.get("MAX_PLAYERS")) || "4")
  const gameDuration = parseInt((await EnvVar.get("GAME_DURATION")) || "300")
  const debugMode = ((await EnvVar.get("DEBUG")) || "false") === "true"
}
```

### 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 de la escena pública.

Puedes almacenar claves privadas en el almacenamiento del servidor y hacer que solo el servidor las lea con `isServer()`. Así, 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 `.env` archivo .env en la raíz de tu proyecto:

```
MAX_PLAYERS=8
GAME_DURATION=300
DEBUG=true
```

Importante: Añade `.env` a tu `.gitignore`, para que estos valores potencialmente sensibles nunca se suban a los servidores de contenido públicos.

### Cambiar variables de entorno

La forma más fácil de cambiar los valores de tus variables de entorno es mediante la storage UI.

Puedes acceder a los datos almacenados por el storage de la escena entrando en este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes llegar a esta página a través de Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **View Storage**.

Allí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Abre tu escena y luego la **Environment** pestaña. Deberías ver todas las variables de entorno del proyecto.

![Activate stream](/files/f93206a7960eef16efb0b018e0f9e3883f9d2264)

Ten en cuenta que no puedes leer los valores de ninguna de estas variables de entorno (eso es para proteger los datos sensibles), pero puedes eliminar o sobrescribir cualquiera de ellas. Simplemente haz clic en el icono del lápiz o de la papelera.

También puedes gestionar las variables de entorno desde la línea de comandos, usando `npx sdk-commands storage env`:

```bash
# Establecer una variable
npx sdk-commands storage env set MAX_PLAYERS --value 8

# Eliminar una variable
npx sdk-commands storage env delete OLD_VAR

# Eliminar todas las variables de entorno
npx sdk-commands storage env clear --confirm
```

También puedes apuntar a un entorno específico con la `--target` bandera:

```bash
# Desplegar en staging
npx sdk-commands storage env set MY_KEY --value my_value --target https://storage.decentraland.zone

# Desplegar en un servidor de desarrollo local
npx sdk-commands storage env set MY_KEY --value my_value --target http://localhost:8000
```

Las variables de entorno desplegadas tienen prioridad sobre `.env` valores.

## Estructura recomendada del proyecto

Separar el código del servidor, del client y del compartido mantiene el código legible a medida que crece:

```
src/
├── index.ts              # Punto de entrada — rama isServer()
├── client/
│   ├── setup.ts          # Controladores de entrada, emisores de mensajes
│   └── ui.tsx            # UI de React ECS (lee el estado sincronizado)
├── server/
│   ├── server.ts         # Bucle del juego, controladores de mensajes, mutaciones de estado
│   └── gameState.ts      # Funciones auxiliares para el estado del servidor
└── shared/
    ├── schemas.ts        # Definiciones de Component + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambos lados
```

{% hint style="info" %}
**💡 Consejo**: Mantén todas las `registerMessages()` llamadas y las definiciones de Component personalizadas en `shared/`. Tanto el servidor como el client importan desde allí, asegurando que siempre estén de acuerdo en los esquemas de mensajes.
{% endhint %}

## Buenas prácticas de rendimiento

Cada cambio de Component envía los *completos* datos del Component 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 Components separados, según la frecuencia de cambio.

### ❌ Evita components monolíticos

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"

// MAL — al cambiar la puntuación también se envía el array de posiciones
const GameState = engine.defineComponent("GameState", {
  playerAScore: Schemas.Int,
  playerBScore: Schemas.Int,
  timer: Schemas.Int,
  playerPositions: Schemas.Array(Schemas.Vector3), // gran carga útil
})
```

### ✅ Prefiere components atómicos

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"

// BIEN — cada actualización es pequeña e independiente
const PlayerScore = engine.defineComponent("PlayerScore", {
  playerA: Schemas.Int,
  playerB: Schemas.Int,
})

const GameTimer = engine.defineComponent("GameTimer", {
  secondsLeft: Schemas.Int,
})
```

*Regla general*: agrupa los campos que cambian juntos y con una frecuencia similar. Separa los datos que cambian rápido (temporizadores, posiciones) de los datos que cambian lentamente (puntuaciones, configuración).

### Limita la frecuencia de los mensajes frecuentes

Evita enviar mensajes en cada frame. Agrupa o limita la frecuencia cuando sea posible:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { room } from "./shared/messages"

let lastSend = 0
engine.addSystem((dt) => {
  lastSend += dt
  if (lastSend > 0.1) {
    // cada 100 ms
    room.send("position", transform.position)
    lastSend = 0
  }
})
```

Por ejemplo, si el servidor 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 servidor difunda su estado actual cada 30 segundos más o menos para garantizar la coherencia.

## Errores comunes

### Olvidar la validación en el estado solo del servidor

Sin `validateBeforeChange`, los clientes pueden escribir en cualquier Component:

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { isServer } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"

// ❌ MAL — los clients pueden hacer trampa
const Score = engine.defineComponent("Score", { value: Schemas.Int })

// ✅ BIEN — solo server
if (isServer()) {
  Score.validateBeforeChange((v) => v.senderAddress === AUTH_SERVER_PEER_ID)
}
```

### Confiar en valores proporcionados por el client

Nunca dejes que un client dicte sus propios valores para datos importantes como health, score o position:

```typescript
import { room } from "./shared/messages"

// ❌ MAL
room.onMessage("setHealth", (data) => {
  player.health = data.health // ¡el client controla el valor!
})

// ✅ BIEN — el server calcula el resultado
room.onMessage("takeDamage", (data) => {
  const damage = calculateDamage(data.source)
  player.health = Math.max(0, player.health - damage)
})
```

### Enviar mensajes antes de la sync del state

Los clients deben esperar hasta que el state esté sincronizado antes de interactuar:

```typescript
import { engine } from "@dcl/sdk/ecs"
import { isStateSyncronized } from "@dcl/sdk/network"

engine.addSystem(() => {
  if (!isStateSyncronized()) return
  // seguro enviar mensajes
})
```

### Espera a que el server se inicie

El server solo está activo si hay al menos un player presente en la scene. Si no hay nadie allí en ese momento, el server se apaga después de unos minutos.

Cuando llega el primer player a la scene después de un tiempo de inactividad, el server tarda unos segundos en iniciarse. 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 catch y retry para ofrecer resiliencia.

## Ejemplo completo

Un contador multijugador mínimo: haz clic en un Button y el server incrementa una puntuación sincronizada. 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 players en la scene, así que sin almacenamiento el conteo se restablecería a cero cada vez que la scene quedara vacía de players.

```typescript
import { engine, Schemas } from "@dcl/sdk/ecs"
import { registerMessages, isServer, syncEntity } from "@dcl/sdk/network"
import { AUTH_SERVER_PEER_ID } from "@dcl/sdk/network/message-bus-sync"
import { pointerEventsSystem } from "@dcl/sdk/ecs"
import { Storage } from "@dcl/sdk/server"

// 1. Define messages (compartido)
const Messages = {
  increment: Schemas.Map({}),
  stateUpdate: Schemas.Map({
    count: Schemas.Int,
    lastPlayer: Schemas.String,
  }),
}

// 2. Define un Component solo para server (compartido)
const Counter = engine.defineComponent("Counter", {
  value: Schemas.Int,
  lastPlayer: Schemas.String,
})

// 3. Create the room
const room = registerMessages(Messages)

export async function main() {
  if (isServer()) {
    // === SERVER ===

    // Solo el server puede modificar este componente
    Counter.validateBeforeChange(
      (v) => v.senderAddress === AUTH_SERVER_PEER_ID
    )

    // Restaura el contador desde Storage en caso de que el server se haya reiniciado
    const savedCount = await Storage.get<string>("counter")
    const savedPlayer = await Storage.get<string>("lastPlayer")
    const initialCount = savedCount ? parseInt(savedCount) : 0
    const initialPlayer = savedPlayer ?? "none"

    const counterEntity = engine.addEntity()
    syncEntity(counterEntity, [Counter.componentId], 1)
    Counter.create(counterEntity, {
      value: initialCount,
      lastPlayer: initialPlayer,
    })

    room.onMessage("increment", async (_data, context) => {
      if (!context) return
      const counter = Counter.getMutable(counterEntity)
      counter.value += 1
      counter.lastPlayer = context.from

      // Persistir en Storage para que el valor sobreviva a los reinicios del server
      await Storage.set("counter", String(counter.value))
      await Storage.set("lastPlayer", counter.lastPlayer)

      room.send("stateUpdate", {
        count: counter.value,
        lastPlayer: context.from,
      })
    })
  } else {
    // === CLIENT ===
    const button = engine.addEntity()
    // ... añade Transform, MeshRenderer, etc.

    pointerEventsSystem.onPointerDown(button, () => {
      room.send("increment", {})
    })

    room.onMessage("stateUpdate", (data) => {
      console.log(`Count: ${data.count} (último clic de ${data.lastPlayer})`)
    })
  }
}
```

## Probando localmente

La Preview estándar maneja todo. Cuando usas la rama auth-server del SDK, el server local se inicia automáticamente en segundo plano junto con la preview del client.

Para probar interacciones multijugador localmente, abre la preview en dos ventanas separadas; cada ventana se trata como un player 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 Decentraland explorer. Debes conectar ambas ventanas con direcciones diferentes. Las mismas sesiones permanecerán abiertas mientras la scene se recarga.

Como alternativa, puedes abrir una segunda ventana de Decentraland explorer escribiendo lo siguiente en una URL del browser:

> `decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&multi-instance=true`

### Consejos de debugging

* *Prefija tus logs* con `[SERVER]` o `[CLIENT]` para poder distinguirlos en la terminal:

  ```typescript
  import { isServer } from "@dcl/sdk/network"

  if (isServer()) {
    console.log("[SERVER] Starting...")
  } else {
    console.log("[CLIENT] Starting...")
  }
  ```
* *Verifica la sync del Component* en el client registrando el conteo de entities:

  ```typescript
  import { engine } from "@dcl/sdk/ecs"

  engine.addSystem(() => {
    const entities = Array.from(engine.getEntitiesWith(MyComponent))
    console.log("[CLIENT] Synced entities:", entities.length)
  })
  ```

## Debug en producción

Para ver `console.log()` la salida de tu server publicado, tu dirección de wallet debe estar incluida en el `logsPermissions` array en `scene.json`:

```json
{
  "logsPermissions": ["0xYourWalletAddress"]
}
```

Sin esto, los logs del server se ocultan en producción, incluso para el owner de la scene.

Transmite los logs en vivo del server desde la línea de comandos ejecutando esto en la carpeta de tu project

```bash
npx sdk-commands sdk-server-logs
```

También puedes especificar manualmente el nombre del world para los logs con:

```bash
npx sdk-commands sdk-server-logs --world WORLD_NAME.dcl.eth
```

Cuando veas logs de una scene en un world de múltiples scenes o de parcels en Genesis City, también pasa un `position` para las coordinates:

```bash
npx sdk-commands sdk-server-logs --world WORLD_NAME.dcl.eth --position=x,y
```

Se te pedirá que firmes un mensaje con una de las wallets listadas en `logsPermissions` para autenticarte. Una vez conectado, verás la salida del lado del server en tiempo real, lo cual es útil para diagnosticar problemas sin necesidad de redeploy. `console.log()` Ver datos de storage

### Abre el world o los datos del player para ver la info almacenada para cada uno.

Puedes acceder a los datos almacenados por el storage de la escena entrando en este enlace:

[decentraland.org/storage](https://decentraland.org/storage)

También puedes llegar a esta página a través de Creator Hub. Abre la **Manage** pestaña, haz clic en los tres puntos junto a un lugar donde hayas publicado contenido y selecciona **View Storage**.

Allí puedes ver una lista de todos los Worlds y LAND donde puedes publicar escenas.

Por ejemplo, si un player en particular tiene un problema al jugar tu scene, podrías buscar a ese player por address y ver qué datos están almacenados para él para entender su situación. Quizás se encontró con un caso límite en el que terminó con datos contradictorios. Incluso puedes borrar o editar los datos de ese player desde esta página para devolverlo a un estado estable.

Por ejemplo, si un player en particular tiene un problema al jugar tu scene, podrías buscar a ese player por address y ver qué datos están almacenados para él para entender su situación. Quizás se encontró con un caso límite en el que terminó con datos contradictorios. Incluso puedes borrar o editar los datos de ese player 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 existe una ventana en la que un client ejecutando lógica antigua hable con un server ejecutando lógica nueva (o viceversa).

Cuando publicas una actualización:

* *Los players que ya están en la scene* siguen viendo la versión antigua de la scene hasta que se van y vuelven. Sus clients permanecen conectados a la instancia de server que coincide con el hash antiguo.
* *Los nuevos players que llegan* después de la actualización cargan la nueva versión de la scene y se conectan a la nueva instancia de server.

Esto garantiza que el state del client y del server nunca se desincronicen debido a un cambio de schema o a un Component renombrado. Una actualización nunca puede romper la session de un player que ya está en tu scene.

La desventaja es que, durante una breve ventana justo después de un deploy, los players pueden quedar repartidos entre dos instancias de server diferentes. Un player que ya estaba allí y un player que acaba de llegar pueden no verse entre sí ni poder interactuar a través de la scene, aunque estén en la misma scene, hasta que los players más antiguos se vayan y vuelvan a entrar.

{% hint style="info" %}
**💡 Consejo**: Los datos almacenados a través del [Storage](#data-storage) service (como tablas de clasificación, progreso del player o cambios persistentes del entorno) se *no* borran entre versiones. Storage se conserva a nivel de ubicación y se comparte entre todas las instancias de server que apuntan a la misma scene, así que las nuevas versiones continúan justo donde la anterior lo dejó.
{% endhint %}

## Migrando desde Colyseus

Si ya tienes una scene existente construida sobre Colyseus, la tabla de abajo mapea patrones comunes de Colyseus a sus equivalentes en SDK7:

| Colyseus                        | Multiplayer Server de 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` + custom components                        |
| JSON serialization              | Binary serialization (automático vía `Schemas`)         |
| Aplicación de server separada   | Misma base de código — `isServer()` branching           |
| Hosting de server personalizado | Integrado: la preview ejecuta el server automáticamente |

Diferencias clave a tener en cuenta:

* *Serialización*: Colyseus envía diffs JSON; el SDK envía el Component completo en cada cambio. Mantén los Components pequeños (ver [Buenas prácticas de rendimiento](#performance-best-practices)).
* *Modelo de state*: Colyseus usa un árbol de state mutable con diffing automático. El SDK usa Components ECS sincronizados vía `syncEntity` y protegidos con `validateBeforeChange`.
* *Hosting*: No hay un despliegue de server separado. El Multiplayer Server se despliega automáticamente junto con la scene.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/creator/content-creator-es/escenas-sdk7/redes/authoritative-servers.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
