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 your app's customers — you can't add each of them in SpipCP, and you shouldn't have to. Custom customer domains turns that into a single toggle: your 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 your app decides which hostnames are allowed via an ask endpoint it owns.

How this differs from Domains & SSL

Domains & SSL is for domains you attach — the app's own hostnames, one site_domains row each, verified and issued through the panel. This page is for domains your 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.

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 tells you 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 — put a second app 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 your 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 your attached hostnames as CNAME targets. Tell each customer to create one record:

A CNAME: status.client-one.comyour 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?
HostedYour own self-hosted PowerDNSYes, once allow-listed (below)
ConnectedA connected provider account you holdYes, once allow-listed (below)
ExternalThe customer's own DNS (the common case), or a zone neither tier coversNo — you get exact records to relay

For a Hosted or Connected zone, the Custom hostnames section below can create the record for you — via your app's API token or the operator's "Add the record for me" button. For everything else, the customer (or you, 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 your 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 your 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 this installation controls — a loud confirmation warns you before "*" takes effect, since it means your app's token can write into any zone you run, including other customers' zones on your 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 you see real demand for it 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 your app's environment. See the Custom hostnames API for what your app calls with it.

The registered-hostnames table

Every hostname registered for this site — by your 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 yourself, from the card — the same engine your 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 your app's ask endpoint denied the hostname (it's not a verified customer domain in your 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 four calls your app drives with its site-bound token, and the register → poll → prime → poll recipe.
  • The ask contract — what your 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