all posts

How to add a custom domain to your app

Ajay Kumar··8 min read

Nobody ships production on a URL with a UUID in it. Attaching your own domain is the last step before an app is real, and it is genuinely simple: prove you control the name, point it at the platform, and let the certificate be issued for you. Everything that goes wrong goes wrong in DNS.

Here's the whole flow, plus the failure modes, in the order you'll meet them. The commands are PandaStack's because that's what I build; the DNS parts are universal.

Step 1: attach the domain

Adding the domain to the app is what generates the records you need — the verification token is per-domain, so there's nothing to copy from documentation.

curl -X POST https://api.pandastack.ai/v1/apps/$APP_ID/domains \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "app.acme.com"}'

Or from the SDK, if you're automating this per customer:

from pandastack import Client

ps = Client()
d = ps.apps.add_domain(app_id, "app.acme.com")

for r in d["records"]:
    print(f"{r['type']:6} {r['name']}  ->  {r['value']}   ({r['purpose']})")

# TXT    _pandastack-verify.app.acme.com  ->  <token>   (verification)
# CNAME  app.acme.com                     ->  ingress.pandastack.ai  (routing)

Step 2: create the two records

One TXT record proving you control the name, one CNAME sending traffic to the platform's ingress host. Create both at your DNS provider exactly as returned.

The verification record exists to stop someone else claiming your hostname. That matters more than it might seem on a multi-tenant platform: without proof of control, anyone could reserve a domain they don't own and intercept it the moment its DNS ever pointed there.

Set the TTL on both records low — 60 to 300 seconds — while you're setting this up. If you get a record wrong at a one-hour TTL, you wait an hour to find out you fixed it. Raise it afterwards if you care to.

Step 3 (apex only): the CNAME you can't create

If you're attaching a bare apex — acme.com rather than app.acme.com — you'll hit a rule that predates all of this: an apex can't hold a CNAME, because it must also hold the zone's SOA and NS records and CNAME cannot coexist with other records at the same name. That's DNS, not the platform.

Every provider offers a workaround, under a different name:

  • ALIAS or ANAME — a synthetic record that resolves the target and answers with its addresses. Route 53 calls it an alias record; DNSimple and NS1 call it ALIAS.
  • CNAME flattening — Cloudflare accepts a CNAME at the apex and flattens it at serve time.
  • As a last resort, A records pointing at the ingress host's addresses — which you should avoid, because those addresses can change and your domain breaks silently when they do.

The TXT verification record works identically either way; it lives on a subdomain label, so the apex restriction never applies to it.

Step 4: watch it go active

From here it's automatic. A reconciler checks for your TXT record, and once it's found, requests the certificate and polls until it's issued. The domain moves through three states.

curl -s https://api.pandastack.ai/v1/apps/$APP_ID/domains/app.acme.com \
  -H "Authorization: Bearer $PANDASTACK_API_KEY" | jq '{status, dns_note, error}'

# {"status": "pending",  "dns_note": "TXT not found yet"}
# {"status": "verified", "dns_note": ""}          <- cert issuing
# {"status": "active",   "dns_note": ""}          <- serving

pending means it's still waiting on your TXT record. verified means ownership is proven and the certificate is being issued, which usually takes minutes. active means it's serving traffic over TLS and you're done.

The dns_note field is the one to read when something is stuck — it reports what the platform currently observes, so you get 'DNS points at X, expected Y' rather than a generic failure.

The four ways it stalls

Almost every stuck domain is one of these.

The TXT record is at the wrong name. Some DNS UIs append the zone to whatever you type, so entering the fully-qualified name produces _pandastack-verify.app.acme.com.acme.com. Check what was actually created rather than what you typed:

dig +short TXT _pandastack-verify.app.acme.com
dig +short CNAME app.acme.com

# Bypass your resolver's cache and ask the authoritative server directly
dig +short TXT _pandastack-verify.app.acme.com @ns1.yourprovider.com

An old record is still there. An A record left over from a previous host wins over the CNAME you just added, or an existing CNAME still points at your old platform. Delete the old records; don't just add new ones alongside.

Proxying was on during setup. If your domain sits behind another proxy, the platform may not be able to see the DNS it needs to verify. Switch the record to DNS-only while activating, then turn proxying back on once the domain reads active.

The domain is already live on another app. A hostname can serve one app at a time, and the first verified owner holds it. If you're moving a domain between apps, detach it from the old one first.

Once it's active

The domain is stable across deploys — that's the point of it. Blue-green deploys swap the sandbox behind the app; the domain keeps pointing at the app, not at any particular instance of it. Scale-to-zero and wake-on-request behave the same on the custom domain as on the default URL.

You can delete the verification TXT record once the domain is active, if tidiness matters to you. A working production domain is not torn down because a verification record was cleaned up later.

Renewal is not your problem. The certificate is issued and renewed at the edge that already fronts the traffic, so there's nothing in your infrastructure with an expiry date on it — which removes the single most common cause of a Saturday-morning outage on a small team.

The short version

  1. Attach the domain to the app; the response tells you the exact records to create.
  2. Create the TXT and the CNAME, at a low TTL, and delete any leftover records for that name.
  3. For an apex, use ALIAS/ANAME or CNAME flattening — an apex cannot hold a plain CNAME.
  4. Poll until status is active; if it stalls, read dns_note and verify with dig against the authoritative server.
  5. Re-enable any proxying afterwards, and leave renewal to the platform.

Frequently asked questions

Why can't I use a CNAME on my apex domain?

Because the DNS specification does not allow a CNAME to coexist with other records at the same name, and an apex must carry the zone's SOA and NS records. This is a protocol rule rather than a platform limitation, which is why every host runs into it. The workarounds are provider features: ALIAS or ANAME records, which resolve the target and answer with its addresses, and Cloudflare's CNAME flattening, which accepts an apex CNAME and flattens it at serve time. Avoid hard-coding A records to the ingress host's current addresses — those can change, and your domain breaks with no warning when they do.

My domain is stuck on pending. What do I check first?

The TXT record's actual name, because most DNS interfaces append the zone to whatever you type and it is easy to end up with the zone doubled. Query it directly with dig rather than reading it back from the provider's UI, and query the authoritative nameserver to bypass any cached negative answer from your resolver. After that, check for leftover records — an old A record from a previous host takes precedence over the CNAME you added — and check whether the record is being proxied, since a proxy in front of the name can prevent verification from seeing what it needs.

Do I need to renew the TLS certificate?

No. The certificate is issued and renewed automatically at the edge that already terminates traffic for the platform, so nothing in your infrastructure holds an expiry date. This matters more than it sounds: expired certificates are one of the most common self-inflicted outages on small teams, precisely because the renewal is somebody's job right up until that person is on holiday. You can also safely delete the verification TXT record once the domain is active — an already-active domain is not torn down because the verification record was later cleaned up.

Can two apps share the same custom domain?

No. A hostname serves one app at a time, and ownership goes to the first party that proves control of it, not the first party to type it into a form. That distinction is what prevents domain squatting on a multi-tenant platform: an unverified claim reserves nothing at all, so nobody can pre-register a hostname they do not own and wait for someone else to point DNS at it. To move a domain between your own apps, detach it from the first app and then attach it to the second.

Does the custom domain survive a deploy?

Yes, and that is the main reason to use one. Deploys are blue-green: a new sandbox is built and health-checked, then the app is atomically pointed at it and the old one is torn down. The domain is attached to the app rather than to any particular instance, so it follows the flip with no DNS change and no gap in service. The same applies to scale-to-zero — a custom domain wakes a sleeping app exactly like the default URL does, and a rollback moves the domain back with the app.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.