Wildcard and apex records for per-tenant subdomains
One wildcard beats ten thousand records, until it doesn't. The trade-offs against per-tenant records, and why the apex needs a different record type.
If every customer gets their-name.yourapp.example, you have two choices on Infrai: write one wildcard record with PUT /v1/dns/record/upsert and let every subdomain resolve, or write a record per tenant as they sign up. The wildcard is one call for all time; per-tenant records are one call per customer and give you something the wildcard can’t — the ability to say no.
Both are legitimate. The decision is about whether an unknown subdomain should resolve.
The wildcard version
curl -sS -X PUT "https://api.infrai.cc/v1/dns/record/upsert" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"zone_id": "3a9d545444dc7a02e085889a3f713078",
"record_type": "CNAME",
"name": "*.yourapp.example",
"content": "edge.yourapp.example",
"ttl": 300,
"proxied": true
}'
{
"ok": true,
"data": {
"record_id": "rec_9wQ1zV6pLkS3dHyB",
"zone_id": "3a9d545444dc7a02e085889a3f713078",
"record_type": "CNAME",
"name": "*.yourapp.example",
"content": "edge.yourapp.example",
"ttl": 300,
"priority": null,
"proxied": true
}
}
One record, every subdomain. northwind.yourapp.example resolves, and so does definitely-not-a-tenant.yourapp.example — which your edge then has to reject, because DNS said yes.
That’s the trade in one sentence: the wildcard moves tenant validation from DNS to your application.
The apex is a different record type
A CNAME at the zone apex — yourapp.example with no label in front — is invalid DNS. Not discouraged, invalid. So the marketing site on your bare domain needs an A (or AAAA) record instead:
curl -sS -X PUT "https://api.infrai.cc/v1/dns/record/upsert" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"zone_id": "3a9d545444dc7a02e085889a3f713078",
"record_type": "A",
"name": "yourapp.example",
"content": "203.0.113.10",
"ttl": 300,
"proxied": true
}'
And note that a wildcard doesn’t cover the apex either — *.yourapp.example matches anything.yourapp.example and not yourapp.example itself. Two records minimum for any product that serves both.
Nor does a wildcard match multiple labels in every resolver’s view consistently, so tenant.region.yourapp.example wants its own *.region.yourapp.example rather than an assumption.
Wildcard versus per-tenant, honestly
| Concern | Wildcard | Record per tenant |
|---|---|---|
| DNS calls | one, ever | one per signup, one per churn |
| Unknown subdomain | resolves; your app must reject | doesn’t resolve at all |
| Certificate | needs a wildcard certificate | per-hostname works |
| Tenant inventory in DNS | none — DNS knows nothing | GET /v1/dns/record/list is the list |
| Signup latency | zero DNS wait | propagation before first use |
| Someone squatting a subdomain | your app’s problem | never resolves |
The row that decides it for most products is the second one. If an unknown subdomain resolving to your edge is harmless — you return a clean 404 and nobody can do anything with it — the wildcard is simpler and there’s no reason to write ten thousand records. If a resolving hostname is itself a problem, because of certificate issuance, cookie scope or phishing that borrows your domain, per-tenant records are the stronger boundary.
Per-tenant, done idempotently
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
ZONE_ID = os.environ["ZONE_ID"]
BASE = "yourapp.example"
EDGE = "edge.yourapp.example"
RESERVED = {"www", "api", "admin", "app", "mail", "smtp", "ftp", "static", "assets",
"docs", "status", "support", "help", "blog", "cdn", "edge"}
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
def valid_label(label: str) -> bool:
"""Reject anything that would collide with your own infrastructure or that DNS
won't accept. A tenant who signs up as 'api' takes your API hostname."""
if label in RESERVED or len(label) > 63 or not label:
return False
return all(c.isalnum() or c == "-" for c in label) and not label.startswith("-") and not label.endswith("-")
def add_tenant(label: str) -> dict:
if not valid_label(label):
raise ValueError(f"unusable subdomain label: {label!r}")
resp = SESSION.put(
f"{API}/v1/dns/record/upsert",
json={"zone_id": ZONE_ID, "record_type": "CNAME", "name": f"{label}.{BASE}",
"content": EDGE, "ttl": 300, "proxied": True},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]
def remove_tenant(label: str) -> bool:
listed = SESSION.get(f"{API}/v1/dns/record/list",
params={"zone_id": ZONE_ID, "name": f"{label}.{BASE}"}, timeout=25)
listed.raise_for_status()
for record in listed.json()["data"].get("records", []):
SESSION.delete(f"{API}/v1/dns/record/delete",
params={"zone_id": ZONE_ID, "record_id": record["record_id"]}, timeout=25)
return True
if __name__ == "__main__":
print(add_tenant("northwind"))
The reserved list is the part people add after the incident. Someone signing up as admin or mail and getting admin.yourapp.example is a problem you can only fix by taking their subdomain away.
Certificates are the hidden coupling
Whichever you choose, TLS has to agree. A wildcard DNS record needs a wildcard certificate, and wildcard certificates cover one label only — *.yourapp.example does not secure tenant.region.yourapp.example.
With proxied: true the provider terminates TLS, which removes most of this concern and is a large part of why the flag exists. Without it, per-hostname certificates plus per-tenant DNS records is the combination that behaves predictably, at the cost of an issuance step in your signup flow.
Limitations
The record types available here are A, AAAA, CNAME, TXT and MX. That leaves out the non-standard apex conveniences some providers sell — Route 53’s alias records and Cloudflare’s CNAME flattening both let an apex behave like a CNAME, and neither is exposed through this API — so the apex needs a stable address you commit to keeping. That commitment is the real constraint: an A record at your apex means that address can’t change with your infrastructure, and if your edge addresses do move, going direct to one of those providers is the better fit.
There’s also no per-record tenant tagging, so DNS alone won’t tell you which customer a subdomain belongs to. Keep that mapping in your own store and treat GET /v1/dns/record/list as the inventory of hostnames rather than of customers.
What’s easier than with a separate DNS vendor is the rest of the signup. The tenant’s subdomain record, the database branch it gets from POST /v1/db/branch/create, the bucket from POST /v1/storage/bucket/create and the welcome email from POST /v1/email/send are one credential and one invoice — so provisioning a tenant is one script with one secret rather than four vendors’ tokens in your onboarding worker. DNS routes report billing_class: free in discovery (verified 2026-09-21), and platform rates drift downward as vendor contracts improve.