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.
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.
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 issuedfor (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 timefrom 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.
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
Code
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"
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.
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).
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.
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.