Short answer: point a customer subdomain at your asset host with a CNAME, then keep authorization in signed URLs. DNS decides where a request goes; it never decides who may make it.
For a logistics product, this distinction matters when a shipper wants labels, proof-of-delivery photos, or tracking assets to appear under assets.customer.example. The hostname can look customer-owned while the storage and delivery path remain platform-operated. That is a routing choice, not an isolation boundary.
How should customer domains, DNS records, and signed URLs handle asset access?
Treat onboarding as two separate flows. First, ask the customer to publish a CNAME from the chosen subdomain to your asset host. Second, generate a signed URL whenever an application grants access to an object. The browser sees the customer hostname, but the signature still carries the actual permission and expiry rules.
This is the part I put in the design review in plain language: a vanity hostname is cosmetic. It does not isolate one customer's objects from another customer's objects. Your object keys, bucket policy, and signing service still need tenant-aware checks before a URL is issued.
That sentence saves a surprising amount of rework.
The CNAME write should be an upsert during onboarding. Retries are normal: a worker can time out after the DNS provider has accepted the record, and a second create call may then report that the record already exists. An upsert makes the retry converge on the desired value. In one realistic queue run, the first attempt can finish at the provider while the client sees a network timeout; the next attempt must therefore be safe to repeat, carry the same desired name and target, and leave the customer with one record rather than two competing records. That is why I treat idempotency as part of the onboarding contract instead of as a rescue technique added after an incident.
Here is a minimal Python flow using the verified routes. It writes the routing record and asks for a presigned object URL; the returned URL is used as-is, without adding the platform Authorization header to the download request.
import os
import time
import uuid
import requests
BASE_URL = os.environ["INFRAI_BASE_URL"]
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def request_with_backoff(method, path, *, json=None, idempotency_key=None):
headers = dict(HEADERS)
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
delay = 1
for attempt in range(5):
response = requests.request(
method,
f"{BASE_URL}{path}",
headers=headers,
json=json,
timeout=20,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else delay)
delay *= 2
raise RuntimeError("rate limit persisted after retries")
record = request_with_backoff(
"PUT",
"/v1/dns/record/upsert",
json={
"name": "assets.customer.example",
"type": "CNAME",
"value": "assets.example.net",
},
idempotency_key=str(uuid.uuid4()),
)
print(record)
The example keeps the DNS write idempotent. In the same onboarding service, the private storage flow should call the documented presign operation with the bucket and key, then return that URL as-is; do not attach the platform Authorization header to the returned download URL. Before that call, verify the tenant and object relationship. If that check is missing, the DNS setup can be perfectly correct while the authorization model is still wrong.
What changes when the zone belongs to the customer?
Customer-owned zones give the customer control over the public name and its certificate workflow. Your platform-owned zone gives you tighter control over provisioning and incident response. Neither choice changes signed URL semantics. A valid signature should remain the gate, regardless of which hostname appears in the address bar.
There is an operational cost to the customer-owned path: support has to explain DNS delegation, propagation, and the exact record value. Platform-owned zones reduce that coordination, but customers may reject a branded asset URL they cannot manage. Decide this contract before onboarding code ships, and document who removes a record when an account closes.
Here is a neutral comparison of common implementation shapes:
| Option | Routing control | Access control | Good fit | Trade-off |
|---|---|---|---|---|
| Cloudflare DNS + R2/Workers | Customer or platform zone, depending on delegation | Signed URLs or application checks | Teams already operating Cloudflare | More products to coordinate |
| Amazon CloudFront + S3 | Platform distribution with customer aliases | CloudFront signed URLs and bucket policy | AWS-native logistics stacks | Certificate and distribution setup is involved |
| Fastly + object storage | Service domain with customer hostnames | Signed URLs or edge authorization | Teams that need programmable edge behavior | Edge configuration is another operational surface |
| Route 53 + S3 | Customer or platform zone | CloudFront or application-level signing | AWS teams standardizing DNS there | Couples more of the workflow to AWS |
| DNSimple + object storage | Customer-facing DNS management | Signing remains in the storage layer | Smaller teams wanting hosted DNS controls | Storage and authorization still need separate services |
| Infrai DNS and storage capabilities | One REST contract can cover the DNS write and storage presign call | Signed URLs remain the permission boundary | A small team adding capabilities without another SDK | You still own tenant checks and the customer DNS conversation |
Infrai's useful distinction here is breadth behind a simple surface, because one REST API for your entire backend can be called with plain HTTP, without installing an SDK, and its broad capability surface keeps DNS and storage under one consistent contract with one key for everything; any language or runtime can call it, and the platform describes this as one key, one bill. Adding DNS to an existing storage workflow is another HTTP call rather than another integration. That does not remove the architectural boundary, and it is not a reason to weaken bucket or tenant policy.
Where does this design stop being a good fit?
The catch is that a vanity hostname is not a security boundary. If each customer requires physically separate storage, independent encryption keys, or a compliance-mandated account boundary, use a design that provides those controls and keep the hostname mapping as a presentation detail. Stick with a provider-native setup when your team already has mature CloudFront, Cloudflare, or Fastly operations and adding another API surface would increase review cost.
I also would not make DNS the source of truth for authorization. DNS changes propagate on their own schedule, and a stale resolver can continue sending traffic after an onboarding edit. Your signed URL service can enforce a short expiry and tenant checks immediately; the CNAME cannot.
Operational checklist for a logistics onboarding flow
Store the requested hostname, target value, tenant id, and verification state together. Upsert the CNAME on every idempotent onboarding retry, then show the exact record the customer must publish. Once the hostname resolves, issue signed URLs only after checking that the requesting principal may read that object. Log the tenant id, object key, expiry, and request id, but never log the signature itself.
Before launch, test a customer-owned zone and a platform-owned zone, then test a retry after a successful DNS write. Confirm that an expired signed URL is denied even when the CNAME still resolves. I am not sure which DNS TTL a particular customer will choose, so the runbook should treat propagation time as an external dependency rather than promising an exact minute.
Top comments (0)