SpipCP
Sites

Custom customer domains

Let a hosted app serve its own customers on their domains — status.client.com, shop.client.ie — with automatic HTTPS, without anyone adding those domains in SpipCP. One toggle, one CNAME per customer, certificates issued on demand.

Some apps serve their customers on the customer's own domain: a status page on status.client-one.com, a storefront on shop.client-two.ie. Those hostnames belong to the app's customers — they can't be added one by one in SpipCP, and they shouldn't need to be. Custom customer domains turns that into a single toggle: the app flips it on, and its end-customers point their own (sub)domains at it. This node's Caddy obtains and renews each certificate lazily at the first HTTPS request, and the app decides which hostnames are allowed via an ask endpoint it owns.

How this differs from Domains & SSL

Domains & SSL is for domains an operator attaches — the app's own hostnames, one site_domains row each, verified and issued through the panel. This page is for domains the app's customers bring: nobody adds them in SpipCP, there's no row per hostname, and the panel only observes the certificates Caddy issues. Zero panel writes per customer domain.

Guided setup — the whole wiring, in order

Every control on this page can also be reached from Networking → Domains → "Set up app customer domains", which walks the sequence end to end: pick the site → enable → allow-list the automatable zones → mint the app token → first hostname → first certificate. It owns no controls of its own — each step opens the section of this card that does — and its progress is read from the live fleet, so it can be left and resumed. See the guided setup journeys.

Enable it

On the site's workspace, open Networking → Domains and find the Custom customer domains card.

The Custom customer domains card
📷Site → Networking → Custom customer domains — the toggle, the ask-endpoint path, and the CNAME-target hint.img/sites-on-demand.avif
Site → Networking → Custom customer domains — the toggle, the ask-endpoint path, and the CNAME-target hint.
  • Site type — only node and docker sites qualify (types that answer HTTP directly). The card indicates when a site type can't use it.
  • One per node — a node holds one on-demand app at a time. If another site on the same node already offers custom customer domains, the card says so — a second app belongs on a second node.
  • Ask endpoint path — the path this node calls to authorize each hostname (default /api/v1/caddy/ask). See the ask contract for what the app must implement.

Give customers a CNAME target

Attach the app's own stable hostname the normal way first (the Attach a domain card). The Custom customer domains card then lists the attached hostnames as CNAME targets. Each customer creates one record:

A CNAME: status.client-one.com → the app's stable hostname (e.g. app.yourdomain.com).

Apex domains (no subdomain) can't CNAME — the customer uses an A/AAAA record to the node, or an ALIAS where their provider supports it.

Where the customer's CNAME lives — automated, or by hand

The customer-facing step is always one record. Which of three tiers it lands in is detected, not picked — the same Hosted / Connected / External authority the fleet-wide Domains inventory uses:

TierWhere the zone livesAutomated?
HostedA self-hosted PowerDNS zoneYes, once allow-listed (below)
ConnectedA connected provider account held by the operatorYes, once allow-listed (below)
ExternalThe customer's own DNS (the common case), or a zone neither tier coversNo — exact records are provided to relay

For a Hosted or Connected zone, the Custom hostnames section below can create the record — via the app's API token or the operator's "Add the record for me" button. For everything else, the customer (or the operator, on their behalf) still creates the record by hand at whoever holds the DNS.

  • Cloudflare orange-cloud. It works — the ACME HTTP-01 challenge passes through Cloudflare on port 80, and after the first issuance "Full (strict)" is satisfied by the certificate on the node. Two caveats: the very first proxied request can return a 525 until issuance completes, and grey-cloud (DNS-only) is the smoother, recommended posture. Custom hostnames never proxies a record it creates — ACME HTTP-01 needs a direct path to the origin.

Custom hostnames — automate the DNS step

The Custom hostnames section (below the Custom customer domains card) gives the app a machine seam for the DNS step, plus an operator door for doing it by hand. It's deny-by-default: nothing writes into a zone the operator hasn't explicitly allowed.

The Custom hostnames card
📷The App API access section — mint a site-bound token, the automated-zones allow-list, and the registered-hostnames table.img/sites-on-demand-api.avif
The App API access section — mint a site-bound token, the automated-zones allow-list, and the registered-hostnames table.

The automated-zones allow-list

A hostname whose zone SpipCP fully controls (Hosted or Connected) still only gets a written record if that zone is on this list. Add a zone apex (e.g. client1.com), or "*" for every zone the installation controls — a loud confirmation warns before "*" takes effect, since it means the app's token can write into any zone the installation runs, including other customers' zones on self-hosted nameservers. Most installs list specific zones.

A zone left off the list isn't blocked — a registration against it just comes back as instructions (the exact records to relay), same as an External hostname. Widen the allow-list whenever real demand for it appears in the registered-hostnames table below.

Minting the app token

Mint app token creates a spat_… bearer token bound to this one site, scoped to custom-hostnames permissions only — it can never reach another site or a fleet-wide surface. Shown once; paste it into the app's environment. See the Custom hostnames API for what the app calls with it.

The app's first call is normally a probe: a read-only preview of what registering a hostname would do — automatic, instructions, or blocked — which lets it show the customer the right lane the moment a domain is typed, before asking anyone to prove ownership or write a record. It is a pure read: no row, no job, nothing in the table below.

The registered-hostnames table

Every hostname registered for this site — by the app's token, or by the operator below — with its tier, state (pending / active / instructions / failed / removing / removal-manual), and source (App / Operator). The kebab (⋮) menu removes a hostname.

Add the record for me (the operator door)

Register a customer's hostname directly, from the card — the same engine the app's token calls. Type the hostname and click Register: a Hosted/Connected + allow-listed hostname plans a real record and provisions it; anything else renders the exact records to relay to the customer.

Add the record for me
📷The operator's Add-the-record-for-me flow — an External-tier hostname renders the exact record to relay to the customer.img/sites-add-record.avif
The operator's Add-the-record-for-me flow — an External-tier hostname renders the exact record to relay to the customer.

Watch issued certificates

Once a customer's domain resolves and someone hits it over HTTPS, Caddy issues the certificate and it appears in the Issued certificates table on the card — hostname, issuer, and expiry.

The issued-certificates table
📷The read-only issued-certificates list — the customer certs Caddy issued on demand, with a Refresh.img/sites-on-demand-certs.avif
The read-only issued-certificates list — the customer certs Caddy issued on demand, with a Refresh.

This list is read-only: the panel observes the certificates (it reads them from the node's cert facts) but never provisions or renews per hostname — renewal is automatic, handled by Caddy. Use Refresh to re-read the node's facts. The same roll-up appears fleet-wide on Networking → SSL in the On-demand custom domains section.

Troubleshooting

  • The HTTPS handshake fails for a customer. Either the app's ask endpoint denied the hostname (it's not a verified customer domain in the app) or the app is down. The ask endpoint is the only issuance gate — if it says no, no certificate is issued.
  • The ask endpoint is down. No new certificates issue while it's unreachable — but certificates already issued keep serving. That's the correct failure direction.
  • Disable ≠ revoke. Turning the feature off removes the catch-all so no new hostnames are served, but certificates already in Caddy's store remain (unused) until they expire naturally.

Next steps

  • Custom hostnames API — the five calls the app drives with its site-bound token, and the probe → verify → register → poll → prime → poll recipe.
  • The ask contract — what the app implements to authorize each certificate issuance.
  • Domains: the fleet inventory — the Hosted/Connected/External tiers, and where registered custom hostnames show up fleet-wide.

On this page