> 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/scenes-sdk7/networking/authoritative-servers.md).

# Servidores autoritativos

## 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:

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

### 2. Configura scene.json

Opcionalmente añade lo siguiente a tu `scene.json` a nivel raíz:

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

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:

```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 en el Server: lógica del juego, validación, gestión de estado
    initServer()
    return
  } else {
    // Solo en el Client: UI, manejo de input, envío de mensajes
    initClient()
    setupUi()
  }
}
```

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:

```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 utilizada por la [función de multijugador serverless](/creator/content-creator-es/scenes-sdk7/networking/serverless-multiplayer.md) 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.

{% hint style="warning" %}
**📔 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](/creator/content-creator-es/scenes-sdk7/networking/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 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:

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

const entity = engine.addEntity()
Transform.create(entity, { position: Vector3.create(10, 2, 10) })

if (isServer()) {
  Transform.validateBeforeChange(entity, (value) => {
    // Rechaza cualquier actualización que colocaría la entity en Y = 0 o por debajo
    return value.newValue.position.y > 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:

```typescript
import { engine, Transform, PlayerIdentityData } from "@dcl/sdk/ecs"
import { Vector3 } from "@dcl/sdk/math"
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 jugador 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 jugador está dentro de 5 metros
      return distance <= 5
    }

    // El remitente no se encontró entre los jugadores conectados — rechazar
    return false
  })
}
```

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:

```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"

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

if (isServer()) {
  // Caché de direcciones de wallet de admin, 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 fetching admin list:", error)
        adminAddresses = new Set()
        return
      }
      adminAddresses = new Set(
        (response ?? []).map((admin) => admin.admin.toLowerCase())
      )
      console.log(
        "[SERVER] Updated admin addresses cache:",
        Array.from(adminAddresses)
      )
    } catch (error) {
      console.error("[SERVER] Error updating admin addresses:", error)
      adminAddresses = new Set()
    }
  }

  // Rellena la caché antes de conectar la validación
  await updateAdminAddresses()

  VideoPlayer.validateBeforeChange(videoEntity, (value) => {
    // Permite siempre los cambios mientras se ejecuta en preview, así las pruebas locales son más fáciles
    if (isPreview()) return true

    const senderAddress = value.senderAddress.toLowerCase()
    if (!adminAddresses.has(senderAddress)) {
      console.log(
        "[SERVER] Unauthorized VideoPlayer change blocked from:",
        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 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:

```typescript
import { engine, Entity, Transform, GltfContainer } from "@dcl/sdk/ecs"
import { Vector3 } from "@dcl/sdk/math"
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 gestionada por el server:
  const entity = engine.addEntity()
  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 bloque `isServer()` . Envuelve `validateBeforeChange()`, que solo tiene sentido en el server — llamarlo en un client produce errores.
{% endhint %}

#### Components personalizados

También puedes aplicar `validateBeforeChange()` en components 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 component
if (isServer()) {
  GameState.validateBeforeChange((value) => {
    return value.senderAddress === AUTH_SERVER_PEER_ID
  })
}
```

## 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:

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

### 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.

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

// Client → Server (broadcast, el server lo 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] })
```

### Recibir mensajes

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

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

// El server recibe desde el 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 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:

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

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

  // Ahora ya es seguro enviar mensajes
  room.send("playerReady", { displayName: "Alice" })
})
```

### 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:

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

// Tipos básicos
Schemas.String // "hello"
Schemas.Int // 42
Schemas.Float // 3.14
Schemas.Bool // 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**: Los Messages *deben* definirse usando `Schemas.Map(...)`. No puedes enviar objetos JavaScript simples, fallarán en la serialización binaria.
{% endhint %}

## 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:

```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 server — nunca confíes en la posición reportada por el client
  }
})
```

{% hint style="warning" %}
**📔 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.
{% endhint %}

## 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.

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

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.

{% hint style="info" %}
**💡 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`.
{% endhint %}

### World Storage — Compartido entre todos los jugadores

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

// Escribir
await Storage.set(
  "leaderboard",
  JSON.stringify([
    { name: "Alice", score: 100 },
    { name: "Bob", score: 85 },
  ])
)

// 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 scene mediante 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 scene
npx sdk-commands storage scene clear --confirm
```

### Player Storage — Por dirección de wallet

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

// Escribir
await Storage.player.set(
  playerAddress,
  "progress",
  JSON.stringify({
    level: 5,
    coins: 250,
  })
)

// 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 mediante 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 server mediante 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 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.

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

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.

{% hint style="warning" %}
**📔 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.
{% endhint %}

#### 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 un `try/catch`, comprueba que los campos existan antes de leerlos y ten preparado 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 — recurrir a los valores predeterminados
    }
  }
  ```
* *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 `version` desde 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:

  ```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 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](https://decentraland.org/storage) o los `npx sdk-commands storage` comandos.
* *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:

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

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 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:

```
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 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:

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

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.

![Activate stream](/files/f93206a7960eef16efb0b018e0f9e3883f9d2264)

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`:

```bash
# Definir 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 dirigirlo a un entorno específico con la bandera `--target` :

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

# Desplegar en un server 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 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:

```
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 server
└── shared/
    ├── schemas.ts        # Definiciones de componentes + validateBeforeChange
    └── messages.ts       # registerMessages() — importado por ambos lados
```

{% hint style="info" %}
**💡 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.
{% endhint %}

## 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

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

// MAL — cambiar el score también 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), // carga útil grande
})
```

### ✅ Prefiere componentes 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 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:

```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 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:

```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 trampas
const Score = engine.defineComponent("Score", { value: Schemas.Int })

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

### 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:

```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 sincronización del estado

Los clients deben esperar hasta que el estado 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 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.

```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 mensajes (compartido)
const Messages = {
  increment: Schemas.Map({}),
  stateUpdate: Schemas.Map({
    count: Schemas.Int,
    lastPlayer: Schemas.String,
  }),
}

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

// 3. Crea la room
const room = registerMessages(Messages)

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

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

    // Restaura el contador desde el almacenamiento por si el server se reinició
    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 almacenamiento 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} (last click by ${data.lastPlayer})`)
    })
  }
}
```

## 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:

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

  if (isServer()) {
    console.log("[SERVER] Starting...")
  } else {
    console.log("[CLIENT] Starting...")
  }
  ```
* *Verifica la sincronización de componentes* en el client registrando los recuentos de entidades:

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

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

## 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`:

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

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

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

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

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

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:

```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 `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:

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

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.

{% hint style="info" %}
**💡 Consejo**: Los datos almacenados mediante el [Storage](#data-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ó.
{% endhint %}

## Migrar desde Colyseus

Si tienes un scene existente construido sobre Colyseus, la tabla siguiente mapea patrones comunes de Colyseus a sus equivalentes en SDK7:

| Colyseus                             | Servidor autoritativo 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` + 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](#performance-best-practices)).
* *Modelo de estado*: Colyseus usa un árbol de estado mutable con diffing automático. El SDK usa componentes ECS sincronizados mediante `syncEntity` y protegidos con `validateBeforeChange`.
* *Alojamiento*: No hay despliegue de server separado. El server autoritativo se despliega automáticamente junto con el 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:

```
GET https://docs.decentraland.org/creator/content-creator-es/scenes-sdk7/networking/authoritative-servers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
