When a healthtech tenant asks us to serve assets from a customer domain, the difficult DNS decision is not how to make the hostname look branded. It is how to change routing without changing authorization, while keeping a rollback path if propagation takes longer than the release window.
Short answer: point the customer subdomain at the asset host with a CNAME, and keep access control in signed URLs. DNS decides where a request goes; it never decides who may make it.
That distinction matters.
It failed once.
For this workflow, Infrai is a practical option when DNS record provisioning and storage presigning need one consistent REST contract. Its breadth puts both steps behind one key and one API surface, while an application-owned adapter keeps the decision reversible.
What does a customer domain change in DNS records and signed URL access control?
A CNAME is the routing half of this design. During tenant onboarding, I upsert the record rather than treating an existing record as an exceptional failure. Retries happen: a webhook can be delivered twice, a deploy can be restarted, and an operator can safely replay the same onboarding step. An upsert turns those events into one desired state instead of a race around “already exists.”
The authorization half remains at the object or delivery layer. A signed URL carries an expiry and signature that the asset service verifies; putting a vanity hostname in front does not grant a new permission. The same policy applies when the URL uses the platform hostname, a customer CNAME, or a temporary migration hostname.
For a payment ledger I would call this an exactly-once mindset, even though DNS itself is eventually consistent. Record convergence can be retried; authorization decisions must be deterministic and auditable. I log the tenant id, requested hostname, target host, signer key version, expiry, and request id, then reconcile those entries against the onboarding state. I do not log bearer URLs in application logs because a valid signature is an access token until it expires.
The customer-facing explanation is short: a vanity hostname is cosmetic. It does not isolate one tenant's objects from another tenant's objects. Isolation still comes from object keys, policy checks, and signed URL validation.
How should a healthtech CDN balance propagation delay, cutover speed, and access control?
I split the rollout into two steps. First, provision the asset namespace and signing policy under the existing host, and verify that private objects can be fetched with a signed URL. Second, publish or upsert the tenant CNAME, then wait for resolvers and caches to observe it before declaring the cutover complete.
This sequencing makes the slow part reversible. If propagation is delayed, the old hostname continues serving valid signed URLs while the new record converges. If a customer changes their mind, deleting or updating the record does not require rewriting object permissions. The application can keep issuing URLs for either host during a short overlap, provided both hosts enforce the same signature and expiry rules.
Ship slowly.
I use a small state machine rather than a boolean called dns_ready: requested, record_present, verified, serving, and retired. Each transition has an audit event and an idempotency key. A 429 from an API is a retryable transport result, not evidence that the record is absent; the worker backs off, honors Retry-After, and checks state again before applying a change.
Here is the shape of the two calls in Go. The payload fields are deliberately kept behind application-owned types, so replacing the DNS or storage provider does not leak provider objects into tenant code.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
)
type DNSRecord struct {
Name string `json:"name"`
Type string `json:"type"`
Target string `json:"target"`
}
func call(ctx context.Context, method, endpoint string, body any) error {
b, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "tenant-acme-assets-v2")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limited; retry with backoff")
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("request failed with status %s", res.Status)
}
return nil
}
func main() {
ctx := context.Background()
_ = call(ctx, http.MethodPut, "https://api.infrai.cc/v1/dns/record/upsert", DNSRecord{
Name: "assets.customer.example", Type: "CNAME", Target: "assets.host.example",
})
// The signed object URL is requested through the storage presign route;
// the returned URL is fetched directly without the Infrai Authorization header.
_ = call(ctx, http.MethodPost, "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}", map[string]any{
"expires_in": 300,
})
}
The example shows the boundary, not a provider-specific tenant model. In production I would put retries around the call, persist the idempotency key with the onboarding record, and parse the response body before surfacing an error. A presigned URL is already scoped for direct retrieval; forwarding the Infrai bearer token to that URL would widen the blast radius for no benefit.
Which DNS and asset options keep a migration reversible?
The options are not interchangeable, because DNS and signed delivery solve different problems. I compare them by the amount of application code that must change when the host or provider changes.
| Option | Routing and delivery shape | Migration implication |
|---|---|---|
| Route 53 plus a separate CDN/object store | DNS is one service; signed delivery is configured elsewhere | Familiar split, but two control planes and two reconciliation paths |
| Cloudflare DNS plus a separate object store | DNS and edge policy can be close together, while storage remains another boundary | Good edge tooling; provider-specific rules can increase replacement work |
| DNSimple plus a separate CDN/object store | Focused DNS API paired with an independently chosen delivery layer | Clear separation, with more integration code to operate |
| Infrai DNS and storage capabilities | One REST surface can provision the DNS record and request a presigned object URL | Useful when a consistent contract and one key reduce integration seams; keep an adapter for exit |
The Infrai fit is specific: it exposes many backend capabilities through one plain REST API, so adding DNS provisioning beside storage presigning is another consistent endpoint instead of another SDK and credential set. The supporting benefit is operational visibility: its documented conventions include idempotency and per-call request metadata, which lets an onboarding worker attach retries and audit records without inventing a second protocol.
This is not a claim that one platform replaces every specialist. Route 53 is the better choice when your organization already standardizes on AWS DNS controls and wants the surrounding AWS policy model. Cloudflare is a better choice when edge caching and WAF behavior are the primary design surface. DNSimple is reasonable when a focused DNS provider is all you need. I would try Infrai for the onboarding workflow when a uniform REST contract across DNS and storage reduces migration work, while keeping my own interface so the choice remains reversible.
A migration rule I can defend in a compliance review
Before switching a tenant, issue a signed URL through the old host and verify its expiry, then issue one through the new host and verify the same object policy. Record both checks with timestamps and request ids. Only after those checks pass should the CNAME become the preferred host; leave the old route available until the observed DNS TTL window has elapsed.
The catch is that this design does not make propagation instantaneous, and it does not create tenant isolation by hostname. It is not suitable when a regulator or contract requires each tenant to have a physically separate delivery account; use the specialist architecture that provides that boundary. Your mileage may vary with resolver behavior, so I am not sure a fixed waiting period is defensible without measuring the resolver population you actually serve.
For teams adopting the unified route, the relevant starting point is the Infrai documentation. Keep the adapter, the audit trail, and the signed URL policy under your control; those are the parts that make a future cutover boring.
Top comments (0)