With a custom domain flow, customers can use their own hostnames, such as app.customer.com, to serve your product. Claiming a hostname is an authorization step: the tenant who claims it receives its traffic and a valid certificate. If your platform accepts claims based only on DNS pointing at it, the hostname will go to the first tenant who requests it. This post explains the process from claiming to releasing a hostname.
System description
A tenant claims a hostname by showing they can update the customer's DNS zone. The platform then creates a binding between the hostname and the tenant, and the edge only serves the hostname while this binding is active. A scheduled job keeps checking the proof until the binding is released.

Architecture choice
Architecture choice
There are two main decisions. The first is how the customer proves they control the hostname.
Dedicated DNS challenge
The platform creates a random token for the claim, and the customer adds it as a TXT record at an underscore-prefixed name, like _yourplatform-challenge.app.customer.com. Setting up the routing record is a separate step.
Use this when:
Your edge targets are shared or static, so the routing record does not identify a tenant
Customers need to verify before cutting traffic over, as in a migration from another provider
You onboard apex domains, which carry A records instead of CNAMEs
Trade-off: the customer needs to publish and maintain a second DNS record. You can expect support tickets both during setup and if a DNS cleanup later removes the record.
Routing record as proof
The CNAME record the customer sets up for routing also acts as proof. Pointing app.customer.com at a tenant-specific target shows the same DNS write access as a TXT challenge.
Use this when:
Every tenant gets a routing target assigned by the platform and unique to them
Onboarding has to be a single DNS change
Trade-off: this method only works if each tenant has a unique target. If the target is shared, it only proves that traffic reaches your platform, so any tenant could claim a name that already points there. Apex domains usually do not provide a unique target, so they still require the TXT challenge.
Common middle ground: routing-record proof for subdomains with unique targets for each tenant, and a TXT challenge for apex domains and migrations.
The second decision is whether the proof needs to remain after the domain is live.
One-shot verification
The challenge is verified when the claim is made, and the customer can delete the record afterwards. The binding remains valid until it is released.
Use this when:
The customer's DNS is managed by a team you cannot ask for standing records
The hostname serves content where drift is acceptable
Trade-off: if the domain expires or changes owners, your platform will keep routing it to the old tenant until someone notices.
Standing verification
The record remains published, and a scheduled job checks it for every active binding. If the record is missing or its value changes, the tenant receives a warning and a grace period before routing stops. The re-check can detect a missing or changed record, but it does not prove the tenant can still update the DNS zone.
Use this when:
Custom hostnames carry sign-in pages or emailed links
Tenants churn often enough that stale bindings accumulate
Trade-off: customers may delete records that seem unused. Re-verification should include a grace period and a warning email before routing stops, to avoid causing an outage.
Default to standing verification. Without it, if a domain changes owners, it will still route to the old tenant. The rest of this post assumes you are using a dedicated challenge and standing verification.
Golden path
Tenant admin submits the hostname → API normalizes the name and records the claim → tenant admin publishes the challenge record → verifier resolves the challenge from public resolvers and creates the verified binding → certificate manager orders the certificate for the verified binding → edge starts routing when the binding and certificate are both ready → a scheduled job re-verifies active bindings → release turns off routing before the name can be claimed againRelated patterns:
Designing a Safe Account Linking Flow applies the same rule to login methods: an identifier attaches to an account only through a verified record
For a claim flow built on an emailed token instead of a DNS record, see Designing a Safe Team Invitation Flow
For the control-plane registry this post's binding registry is modeled on, see Multi-Tenant File Sharing: Secure Control Plane Architecture
Core design
Claim and binding records
A claim and a binding are separate rows. The claim is short-lived: it records the tenant, the hostname, the challenge token hash, and an expiry.
The binding is created when a proof succeeds. Minimum fields:
binding_id: opaque identifiertenant_id: the tenant that receives this hostname's traffic; immutable, a change of tenant is a release plus a new claimhostname: the canonical form (lowercase, A-label, no trailing dot)is_wildcard: whether this binding covers one label under the parent (*.customer.commatchesapp.customer.com, nota.b.customer.com); wildcard claims store the parent name inhostnamedisplay_hostname: what the customer typed, display onlystate:verified,active,suspended,releasedchallenge_token_hash: the value the standing re-check expectsrouting_target: the per-tenant target the customer points DNS atcertificate_ref: the certificate order or object for this hostnameverified_at,last_checked_atcreated_by: the admin who filed the claim
The unique constraint is on (hostname, is_wildcard) and holds from the moment the binding is created until the release cooldown ends. The wildcard-overlap check runs in the same transaction that creates the binding.
Claim
Normalize the submitted name before using it as a lookup key:
Reject anything that is not a bare hostname: schemes, ports, paths, IP literals
Run the same domain-to-ASCII conversion browsers use, which lowercases and converts Unicode labels to
xn--A-labelsStrip a single trailing dot
Enforce label shape: 63 octets per label, 253 total, no empty labels
Refuse names at or above the public-suffix boundary
The serve path runs the same function before binding lookup.
Filing a claim returns the challenge record and the routing target. The verifier checks the exact challenge name using public recursive resolvers and compares the token to the stored hash. If the proof is successful, the binding is created. If two tenants try to prove the same unclaimed name at the same time, the first one to succeed wins, and the second fails due to the unique constraint. If a name is already bound, a new claim is treated as a transfer, which is handled during release.
For a wildcard claim (*.customer.com), strip the leading *. first and normalize the parent name; the wildcard flag is its own field. At the edge, an exact binding wins over a wildcard binding for the same name.
Serve
The edge serves a list of hostnames with active bindings, and uses SNI to select the right certificate during the handshake. For each request, the edge reads the requested hostname (from the Host header in HTTP/1.1 or :authority in HTTP/2 and HTTP/3), looks up the binding, and passes the tenant_id and binding_id to the application. The platform's shared domain is also included in the serving set, with its own certificate.
Release
Release happens in this order:
The binding leaves
activeand the edge drops the hostname from the serving setCertificate renewal stops
The row enters a cooldown, during which the name stays reserved; the cooldown does not end until every edge node has stopped serving the name
A new claimant with a fresh proof can skip the rest of the cooldown, but their binding does not go live until the edge has stopped serving the old one; this is how a legitimate migration gets through
A claim for a name that is already bound works the same way: when the new claimant's proof succeeds, the old binding is released and its tenant is notified.
Suspended bindings keep their reservation on the name and return to active on a successful re-check.
Minimal API shape
POST /tenants/{id}/domain-claims → 201 { claim_id, challenge_record, routing_target }
GET /tenants/{id}/domain-claims/{cid} → { hostname, state, expires_at, binding_id? }
POST /tenants/{id}/domain-claims/{cid}/verify → 202 (triggers a check)
GET /tenants/{id}/domains/{bid} → { hostname, state, last_checked_at }
DELETE /tenants/{id}/domains/{bid} → 204 (starts the release sequence)
Clients submit a hostname; every other field of the binding comes from the server.
Security properties of the DNS challenge
A passed challenge proves:
The claiming tenant controlled the hostname at the moment of verification
Someone could write to the customer's DNS zone, or to a zone the challenge name was delegated to
It does not prove:
That the write access still exists after the check
That the writer is the domain's registrant; a hosting panel or DNS provider account with write access passes the same check
That the customer got the certificate themselves: once traffic routes to your edge, your platform passes the CA's challenge on its own
A managed certificate only shows that traffic reaches your edge. Your own challenge is what ties the name to a tenant, which is why certificate orders wait for a verified binding.
Threat model
Baseline assumptions
Tenant admins are authenticated; the hostnames they submit are untrusted input
The customer's DNS zone is outside your control and can change at any time after verification
The control plane derives tenant context from the session, not from the submitted hostname or the request's Host header
The edge terminates TLS and can rebuild its serving set from the binding registry
Standard infra controls such as TLS configuration, WAF, and database AuthN are assumed to be in place. This model focuses on the hostname-to-tenant binding lifecycle
A note on risk
This table is not a checklist. Focus on preventing the highest-impact failures first. Detection and response are acceptable where prevention is impractical.
Phase 1: Claim
Focus: Binding a hostname to the tenant that controls it
Asset | Threat | Baseline Controls | Mitigation Options | Risk |
|---|---|---|---|---|
Tenant binding | Pre-claim squat: A hostname points at the platform before its owner registers it (DNS staged early, setup docs that say point first), and whichever tenant submits the name first receives the binding and its traffic | None | 1. Fresh challenge: verify a platform-generated token published in the customer zone before any claim activates 2. Tenant-scoped record: put the claim identity in the record name or value, with a random token per claim | High |
Onboarding availability | Claim parking: Any tenant files a claim for a hostname it cannot prove, and a claim that reserves the name blocks the real owner from onboarding for as long as the claim lives | None | 1. Non-exclusive claims: let several tenants hold claims on one name and hand out the unique binding only on proof 2. Claim expiry: drop unproven claims after 72 hours 3. Rate limits: cap open claims per tenant | Medium |
Certificate issuance | Cert in the claim window: Managed certificate automation answers the CA's challenge from the platform's own edge once traffic routes there, so a claim that never verified still yields a valid certificate for the name | CA domain-control validation | 1. State gate: create certificate orders only for bindings in 2. Registry-sourced orders: the issuance job reads the hostname from the binding row, never from the claim request 3. Issuance alerting: alert on any certificate order for a hostname without a verified binding | High |
Binding uniqueness | Double binding: The claim path stores the hostname as typed while the serve path indexes what clients send (lowercase A-labels without a trailing dot), so the binding that verified is not the binding the serve path finds, and a second encoding of the same name is free for another claim | Exact-string uniqueness | 1. One canonical form: run the same domain-to-ASCII conversion on both paths and key the uniqueness constraint on its output 2. Display split: keep the typed form as display metadata only | Medium |
Overlapping claims | Wildcard shadow: A tenant claims | Exact-hostname uniqueness | 1. Parent proof: verify wildcard claims at the parent name 2. Same-owner parent: reject wildcard and exact bindings under one parent that belong to different tenants 3. Claim floor: refuse claims at or above the public-suffix boundary | Medium |
Verification integrity | Forged DNS answer: An attacker who can poison or intercept the verifier's DNS resolution makes the expected token appear for a name they do not control, and the binding verifies | Random per-claim token | 1. DNSSEC validation: for signed zones, an answer counts as proof only if validation succeeds 2. Independent paths: corroborate first-time claims and transfers from a second network vantage point 3. Indeterminate on error: a resolver failure is neither a pass nor a fail | Medium |
Phase 2: Serve
Focus: Routing every request through the verified binding
Asset | Threat | Baseline Controls | Mitigation Options | Risk |
|---|---|---|---|---|
Tenant routing | Host-picked tenant: The application resolves the tenant from the request authority before checking that the hostname is a registered binding, so any client-controlled value selects the tenant context | Edge terminates TLS per hostname | 1. Binding-first dispatch: reject a request whose Host has no active binding before any tenant code runs 2. Header hygiene: strip or overwrite forwarded host headers between edge and application so internal services see only the binding's hostname 3. Registry-sourced links: build password-reset and invite links from the binding record's hostname | High |
Unbound hostnames | Catch-all serving: A default virtual host or a wildcard platform certificate answers for names with no binding, so any hostname pointed at the platform serves content and completes TLS without a claim | None | 1. Refuse the handshake: abort the TLS handshake for SNI values without an active binding 2. 421 on established connections: return | Medium |
Connection reuse | Per-connection tenant: The edge resolves the tenant once from the SNI and caches it on the connection, so a reused connection carries requests for a different bound hostname to the first tenant's application | None | 1. Per-request lookup: resolve the binding from each request's authority, whatever SNI opened the connection 2. Coverage check: return | Medium |
Phase 3: Drift and release
Focus: Catching ownership changes and cleaning up on release
Asset | Threat | Baseline Controls | Mitigation Options | Risk |
|---|---|---|---|---|
Released hostname | Leftover-record re-claim: The customer's routing record stays up after the tenant releases the hostname, and a claim flow that accepts the routing record as proof activates the name for a different tenant | None | 1. Cooldown: hold released names in a reserved state for a set period, with a fresh proof as the early exit 2. Re-claim alerting: alert when a hostname released by one tenant is claimed by another | High |
Active binding | Silent transfer: The customer's domain expires or is sold, and the binding keeps routing the name to the old tenant because nothing re-checks control after the first proof | One-time verification at claim | 1. Scheduled re-verification: re-resolve the challenge for active bindings 2. Grace window: warn the tenant and keep routing through a short window before suspending 3. Watch the pointing record: alert when the hostname stops resolving to your target | Medium |
Certificate and edge state | Teardown gap: Release removes the registry row but the certificate keeps renewing or an edge node serves from a stale copy of the set, so the platform still answers for a hostname it released | Registry drives the serving set | 1. State-coupled renewal: renew only for 2. Reissue shared certificates: cut the released name from any multi-name certificate at release | Medium |
Lifecycle audit | Attribution gap: A hostname moves between tenants through a support transfer, and the record shows the new owner but not who approved it or which proof was checked | State transitions logged | 1. Full transitions: record the actor, both states, and the proof that satisfied the check on every change 2. Transfer reason: require a ticket reference on manual transfers | Low |
If you use the routing record as proof
If the CNAME that routes traffic is also the ownership proof, the trade-offs shift:
The pre-claim and post-release squats come back unless every routing target is unique per tenant; with a shared target, any tenant can claim any name that already points at you
Apex domains usually resolve to shared IPs that do not name a tenant, so they still need the TXT challenge
The routing record is also the only warning you get that ownership changed; when it stops pointing at your target, treat the binding as unverified
A transfer between two of your own tenants changes nothing in DNS, so there is nothing to check; require a TXT challenge or a support ticket for transfers
FAQs
Is a CNAME pointing at our platform enough to prove the customer owns the hostname?
It proves that whoever controls the zone wants traffic to reach your platform. Whether it also identifies the tenant depends on the target. A record pointing at a platform-assigned target unique to the tenant proves the same thing a TXT challenge would; a record pointing at a shared target does not say which tenant the customer meant. Shared targets and apex domains need the TXT challenge, and so does any re-claim of a previously bound name.
What happens when a customer deletes the challenge record after setup?
Under one-shot verification, nothing; the binding stays valid until released, and nobody notices the record is gone. Under standing verification, the next scheduled check fails and a warning email goes out; the binding stays active through a grace period before routing stops. Pick the grace period with support in mind: long enough that a DNS mistake can be fixed after the warning, short enough that a domain that actually changed hands stops serving within days.
Verification checklist
Claim intake
A name claimed with mixed case or Unicode labels resolves to the same binding row the serve path looks up
Two tenants cannot hold bindings for the same canonical hostname
A pending claim by one tenant does not stop another tenant from claiming and proving the same hostname
A claim for
co.ukor another public suffix is rejected at intakePublishing the correct token after the claim expired does not create the binding
Proof of control
A challenge token issued to one tenant does not verify a claim by another tenant, even for the same hostname
The verifier resolves the exact challenge name; a token published at the parent zone does not pass a child claim
A wildcard claim fails while another tenant holds a verified hostname one label below the wildcard's parent
Concurrent proofs for
*.customer.comandapp.customer.comfrom different tenants create at most one binding
Serving
A TLS handshake for an unclaimed hostname fails with no certificate presented
Reusing a connection opened for one hostname to request an unbound hostname returns 421
Requests for two bound hostnames on one reused connection each reach their own tenant
The application receives
tenant_idfrom the routing layer; removing every Host-header read below the router breaks nothingPassword-reset and invite links carry the binding record's hostname even when the request arrived with a different Host
Certificates
Pointing DNS at the platform without completing verification produces no certificate order
Suspending or releasing a binding stops its renewal orders
After a release, the certificate presented for surviving hostnames on a shared multi-name certificate no longer lists the released name
Lifecycle
In the release sequence, the edge stops answering for the hostname before the name becomes claimable
Re-claiming a hostname another tenant released fails until a new challenge token is published and verified
The old tenant's standing token cannot move an active hostname to a different tenant; a transfer verifies only against the claimant's fresh challenge
After a transfer verifies, the new tenant receives no traffic until the old edge mapping is gone
Deleting the challenge record for an active binding produces a warning to the tenant before routing stops
When every configured resolver times out, active bindings stay active and an alert fires for the verifier itself
Every state transition records the actor and the old and new states
Implementation & Review
The full threat model matrix, architectural diagrams, and a printable verification checklist for this pattern are available in the Secure Patterns repository. Use these artifacts to guide your design reviews and internal audits.
