Domains and certificates
An owner can give a site their own domain: a subdomain with a CNAME record, or a bare domain with
an A record, plus one TXT record that proves ownership. The certificate is issued automatically
(on a small box, by Caddy when the name is first asked for). www and the bare domain work
together, one redirecting to the other.
Checks and release. A connected domain whose DNS never points here is released after
DOMAIN_UNPROVEN_HOURS; one that points here but never goes live over HTTPS after
DOMAIN_UNPROVEN_MAX_DAYS. A live domain that starts failing every check gets its owner an email,
and later stops being the site’s address; the date in that email holds even if the setting
changes.
Address families. An account can connect *.<its domain> once, and every one of its sites
then answers at <site>.<its domain> (with an optional site-name prefix: *.voucher.brand.com
with prefix voucher- serves the sites voucher-<name>). The owner adds a wildcard record
(CNAME *.<domain> to CNAME_TARGET, or an A record to CUSTOM_DOMAIN_IP) and a TXT record
_simple-host.<domain> holding the family’s token. Nothing is served before both are seen; a
family still unproven after ADDRESS_FAMILY_UNPROVEN_HOURS is dropped. Once verified it is the
account’s alone: no other account can connect it, a family overlapping it, or a custom domain
under it. A family failing its checks emails its owner after ADDRESS_FAMILY_LAPSE_WARN_HOURS
and is disconnected after ADDRESS_FAMILY_LAPSE_HOURS. The event domains and the server’s own
zones are never accepted as a custom domain or a family.
Certificates for families are the operator’s in this release: issue a wildcard certificate for
*.<domain> with certbot (DNS-01, renewed as usual), then name its lineage on the family from
the admin API (PUT /v1/admin/address-families/{id}/cert-mode
{"cert_mode":"wildcard","cert_name":"<lineage>"}). Until then the family shows
waiting_for_operator. The family issuer (deploy/family-certs/) checks the lineage covers the
family and writes one nginx file per family; it never issues, renews or deletes a certificate.
Families need ADDRESS_FAMILY_CERT_DIR pointing at that issuer’s state directory; without it no
family is ever served. They are not available on a small box with Caddy yet.
| Setting | Default | Allowed | What it does |
|---|---|---|---|
DOMAIN_UNPROVEN_HOURS |
24 |
1–720 hours | A connected domain whose DNS never points here is released after this long. |
DOMAIN_UNPROVEN_MAX_DAYS |
7 |
1–90 days | A domain that points here but never goes live over HTTPS is released after this long. |
DOMAIN_LAPSE_WARN_HOURS |
24 |
1–720 hours | A live domain failing every check: its owner is emailed after this long. |
DOMAIN_LAPSE_HOURS |
72 |
2–2160 hours | … and it stops being the site’s address after this long. Longer than DOMAIN_LAPSE_WARN_HOURS. |
DOMAIN_CHECK_INTERVAL_MINUTES |
2 |
1–60 minutes | How often connected domains are checked. |
DOMAIN_CERTS_PER_ACCOUNT_DAILY |
5 |
1–1000 certificates | New custom-domain certificates one account may ask for in a day. |
EVENT_TTL_DAYS |
21 |
1–60 days | How long a claimed event hostname lives (only where EVENT_DNS_TOKEN is set). |
EVENT_MAX_CLAIMS |
5 |
1–100 names | Event hostnames one account may hold at once. |
ADDRESS_FAMILIES |
on |
on / off |
on lets an account connect *.<its domain> once, so every site of the account answers at <site>.<its domain> (an address family). Families also need ADDRESS_FAMILY_CERT_DIR and a wildcard certificate the operator sets up. |
ADDRESS_FAMILIES_PER_ACCOUNT |
5 |
0–100 families | Address families one account may connect. 0: none. |
ADDRESS_FAMILY_UNPROVEN_HOURS |
24 |
1–720 hours | How long a new address family may wait for its DNS records before it is dropped. |
ADDRESS_FAMILY_LAPSE_WARN_HOURS |
24 |
1–720 hours | How long a working address family may fail its checks before its owner is emailed. |
ADDRESS_FAMILY_LAPSE_HOURS |
72 |
2–2160 hours | How long a working address family may fail its checks before it is disconnected. Must be longer than ADDRESS_FAMILY_LAPSE_WARN_HOURS. |
ADDRESS_FAMILY_CHECK_INTERVAL_MINUTES |
10 |
1–60 minutes | How often address families are checked. |
ADDRESS_FAMILY_ACTIVE_RECHECK_MINUTES |
60 |
5–1440 minutes | How often a working address family’s DNS records are proved again. |
ADDRESS_FAMILY_CERTS_PER_ACCOUNT_DAILY |
12 |
1–1000 certificates | Per-site-name certificates one account’s families may ask for in a day (for a later release; wildcard families need none). |
ADDRESS_FAMILY_RESERVED_LABELS |
www |
DNS labels | Names (comma-separated) that never name a site under an address family. |
ADDRESS_FAMILY_CACHE_SECONDS |
30 |
1–3600 seconds | How long the server keeps its list of working address families before reading it again. |
RATE_LIMIT_TLS_ASK |
60,100ms |
any (warns past 10× looser) | Certificate checks (/internal/tls-ask), per address. |
RATE_LIMIT_DOMAIN_CHECK |
10,10s |
any (warns past 10× looser) | “Check again” on a domain, per address. |
RATE_LIMIT_DOMAIN_CHECK_USER |
3,30s |
any (warns past 10× looser) | “Check again” on a domain, per account. |
RATE_LIMIT_ADDRESS_FAMILY_CHECK |
10,10s |
any (warns past 10× looser) | “Check again” on an address family, per address. |
RATE_LIMIT_ADDRESS_FAMILY_CHECK_USER |
3,30s |
any (warns past 10× looser) | “Check again” on an address family, per account. |
CNAME_TARGET |
cname.<SITE_DOMAIN> |
text | The hostname people point their own domain at with a CNAME record. |
CUSTOM_DOMAIN_IP |
none | text | This server’s public IPv4, given as the A record for a bare domain (brand.com). |
CUSTOM_DOMAIN_IP_ALSO |
none | text | Earlier public IPv4s of this server, comma-separated. Domains still pointed at one keep working; only CUSTOM_DOMAIN_IP is handed out. |
SITE_CERT_DIR |
none | text | Where per-person certificates are requested from, and found ready from, the certificate issuer. |
SITE_BASE_CERT_DIR |
none | text | Like SITE_CERT_DIR for the alternate base. Required with ready/ and requests/ while the split base move is on. Unset when the move is off; no certificates are requested for the paused base. |
DOMAIN_CERT_DIR |
none | text | Where custom-domain certificates are requested from the certificate issuer. Empty: issued by hand. |
ADDRESS_FAMILY_CERT_DIR |
none | text | Where address-family requests go to the family issuer, and where it says a family is served (ready/). Empty: no address family is ever served. |
EVENT_DNS_TOKEN |
none | secret | The DNS token that hands out event hostnames. The public instance only. Security-sensitive. |
EVENT_DNS_TEAM_ID |
none | text | The DNS account the event token belongs to. |
EVENT_DOMAINS |
none | text | Zones (comma-separated) event hostnames are handed out under. |
Recipes
Bare domains on a small box. Set CUSTOM_DOMAIN_IP to the server’s public IPv4 so owners are
told the right A record.
Changing the server’s address. Point both addresses at the box, set CUSTOM_DOMAIN_IP to the
new one and CUSTOM_DOMAIN_IP_ALSO to the old one. New domains are told the new A record; domains
still on the old address keep passing their checks and renewing their certificates until their
owners move the record. Remove the old address from CUSTOM_DOMAIN_IP_ALSO once none points there,
and at once if the server ever loses it: whoever holds the address next would pass the same checks.
Faster release of abandoned domains.
DOMAIN_UNPROVEN_HOURS=6
DOMAIN_UNPROVEN_MAX_DAYS=2
Adopt an existing hand-made wildcard server. Connect the family for the account from the
admin API with its lineage (POST /v1/admin/users/{id}/address-families
{"suffix":"*.trips.brand.com","cert_name":"trips.brand.com","canonical":false}, and
"proof_exempt":true if you already run its DNS). Once it is verified, the issuer reports
“already served here” until deploy/prod/family-adopt.sh <domain> --old <vhost> (dry run; then
--apply) swaps the hand-made server for the managed one; --rollback --apply puts it back.
Certificate issuers (root scripts)
The two root certificate issuers in deploy/ are not part of the service’s environment. Each
reads its own conf file, sourced as shell, which is where their limits are changed; the file is
optional and unset values keep the script’s defaults.
/etc/simple-host-domain-certs.conf (custom domains, deploy/domain-certs/issue.sh):
| Variable | Default | What it controls |
|---|---|---|
DAILY |
50 | New custom-domain certificates per rolling 24 hours, for the whole server. |
PER_RUN |
10 | Certificates one run may request. |
RETRY_AFTER |
21600 | Seconds before a failed domain is tried again. |
The per-account cap the app enforces (DOMAIN_CERTS_PER_ACCOUNT_DAILY) is separate from, and
normally well under, this server-wide DAILY.
/etc/simple-host-site-certs.conf (per-person *.<handle> addresses, deploy/site-certs/issue.sh):
| Variable | Default | What it controls |
|---|---|---|
BUDGET |
40 | New certificates per rolling 7 days. Let’s Encrypt allows 50 per registered domain per week; keep it at or under 50. |
DAILY |
12 | New certificates per rolling 24 hours. |
PER_RUN |
6 | Certificates one run may request. |
RETRY_AFTER |
21600 | Seconds before a failed handle is tried again. |
The site-certs issuer writes the limits in force to $SITE_CERT_DIR/limits, and the app reads
them from there for the waiting-time estimate it shows, so nothing else needs changing. How
often each issuer runs is its systemd timer (simple-host-*-certs.timer, every 10 minutes);
change it with a drop-in (systemctl edit).
Checks and certificate hand-off
DOMAIN_UNPROVEN_MAX_DAYS must not be shorter than DOMAIN_UNPROVEN_HOURS.
Domain and address-family certificate daily caps use rolling days. Pending
address families serve nothing while waiting for the wildcard and TXT records.
www.<family> belongs to the customer; reserved labels never name a site.
Every family change refreshes the working-family cache immediately.
Per-site-name family certificates are for a later per-host release; wildcard
families need none.
SITE_CERT_DIR, DOMAIN_CERT_DIR and ADDRESS_FAMILY_CERT_DIR hand off to
deploy/site-certs/, deploy/domain-certs/ and deploy/family-certs/ respectively.
The alternate-base issuer uses deploy/site-certs/simple-host-site-certs-site.*.
When the move is on and SITE_BASE_DOMAIN differs from SITE_DOMAIN, startup
fails without SITE_BASE_CERT_DIR or its ready/ and requests/ directories;
the app never creates those directories. The hosted move was paused on
2026-10-09, to revisit about 2026-11-09; see the
historical plan.