Vercel: custom preview domain for free?
April 6, 2023
April 6, 2023
If you host your app on Vercel, you must be familiar with how preview deployments are a first-class citizen, and each pull request you make gets a preview deployment.
Those preview deployments default to be hosted on vercel.app
, and
Vercel provisions two preview domains for it, with the following
patterns:
{app}-{id}-{org}.vercel.app
{app}-git-{branch}-{org}.vercel.app
So if you make a PR on an app called my-app
in an organization
my-org
, on a branch called hello-world
, and the deployment ID for
this commit was lkj8trp27
, your URLs would be:
https://my-app-lkj8trp27-my-org.vercel.app/
https://my-app-git-hello-world-my-org.vercel.app/
But what if you want, for example, to allow OAuth on your preview
domains? Whitelisting vercel.app
is out of the question since it would
allow any Vercel website (including an attackerâs website) to be a
valid redirect URI for our OAuth provider!
And we canât typically whitelist a domain pattern like *-my-org.vercel.app
,
not that this would be a good idea anyway because this pattern is
not private to your organization. Any random Vercel user can use
it in their own app!
Then, it would be useful to use your own domain instead of vercel.app
.
Turns out Vercel supports this, and charges $100/month
for it! Steep.
Steep, but if youâre looking for a turnkey solution, itâs definitely worth it. Otherwise, keep reading.
An interesting thing in the Vercel CLI is that it lets us manually
associate a custom domain to a given deployment using
vercel alias
. đ
Letâs say codejam.info
is part of my Vercel-managed domains:
vercel alias set my-app-lkj8trp27-my-org.vercel.app hello-world.preview.codejam.info
This will associate the deployment example from earlier to my custom domain!
We can literally put anything we want under codejam.info
there, and it
will happily generate a SSL certificate for that arbitrary subdomain,
and associate it to our deployment.
This will work as long you associated a wildcard subdomain on your DNS
to Vercel, like *.preview.codejam.info
in our example.
This is a good start, but it doesnât scale!
Luckily, the Vercel API exposes an endpoint to do just the same thing:
POST /v2/deployments/{id}/aliases
.
In our example, we can call it with:
{
"alias": "hello-world.preview.codejam.info"
}
How do we find the deployment ID though? We need the full deployment ID,
something looking like dpl_hgLKkCqMExSzNpTtA3Dy6sVfWuYj
.
Vercel gives us a handy GET /v13/deployments/{idOrUrl}
endpoint for this, where we can pass our deployment URL and get the
deployment object back, including its full id
.
By combining those two endpoints, we can dynamically associate our custom domain to any Vercel preview deployment. đ
In order to call the API, we need to pass a bearer token in the
Authorization
header. You can create a token from your
Vercel account settings.
Then, you can put it in your appâs environment variables, e.g. as
VERCEL_TOKEN
, so itâs available in your server-side code environment.
From there, we can detect when weâre running under a vercel.app
preview domain, call the API to associate our own custom domain, and
finally redirect to it. This will add a bit of delay when loading our
preview deployments from the vercel.app
domains, but no big deal.
Where you hook in order to do that is up to you. _app.jsx
may be a
good start, or maybe some component thatâs included in all of your
pages, maybe just the home page if you donât expect any deep link on
your vercel.app
preview domains, or maybe even somewhere in
getServerSideProps
?
If you do this client-side, youâll want to add an API route or go
through a SSR page that will be doing the call to the Vercel API (you
donât want to expose your Vercel API token client-side), but if youâre
hooking directly in getServerSideProps
, you can skip that step.
On the client, you could do something like this:
const router = useRouter()
if (location.host.endsWith('-my-org.vercel.app')) {
// Preview env
router.push('/preview-redirect')
}
Then, implement a preview-redirect
page to associate your custom
domain to the current preview environment, then redirect to it.
export async function getServerSideProps (context) {
const deployment = await getDeployment(context.req.headers.host)
const domain = `pr-${deployment.gitSource.prId}.preview.codejam.info`
await associateDomainToDeployment(deployment.id, domain)
return {
redirect: {
destination: `https://${domain}`
}
}
}
Where getDeployment
is a wrapper to GET /v13/deployments/{idOrUrl}
,
and associateDomainToDeployment
wraps POST /v2/deployments/{id}/aliases
(writing those is left as an exercise to the reader).
Here, I chose to prefix the domain with pr-
and the PR number, but
youâre free to construct your preview domains however you want.
Youâll notice this works the first time, but obviously if you open again
the vercel.app
preview URL, it will fail because the domain was
already assigned! To cover that, you need to call /v2/deployments/{id}/aliases
and redirecting to the existing domain if you already associated it
before.
We can add something like this in the beginning of our previous function:
const aliases = await getDeploymentAliases(deployment.id)
const existingDomain = aliases.find(alias =>
alias.alias.endsWith('.preview.codejam.info')
)
if (existingDomain) {
return {
redirect: {
destination: `https://${existingDomain.alias}`
}
}
}
After this, you should have your free custom preview domains working, congrats!
However, you may realize this is bloating your domainâs SSL certificates list on Vercel. Every single preview deployment will add a new entry in your SSL certificates list, and because the Vercel UI for this doesnât really expect an infinitely growing list of certificates, itâll make it a pain for you to manage your âactualâ certificates!
To prevent this, you need to manually create a wildcard certificate for
the domain you use for your preview deployments. In the example featured
in this post, that would be *.preview.codejam.info
.
Vercel is smart enough to notice when we associate a new domain to a deployment, that a wildcard certificate covering it already exists, and so doesnât create an individual certificate for that particular preview. This will keep your certificates list clean and tidy!
You canât create the wildcard certificate from the dashboard directly,
but you can do so with the CLI using vercel certs issue
.
vercel certs issue '*.preview.codejam.info'
Note that this will only work if you use Vercelâs nameservers. This
means the following wonât work (e.g. in the codejam.info
DNS zone):
*.preview CNAME cname.vercel-dns.com
But the following will work:
preview NS ns1.vercel-dns.com
preview NS ns2.vercel-dns.com
If you made it here, congrats! You now have everything you need in order to implement your own custom preview domains, without paying Vercel big money for it.
Is going through all of this worth saving $100/month? Thatâs up to you. But as far as Iâm concerned, the joy of putting together this little system was well worth the savings. đ