Domains

Your product answers at its own domains. Your platform's DNS zones (the cluster file's [dns] zones) are your product's too: your API at api.shop.example and your homepage at shop.example share shop.example, and a name there is your services' as soon as a deploy claims it, its record published by your platform (Names in your platform's zones). A domain of another zone is your services' once you prove it with DNS records you create at your DNS provider; your platform never writes records in that zone. Browsers see Cloudflare's certificate for your names; nothing per name is issued, renewed or waited for.

Names in your platform's zones

A name of your platform's zones, the zone itself or one label below it (what Cloudflare's free certificate and your platform's own cover), may be a service's, your status page's or a public bucket's:

[env.prod.services.web]
domains = ["shop.example", "www.shop.example"]   # beside your API at api.shop.example

Nothing to create or prove: the deploy claims the name, your platform publishes its record (a proxied CNAME to its entry, written by its ExternalDNS) and the name answers once the deploy is live. shop-ops domains lists it as verified, with no records. A static service on Cloudflare may answer in these zones too (Cloudflare makes its record). Your platform's own names are never a service's: its API, dashboard, console, status page and outside watcher (public_api, dashboard, console, status_page, watcher), and the names it makes in its first zone (entry, s3, s3-direct, acme); a deploy naming one is refused, and so is a sync giving your platform a name a service has. Names two labels below a zone (www.app.shop.example) are refused: no free certificate covers them.

Names in other zones

List a service's names in scaledbox.toml, per environment:

[env.prod.services.web]
domains = ["shop.example", "www.shop.example"]

[env.staging.services.web]
domains = ["staging.shop.example"]

The next deploy claims them. shop-ops domains lists each name, its status and the records it needs:

_scaledbox-challenge.shop.example  TXT    "<the value shown>"
shop.example                       CNAME  shop--product.<targets>
staging.shop.example               CNAME  shop.example

<targets> is your platform's targets zone (the cluster file's targets_zone: a domain of its own on Hetzner's DNS, off Cloudflare, so a Cloudflare zone can proxy a CNAME to it); shop-ops domains shows the full names.

Your platform checks the records every few minutes. A proxied CNAME is hidden from DNS, so it checks it by fetching http://<your name>/.well-known/scaledbox-check/<token> through it (as Let's Encrypt checks a name): leave that path on plain HTTP, out of any redirect or WAF rule of yours. A name answers, over HTTPS, once both hold.

Certificates

Visitors get Cloudflare's certificate for your names (its free Universal SSL covers your zone's apex and one level below it). Behind it, your platform's edge presents a certificate for your target's name, which Cloudflare accepts in any SSL/TLS mode, Full (strict) included: so no name of yours needs a certificate, a record or a wait of its own, and your CAA records only concern Cloudflare's.

When a name points away

A name whose records stop pointing at your product stops answering after its checks have failed for ten minutes, and is released seven days later unless it points at your product again: it answers again then, with no TXT record needed (it was proven once). Each name has one owner across your services and your tenants: a name a tenant has claimed can't be your services', nor theirs once yours.

Your tenants' domain

In saas mode, your tenants' apps (Tenants) answer under a domain of yours: one of your proven domains, a name below one, or a name one label below a zone of your platform's:

[tenants]
domain = "apps.shop.example"     # a tenant's app answers at <subdomain>.apps.shop.example

In your platform's zones, your platform publishes its wildcard itself. Elsewhere it needs one record, listed by shop-ops domains (and the domain's TXT proof, if neither it nor a domain of yours above it is proven yet):

*.shopapps.example                CNAME  tenants.shop.<targets>    (proxied)

Proxied, it puts every tenant's app behind Cloudflare's proxy at no cost per name: each app and each of its environments is one level below your domain (blog--acme.shopapps.example, staging--blog--acme.shopapps.example). Cloudflare's free certificate covers that when your tenant domain is a zone's apex (shopapps.example); below a zone (apps.shop.example), its apps are two labels below it, which only an advanced certificate for *.apps.shop.example covers (Cloudflare's Advanced Certificate Manager, on that zone): your choice, billed by Cloudflare. A zone of its own (shopapps.example) costs nothing. A zone of your platform's itself is never your tenant domain: its wildcard leads to your platform. Until it holds, your tenants' apps have no names, and their deploys say so. Submitting the domain to the Public Suffix List makes browsers keep your tenants' cookies apart; that is yours to do, and it takes weeks: start early.

Your previews ([previews] domain) may answer under your tenant domain only while your tenants run on the same cluster as your environments (that wildcard leads to your tenants'); otherwise a preview's deploy is refused, saying so: give previews another of your domains.

A tenant's own domain, for its app, is added through the API and proven by records its owner creates (see Tenants).

Your status page

[status] domain is one of your names too, proven the same way and listed as (status page) (see Status pages).

A public bucket's domain

An environment's public bucket ([bucket] public = true) may answer on a name of yours, [bucket] domain = "files.example.com": listed as (bucket), proven the same way (its CNAME proxied, orange cloud), then GET https://files.example.com/<key> reads the object <key> under its BUCKET_PREFIX, through Cloudflare's CDN. Objects without a Cache-Control of their own are served with public, max-age=3600; set one at upload to change it. Cloudflare caches by file extension unless your zone's Cache Rules say otherwise. Nothing but reading goes there: writes need the bucket's keys, at its S3 endpoint.