Vercel PR Preview Custom Domains, Without the Add-on
We deploy our frontend previews to Vercel. Every pull request gets a fresh deployment, and we use those for end-to-end tests and for sharing work in progress. By default, Vercel gives each one a *.vercel.app URL. Most of the time that is perfectly fine.
Most of the time. We hit a case where we really needed the previews to live on our own domain. Our authentication is handled by Clerk, and Clerk's session cookies are first-party to our root domain. A frontend on *.vercel.app is a foreign origin to that root, so the session a user has on the frontend cannot follow them into the backend. The handshake breaks, and our admin sign-in test never reaches the dashboard.
We hit the same wall on the backend side first and wrote it up here: Railway PR Preview Custom Domains. This article is the Vercel half.
What Vercel offers
Vercel does have an official answer for this. It is called the Preview Deployment Suffix. You give it a domain, switch its nameservers to Vercel, and every preview URL gets that suffix instead of .vercel.app. Clean and automatic.
The catch is the price. It is a paid add-on on top of Pro, listed at $100 per month on Vercel's pricing page. For a small team that just wants nice preview URLs in CI, that is a lot of money for what is essentially a string rewrite.
We wanted the same result without the bill. It turns out you can have it.
The free path
The recipe is short. You add *.preview.yourdomain.com to your Vercel project once. Then for each PR, you run:
vercel alias set <deployment-url> pr-<N>.preview.yourdomain.com --token "$VERCEL_TOKEN"
That command attaches a specific preview deployment to a specific subdomain. The TLS certificate is issued for you. The next time the same PR pushes a new deployment, you run the same command again, and Vercel quietly detaches the previous deployment and reattaches the alias to the new one. No special re-run logic and no flaky cleanup.
This is all in vercel alias, and there is even a short walk-through called How to alias a preview deployment using the CLI. None of it requires the paid add-on.
So what does the add-on actually buy? It does one thing the free path does not: it automatically rewrites every preview URL Vercel generates. With the alias approach, you do that rewriting yourself in CI. For us that is one CLI line per PR. We can afford that.
The nameserver thing
This is the part we want to be loud about, because it is where the Vercel dashboard lost us for a while.
When you add a wildcard domain like *.preview.yourdomain.com to a Vercel project, the dashboard pushes you toward what it calls the "Nameservers" method. The add-a-domain doc is explicit about this: "If using your custom domain as a wildcard domain, you must use the nameservers method for verification." The reason is real. Vercel needs to solve the ACME DNS-01 challenge to issue a wildcard certificate, and the standard way to do that is to control the zone.
The problem is that "control the zone" reads as "change the nameservers on the entire domain." That is how nameservers work in 99% of how-tos. We were not about to do that. Our domain is registered through Railway, which manages the DNS for our backend's per-PR subdomains automatically. Handing the whole zone to Vercel would have broken all of that.
The trick — and we had never seen it spelled out plainly — is that you do not have to delegate the whole domain. You can delegate just one subdomain: the one Vercel uses for the ACME challenge. There is a KB page that documents exactly this, and it is where every part of the puzzle finally clicks: Preview Deployment Suffix without Vercel Nameservers.
Here are the exact records you add at your DNS provider:
NS _acme-challenge.preview ns1.vercel-dns.com.
NS _acme-challenge.preview ns2.vercel-dns.com.
CNAME *.preview cname.vercel-dns-0.com.
The wildcard CNAME routes the traffic. The two NS records on _acme-challenge.preview carve out one tiny sub-zone and hand it to Vercel, just enough for Vercel to answer the cert challenge and renew it later. Everything else in your DNS stays exactly where it was.
If you have only ever set nameservers at the registrar for an entire zone, delegating a child like this feels wrong on first read. It is not. Child NS records are how DNS delegation has always worked; you are just rarely asked to use them at this level. Any DNS provider with a real records editor supports them.
NOTE: The Vercel dashboard will keep nudging you toward switching the whole domain's nameservers. Ignore it. Add the three records above instead, give DNS a moment to propagate, and the wildcard will verify on its own.
The "domain in limbo" thing
This one caught us off guard after everything else was working.
Once the wildcard is added and verified, the dashboard insists that it be pointed at something. You cannot add the wildcard and let it sit. Your options are:
- Assign it to the production branch
- Assign it to the preview branch (a specific git branch)
- Assign it to a specific deployment
- Configure it as a redirect to another domain
None of those quite matched what we wanted. We wanted the wildcard to behave as a pool — a set of names that per-PR aliases would carve subdomains out of. There is no "leave it alone, the CLI will claim subdomains under it later" option.
What we did is the fourth one: we set the wildcard to redirect to our production domain. So if a stray request lands on a subdomain that does not have an active per-PR alias attached, the visitor ends up on production. Harmless. And Vercel is happy, because the wildcard is "linked to something."
It is a small thing, but it is a real gotcha. Without it, the wildcard sits in an "Invalid Configuration" state in the dashboard even though the DNS is perfect, and you spend a frustrating half hour second-guessing the NS delegation you just set up.
What this looks like in CI
Once the wildcard, the DNS, and the "linked to something" steps are done once, the per-PR work is two lines. We capture the deployment URL from vercel deploy, then pass it to vercel alias set:
DEPLOYMENT_URL=$(vercel --yes --token "$VERCEL_TOKEN")
vercel alias set "${DEPLOYMENT_URL#https://}" \
"pr-${PR_NUMBER}.preview.yourdomain.com" \
--token "$VERCEL_TOKEN"
On PR close, one more line frees the subdomain:
vercel alias rm "pr-${PR_NUMBER}.preview.yourdomain.com" \
--yes --token "$VERCEL_TOKEN" || true
The || true is there in case the alias was never created (a PR that closed before the deploy step ever ran). And re-runs of the same PR are already handled for us — vercel alias set is happy to take an existing alias from a previous deployment and move it to the new one. You do not need to remove first.
Wrapping up
A few things we wish Vercel's docs had said louder:
- You do not need the Preview Deployment Suffix add-on to get clean preview URLs.
vercel alias setdoes the same job. The add-on automates the rewrite; if you are already in CI, you can do that yourself for free. - You do not need to switch the whole domain's nameservers either. Two
NSrecords on_acme-challenge.<your-subdomain>and a wildcardCNAMEare enough for Vercel to issue and renew the cert. - Vercel will not let your wildcard sit unassigned. Pick one of the four options. We used a redirect to production, which keeps things tidy.
The official solution is real and works, but it leans on a paid add-on and a zone-wide nameserver change. The free path is fully documented, but only in a KB page you have to know exists to find. Hopefully this writeup means someone else does not lose an afternoon to it.
If you run into the same problem on the backend side, we covered Railway here: Railway PR Preview Custom Domains.
Thank you for reading! If you have any questions or feedback, please feel free to contact us at hi@davette.ca.