Zaroz
Operate services

Custom domains, path routing, game subdomains and Domain Connect

A service is always reachable on a free platform address. This guide is how you make it answer on a name of your own. Which of these your service has depends on its product, so ask for its capabilities first: custom_domain and external_access appear when it supports them. The samples use the helper from Using the Zaroz API.

You wantUse
www.example.com in front of a website or APICustom domain
example.com/api to reach a different service than example.comPath routing
play.yourgame.zaroz.cloud for a game serverGame subdomain
Your DNS provider to create the records for youDomain Connect

Domains that need a certificate use automatic TLS, so the name must already point at Zaroz in DNS before the certificate can be issued. A pending domain is normal while you wait for DNS; it is not an error.

Custom domains

Prove you own the domain

Unless the name is one single label under a parent domain Zaroz itself registered for you, the organization must first prove control of the domain with a DNS TXT record: see Verify a domain. Names inside Zaroz's own DNS zone are refused for good, and a name another organization already holds is refused with the same answer as one you have not proven, so this API cannot be used to find out who owns what.

Check, attach, wait, detach

Ask first whether a name could be attached; it changes nothing.

curl

TerminalCode
curl -sS "$ZAROZ_API/service-instances/$SERVICE/custom-domains/availability?hostname=www.example.com" -H "$AUTH" # {"status": "available"} or 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

TypeScriptCode
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 until DNS points here and the certificate is issued 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

Code
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 until DNS points here and the certificate is issued _, 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)
  • Create a CNAME (www.example.com to the platform host the listing returns as platform_host) at your DNS provider. A bare domain (example.com) cannot hold a CNAME: use your provider's ALIAS, ANAME or CNAME-flattening record to the same target.
  • status goes pending, then active (route and certificate live) or failed (usually the certificate: it cannot be issued until the DNS points here; fix the record and it retries).
  • Attaching is idempotent: a name already attached to this service is re-applied and returned. A service holds a limited number of domains (limit in the listing), and a service that publishes no HTTP cannot take one.
  • Detaching removes the route, the certificate and your claim on the name, so you can attach it again later. The free platform host cannot be detached.

Domains on an HTTPS endpoint

A Workload publishes websites and APIs as HTTPS endpoints (see Public ports and endpoints and Add a public port). Their domains are managed on the endpoint:

curl

TerminalCode
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": "<route id>", "domain": "www.example.com", "status": "pending", # "dns": {"record_type": "CNAME", "name": "www.example.com", "target": "<free host>"}} curl -sS -X DELETE "$ZAROZ_API$E/$ENDPOINT/domains/$ROUTE" -H "$AUTH"

TypeScript

TypeScriptCode
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); // create exactly this record at your DNS provider

Python

Code
E = f"/projects/{PROJECT}/service-instances/{SERVICE}/endpoints/{ENDPOINT}" _, d = call("POST", f"{E}/domains", {"domain": "www.example.com"}) print(d["dns"]) # create exactly this record at your DNS provider

The response tells you the exact DNS record to create (dns). A refusal names its reason before the colon in details: invalid_domain (400), or platform_domain, domain_taken, domain_not_verified, domain_limit, not_ready (422). A failure code explains a failed domain.

Path routing

Once a domain is on an HTTPS endpoint you can send some paths elsewhere, so app.com reaches one service and app.com/api another. The target must be an open HTTPS endpoint of a service in the same project. The longest matching path wins; the endpoint the domain is attached to answers everything else. The call replaces the whole set, and an empty list removes every path.

curl

TerminalCode
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": "<api service uuid>", "target_endpoint_id": "<its https endpoint uuid>"} ] }' # how does this domain route now, and where would /api/users go? curl -sS "$ZAROZ_API$E/$ENDPOINT/domains/$ROUTE/routing?path=/api/users" -H "$AUTH"

TypeScript

TypeScriptCode
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

Code
_, 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"}

With strip_prefix: true the target receives /users; with false it receives /api/users. Each rule has a status: serving, starting (its target is not live yet), shadowed (another rule with the same path always wins, so this one never serves) or broken (its target cannot answer). Refusals: invalid_path and target_not_found (400, a missing, closed or other-project target all answer alike), and path_limit, duplicate_path, target_not_http (422).

Game subdomains

A game server can take a free name under a platform zone (<name>.<zone>), pointing at its public address, with an SRV record for games that read a port from it. A service holds one.

curl

TerminalCode
curl -sS "$ZAROZ_API/service-instances/$SERVICE/game-subdomains" -H "$AUTH" # {"limit": 1, "subdomains": [], "zones": [{"id": "<zone uuid>", "name": "<zone>"}]} 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": "<zone uuid>", "subdomain": "myserver"}' # 201 {"hostname": "myserver.<zone>", "status": "active", "has_srv_record": true, ...}

TypeScript

TypeScriptCode
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

Code
_, 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 is pending (records being created), active, failed (retried automatically) or deleting. PATCH .../game-subdomains/{id} with a new name replaces it (the answer carries a new id), and DELETE releases it. A name stays taken for a while after its previous holder released it. Refusals: zone_unavailable, invalid_subdomain, subdomain_taken, already_has_subdomain (422).

For a FiveM server behind your own domain, see custom domain in the server list.

Domain Connect

If your DNS provider supports Domain Connect, Zaroz can send you there to approve the exact records instead of you copying them. It never writes your DNS itself. Only products that declare a template (game servers with an SRV record) offer it, and a deployment may not run it at all: check domain_connect in what the deployment offers.

curl

TerminalCode
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"} # send the customer's browser to apply_url, then: 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

TypeScriptCode
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", // "" points the apex at the server }); console.log("Open", link.body.apply_url); // after the customer approves at their DNS provider: const v = await call("POST", `/service-instances/${SERVICE}/domain-connect/${link.body.id}/verify`); console.log(v.body.status); // active | pending | failed }

Python

Code
_, 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"}) # "" points the apex at the server print("Open", link["apply_url"]) # after the customer approves at their DNS provider: _, v = call("POST", f"/service-instances/{SERVICE}/domain-connect/{link['id']}/verify") print(v["status"]) # active | pending | failed

verify checks live DNS: active once the SRV record is published and points at the expected host and port, failed when a different record is found, pending while nothing has propagated. Releasing a link does not touch your DNS provider; remove the records there yourself. A service holds at most 20 links, and a provider without a synchronous flow cannot be used (create the records by hand).

Raw ports with an HTTP domain

A service with external access can also publish a domain in front of one TCP port, with a managed certificate, through POST /service-instances/{id}/external-access/{allocation_id}/http (body {"domain": "..."}). It follows the same rules as a custom domain (DELETE on the same path removes it). Opening, closing and quota of the ports themselves are in Public ports and endpoints.

Last modified on