# Dominios personalizados, enrutado por rutas, subdominios de juego y Domain Connect

Un servicio siempre es accesible en una dirección gratuita de la plataforma. Esta guía explica cómo hacer que responda en un **nombre propio**. Cuáles de estas opciones tiene tu servicio depende de su producto, así que consulta antes sus [capacidades](/es/concepts/capabilities): `custom_domain` y `external_access` aparecen cuando las admite. Los ejemplos usan el ayudante de [Usar la API de Zaroz](/es/guides/api/overview).

| Quieres | Usa |
|---|---|
| `www.example.com` delante de una web o API | [Dominio personalizado](#dominios-personalizados) |
| Que `example.com/api` llegue a un servicio distinto de `example.com` | [Enrutado por rutas](#enrutado-por-rutas) |
| `play.tujuego.zaroz.cloud` para un servidor de juegos | [Subdominio de juego](#subdominios-de-juego) |
| Que tu proveedor de DNS cree los registros por ti | [Domain Connect](#domain-connect) |

:::note
Los dominios que necesitan certificado usan TLS automático, así que el nombre debe apuntar ya a Zaroz en el DNS antes de que se pueda emitir. Un dominio `pending` es normal mientras esperas al DNS; no es un error.
:::

## Dominios personalizados

### Demuestra que el dominio es tuyo

Salvo que el nombre sea una sola etiqueta bajo un dominio padre que Zaroz mismo registró para ti, la organización debe demostrar antes que controla el dominio con un registro DNS TXT: consulta [Verificar un dominio](/es/guides/organizations/members#verificar-un-dominio). Los nombres dentro de la zona DNS de Zaroz se rechazan para siempre, y un nombre que ya tiene otra organización se rechaza con la misma respuesta que uno que no has demostrado, de modo que esta API no sirve para averiguar quién tiene qué.

### Comprobar, asociar, esperar, desasociar

Pregunta primero si un nombre se podría asociar; no cambia nada.

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/custom-domains/availability?hostname=www.example.com" -H "$AUTH"
# {"status": "available"}     o taken | invalid | platform_zone | not_verified

curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/custom-domains" -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"hostname": "www.example.com"}'
# {"id": "...", "hostname": "www.example.com", "status": "pending", "url": "https://www.example.com"}

curl -sS "$ZAROZ_API/service-instances/$SERVICE/custom-domains" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/service-instances/$SERVICE/custom-domains/$ROUTE" -H "$AUTH"
```

**TypeScript**

```ts
import { call } from "./zaroz";

const avail = await call("GET", `/service-instances/${SERVICE}/custom-domains/availability?hostname=www.example.com`);
if (avail.body.status !== "available") throw new Error(avail.body.status);

await call("POST", `/service-instances/${SERVICE}/custom-domains`, { hostname: "www.example.com" });

// pending hasta que el DNS apunte aquí y se emita el certificado
for (let i = 0; i < 60; i++) {
  const { body } = await call("GET", `/service-instances/${SERVICE}/custom-domains`);
  const mine = body.domains.find((d: any) => d.hostname === "www.example.com");
  console.log(mine.status);
  if (mine.status !== "pending") break;
  await new Promise((r) => setTimeout(r, 30_000));
}
```

**Python**

```python
import time
from zaroz import call

_, avail = call("GET", f"/service-instances/{SERVICE}/custom-domains/availability?hostname=www.example.com")
assert avail["status"] == "available", avail["status"]

call("POST", f"/service-instances/{SERVICE}/custom-domains", {"hostname": "www.example.com"})

for _ in range(60):  # pending hasta que el DNS apunte aquí y se emita el certificado
    _, body = call("GET", f"/service-instances/{SERVICE}/custom-domains")
    mine = next(d for d in body["domains"] if d["hostname"] == "www.example.com")
    print(mine["status"])
    if mine["status"] != "pending":
        break
    time.sleep(30)
```

- Crea un **CNAME** (`www.example.com` hacia el host de la plataforma que el listado devuelve como `platform_host`) en tu proveedor de DNS. Un dominio desnudo (`example.com`) no puede tener CNAME: usa el registro ALIAS, ANAME o de aplanado de CNAME de tu proveedor hacia el mismo destino.
- `status` pasa de `pending` a `active` (ruta y certificado en marcha) o `failed` (normalmente el certificado: no se puede emitir hasta que el DNS apunte aquí; corrige el registro y se reintenta).
- Asociar es idempotente: un nombre ya asociado a este servicio se vuelve a aplicar y se devuelve. Un servicio admite un número limitado de dominios (`limit` en el listado), y uno que no publica HTTP no puede tener ninguno.
- Desasociar elimina la ruta, el certificado y tu reclamación del nombre, así que puedes volver a asociarlo más tarde. El host gratuito de la plataforma no se puede desasociar.

### Dominios en un endpoint HTTPS

Un Workload publica webs y APIs como **endpoints HTTPS** (consulta [Puertos públicos y endpoints](/es/units/endpoints) y [Añadir un puerto público](/es/guides/containers-public-port)). Sus dominios se gestionan en el endpoint:

**curl**

```bash
E=/projects/$PROJECT/service-instances/$SERVICE/endpoints
curl -sS -X POST "$ZAROZ_API$E/$ENDPOINT/domains" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"domain": "www.example.com"}'
# {"id": "<id de ruta>", "domain": "www.example.com", "status": "pending",
#  "dns": {"record_type": "CNAME", "name": "www.example.com", "target": "<host gratuito>"}}
curl -sS -X DELETE "$ZAROZ_API$E/$ENDPOINT/domains/$ROUTE" -H "$AUTH"
```

**TypeScript**

```ts
const E = `/projects/${PROJECT}/service-instances/${SERVICE}/endpoints/${ENDPOINT}`;
const d = await call("POST", `${E}/domains`, { domain: "www.example.com" });
console.log(d.body.dns); // crea exactamente este registro en tu proveedor de DNS
```

**Python**

```python
E = f"/projects/{PROJECT}/service-instances/{SERVICE}/endpoints/{ENDPOINT}"
_, d = call("POST", f"{E}/domains", {"domain": "www.example.com"})
print(d["dns"])  # crea exactamente este registro en tu proveedor de DNS
```

La respuesta te dice el registro DNS exacto que crear (`dns`). Un rechazo nombra su motivo antes de los dos puntos en `details`: `invalid_domain` (400), o `platform_domain`, `domain_taken`, `domain_not_verified`, `domain_limit`, `not_ready` (422). Un código `failure` explica un dominio `failed`.

## Enrutado por rutas

Cuando un dominio está en un endpoint HTTPS puedes enviar algunas rutas a otro sitio, de modo que `app.com` llegue a un servicio y `app.com/api` a otro. El destino debe ser un endpoint HTTPS abierto de un servicio **del mismo proyecto**. Gana la ruta coincidente más larga; el endpoint al que está asociado el dominio responde todo lo demás. La llamada **reemplaza todo el conjunto**, y una lista vacía elimina todas las rutas.

**curl**

```bash
curl -sS -X PUT "$ZAROZ_API$E/$ENDPOINT/domains/$ROUTE/paths" -H "$AUTH" -H "Content-Type: application/json" -d '{
  "paths": [
    {"path": "/api", "strip_prefix": true,
     "target_service_instance_id": "<uuid del servicio de la API>", "target_endpoint_id": "<uuid de su endpoint https>"}
  ]
}'

# ¿cómo enruta ahora este dominio y adónde iría /api/users?
curl -sS "$ZAROZ_API$E/$ENDPOINT/domains/$ROUTE/routing?path=/api/users" -H "$AUTH"
```

**TypeScript**

```ts
const routing = await call("PUT", `${E}/domains/${ROUTE}/paths`, {
  paths: [
    { path: "/api", strip_prefix: true, target_service_instance_id: API_SERVICE, target_endpoint_id: API_ENDPOINT },
  ],
});
for (const r of routing.body.rules) console.log(r.order, r.rule, r.status); // serving | starting | shadowed | broken

const sample = await call("GET", `${E}/domains/${ROUTE}/routing?path=/api/users`);
console.log(sample.body.sample); // { path, winning_order, upstream_path: "/users" }
```

**Python**

```python
_, routing = call("PUT", f"{E}/domains/{ROUTE}/paths", {"paths": [
    {"path": "/api", "strip_prefix": True,
     "target_service_instance_id": API_SERVICE, "target_endpoint_id": API_ENDPOINT},
]})
for r in routing["rules"]:
    print(r["order"], r["rule"], r["status"])   # serving | starting | shadowed | broken

_, sample = call("GET", f"{E}/domains/{ROUTE}/routing?path=/api/users")
print(sample["sample"])   # {"path", "winning_order", "upstream_path": "/users"}
```

Con `strip_prefix: true` el destino recibe `/users`; con `false` recibe `/api/users`. Cada regla tiene un `status`: `serving`, `starting` (su destino aún no está activo), `shadowed` (otra regla con la misma ruta gana siempre, así que esta nunca sirve) o `broken` (su destino no puede responder). Rechazos: `invalid_path` y `target_not_found` (400; un destino inexistente, cerrado o de otro proyecto responden igual), y `path_limit`, `duplicate_path`, `target_not_http` (422).

## Subdominios de juego

Un servidor de juegos puede tomar un nombre gratuito bajo una zona de la plataforma (`<nombre>.<zona>`), que apunta a su dirección pública, con un registro SRV para los juegos que leen el puerto de ahí. Un servicio tiene uno.

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/game-subdomains" -H "$AUTH"
# {"limit": 1, "subdomains": [], "zones": [{"id": "<uuid de zona>", "name": "<zona>"}]}

curl -sS "$ZAROZ_API/service-instances/$SERVICE/game-subdomains/availability?parent_domain_id=$ZONE&subdomain=myserver" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/game-subdomains" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"parent_domain_id": "<uuid de zona>", "subdomain": "myserver"}'
# 201 {"hostname": "myserver.<zona>", "status": "active", "has_srv_record": true, ...}
```

**TypeScript**

```ts
const list = await call("GET", `/service-instances/${SERVICE}/game-subdomains`);
const zone = list.body.zones[0];
const free = await call("GET",
  `/service-instances/${SERVICE}/game-subdomains/availability?parent_domain_id=${zone.id}&subdomain=myserver`);
if (free.body.status === "available") {
  const sub = await call("POST", `/service-instances/${SERVICE}/game-subdomains`, {
    parent_domain_id: zone.id,
    subdomain: "myserver",
  });
  console.log(sub.body.hostname, sub.body.status);
}
```

**Python**

```python
_, listing = call("GET", f"/service-instances/{SERVICE}/game-subdomains")
zone = listing["zones"][0]
_, free = call("GET", f"/service-instances/{SERVICE}/game-subdomains/availability"
                      f"?parent_domain_id={zone['id']}&subdomain=myserver")
if free["status"] == "available":
    _, sub = call("POST", f"/service-instances/{SERVICE}/game-subdomains",
                  {"parent_domain_id": zone["id"], "subdomain": "myserver"})
    print(sub["hostname"], sub["status"])
```

`status` es `pending` (creando los registros), `active`, `failed` (se reintenta solo) o `deleting`. `PATCH .../game-subdomains/{id}` con un nombre nuevo lo reemplaza (la respuesta trae un **id nuevo**) y `DELETE` lo libera. Un nombre sigue ocupado un tiempo después de que su titular anterior lo liberó. Rechazos: `zone_unavailable`, `invalid_subdomain`, `subdomain_taken`, `already_has_subdomain` (422).

Para un servidor de FiveM detrás de tu **propio** dominio, consulta [dominio propio en la lista de servidores](/es/fivem/custom-domain-in-server-list).

## Domain Connect

Si tu proveedor de DNS admite [Domain Connect](https://www.domainconnect.org/), Zaroz puede enviarte allí para que apruebes los registros exactos en lugar de copiarlos tú. Nunca escribe tu DNS por sí mismo. Solo lo ofrecen los productos que declaran una plantilla (servidores de juegos con un registro SRV), y un despliegue puede no ejecutarlo en absoluto: comprueba `domain_connect` en [qué ofrece el despliegue](/es/guides/api/overview#qué-ofrece-un-despliegue).

**curl**

```bash
curl -sS "$ZAROZ_API/service-instances/$SERVICE/domain-connect" -H "$AUTH"
# {"supported": true, "links": []}

curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/domain-connect/discover" -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"domain": "example.com", "host": "play"}'
# {"id": "...", "status": "pending", "apply_url": "https://dns-provider.example/...", "hostname": "play.example.com"}

# envía el navegador del cliente a apply_url y después:
curl -sS -X POST "$ZAROZ_API/service-instances/$SERVICE/domain-connect/$LINK/verify" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/service-instances/$SERVICE/domain-connect/$LINK" -H "$AUTH"
```

**TypeScript**

```ts
const state = await call("GET", `/service-instances/${SERVICE}/domain-connect`);
if (state.body.supported) {
  const link = await call("POST", `/service-instances/${SERVICE}/domain-connect/discover`, {
    domain: "example.com",
    host: "play", // "" apunta el dominio raíz al servidor
  });
  console.log("Abre", link.body.apply_url);
  // cuando el cliente apruebe en su proveedor de DNS:
  const v = await call("POST", `/service-instances/${SERVICE}/domain-connect/${link.body.id}/verify`);
  console.log(v.body.status); // active | pending | failed
}
```

**Python**

```python
_, state = call("GET", f"/service-instances/{SERVICE}/domain-connect")
if state["supported"]:
    _, link = call("POST", f"/service-instances/{SERVICE}/domain-connect/discover",
                   {"domain": "example.com", "host": "play"})   # "" apunta el dominio raíz al servidor
    print("Abre", link["apply_url"])
    # cuando el cliente apruebe en su proveedor de DNS:
    _, v = call("POST", f"/service-instances/{SERVICE}/domain-connect/{link['id']}/verify")
    print(v["status"])   # active | pending | failed
```

`verify` comprueba el DNS en vivo: `active` cuando el registro SRV está publicado y apunta al host y puerto esperados, `failed` cuando se encuentra un registro distinto, `pending` mientras no se haya propagado nada. Liberar un enlace no toca tu proveedor de DNS; quita allí los registros tú mismo. Un servicio admite como máximo 20 enlaces, y un proveedor sin flujo síncrono no se puede usar (crea los registros a mano).

## Puertos en bruto con un dominio HTTP

Un servicio con acceso externo también puede publicar un **dominio delante de un puerto TCP**, con certificado gestionado, mediante `POST /service-instances/{id}/external-access/{allocation_id}/http` (cuerpo `{"domain": "..."}`). Sigue las mismas reglas que un dominio personalizado (`DELETE` en la misma ruta lo elimina). Abrir, cerrar y la cuota de los propios puertos están en [Puertos públicos y endpoints](/es/units/endpoints).
