Claudium
Operations

Custom domain per org

Map a hostname like claudium.acme.com to a workspace. Two TLS recipes — Cloudflare proxy (zero-config) and Caddy reverse proxy (self-hosted).

Branding-kit slice 1: a customer org can claim a hostname like claudium.acme.com and have the post-auth surfaces (the orb, settings, billing) served at that URL. DNS + TLS are operator-side concerns; the app layer only owns the org → hostname mapping.

What this gives you

  • Customers see your product at their own URL (claudium.acme.com) instead of a shared subdomain on yours.
  • The hub recognises requests on that hostname and stashes the matching org on req._hostOrg for downstream branding logic (banner colour, logo, name — those land in the next branding-kit slices).
  • Multiple customers can claim distinct hostnames; the database enforces uniqueness so no two orgs can claim the same host.

What it does not do (yet):

  • Apply custom colours, logos, or names — those are separate slices on the branding-kit roadmap.
  • Issue TLS certificates — that's the operator's deployment layer (one of the recipes below).
  • Replace login URLs at the brand level — claudium.acme.com/login works, but the OTP email still names "Claudium" until the email-branding slice ships.

Set the domain on a workspace

The owner or admin of an org opens Settings → Custom domain in /brain, types the hostname (e.g. claudium.teleperson.com), and clicks Save. The hub validates the hostname server-side against RFC-1035 label rules + refuses its own canonical host. Once saved, the panel shows the exact DNS record the customer should paste into their provider, plus a Verify DNS button that runs a live CNAME lookup so the customer can confirm propagation without dropping to a terminal.

For automation (e.g. provisioning a fleet of orgs), the same endpoint accepts curl from a signed-in session:

curl -X POST https://<hub-host>/api/admin/org/custom-domain \
  -b 'claudium-session=<sid>' \
  -H 'Content-Type: application/json' \
  -d '{"domain":"claudium.teleperson.com"}'

The server validates the hostname against RFC-1035 label rules + refuses the canonical hub host. On success it returns { ok, domain, cnameTarget }. The cnameTarget is the hub host you point the customer's DNS at.

To clear:

curl -X POST https://<hub-host>/api/admin/org/custom-domain \
  -b 'claudium-session=<sid>' \
  -H 'Content-Type: application/json' \
  -d '{"domain":null}'

To verify propagation programmatically:

curl https://<hub-host>/api/admin/org/custom-domain/verify \
  -b 'claudium-session=<sid>'
# → { ok, domain, cnameTarget, status: 'verified' | 'wrong-target'
#     | 'not-a-cname' | 'not-resolving' | 'lookup-failed' }

DNS — what the customer configures

On their DNS provider (whatever's authoritative for acme.com), the customer adds:

claudium.acme.com.   IN   CNAME   <cnameTarget>.

Where <cnameTarget> is the hub host the API returned (e.g. app.claudium.com or your Render URL). CNAME — not A record — so DNS resolution always follows the hub's own IP changes.

That's the only DNS change. TLS happens at the hub's edge.

If the customer already uses Cloudflare for acme.com, this is the cleanest path:

  1. In Cloudflare, add a CNAME record claudium.acme.com<cnameTarget>.
  2. Toggle the proxy orange-cloud ON.
  3. Cloudflare's edge automatically terminates TLS for claudium.acme.com (Universal SSL — no extra config) and forwards the request to the hub with the original Host header intact.
  4. The hub reads req.headers.host, finds the org by custom_domain, and serves the branded view.

What about the hub-side certificate? The hub still needs a cert for its own hostname (the cnameTarget). Render handles that automatically. The customer's claudium.acme.com cert is Cloudflare's responsibility — they issue + renew it; you do nothing.

Caveat: Cloudflare's free plan does not allow non-Cloudflare TLS to a non-standard port, but Render-hosted hubs run on standard 443 so this is moot.

TLS — recipe B: Caddy reverse proxy (self-hosted)

If you're self-hosting the hub (or Cloudflare isn't an option for the customer):

  1. Add a Caddyfile in front of the hub:
# /etc/caddy/Caddyfile

# Pin a single hostname (simplest case — one org, one cert):
claudium.teleperson.com {
    reverse_proxy localhost:4242
}

# Or use on-demand TLS to handle any hostname your customers CNAME
# at this Caddy instance. The hub exposes /api/tls/allow as the
# `ask` endpoint Caddy calls before issuing each cert — it returns
# 200 if the hostname is claimed by some org in the database, 404
# otherwise. So a customer can't trick you into provisioning a
# cert for a domain that isn't actually theirs.
{
    on_demand_tls {
        ask https://<hub-host>/api/tls/allow
    }
}

:443 {
    tls {
        on_demand
    }
    reverse_proxy localhost:4242
}

The on-demand variant issues Let's Encrypt certs for any hostname that resolves to this Caddy instance, calling /api/tls/allow?domain=<host> to confirm the hostname is claimed (so you don't issue certs for random domains pointed at you). The hub returns 200 or 404 only — no body, no org info leaks to Caddy or the requesting client.

  1. Open ports 80 + 443 to Caddy (ACME HTTP-01 challenges need 80).
  2. Customer points claudium.teleperson.com at your Caddy IP via A record (or via a CNAME you publish).

Caddy handles cert issuance + renewal in the background. Nothing on the hub side changes.

Verify the customer's DNS

A quick check after the customer says "I've set the CNAME":

dig +short CNAME claudium.acme.com
# should print: <cnameTarget>.

If dig returns nothing or the wrong target, DNS hasn't propagated yet (give it 5–60 minutes depending on TTL) or the customer pointed at the wrong host. Once it resolves, hit https://claudium.acme.com/login from your browser and you should land on the hub login page — branding still default for now, but the URL is theirs.

Reserved + rejected hostnames

The validator refuses:

  • Hostnames > 253 characters total or labels > 63 characters.
  • Edge dots (.acme.com, acme.com.) — DNS won't accept these.
  • Edge or label-edge hyphens (-acme.com, acme.com-, foo-.acme.com).
  • Bare names without a TLD (localhost).
  • Non-ASCII, uppercase letters (auto-lowercased), punctuation other than . and -.
  • The canonical hub hostname (read from PUBLIC_URL). Prevents a customer from accidentally claiming the platform's own URL.

All validations happen server-side; the API returns the specific reason code in the error body so the UI can surface a precise message.

On this page