Custom Domains for a SaaS on Vercel: Publishing to a Subdomain or Domain
A free subdomain needs no API call. A custom domain needs add, DNS, verify, SSL and cleanup. Here is the whole publish flow, the status states and the errors to handle.

Short answer: publishing to a free subdomain is a database write, because a wildcard domain on your Vercel project already routes every subdomain. Publishing to a custom domain is a five-state flow: add the domain through Vercel's API, show the customer their DNS records, verify, let Vercel issue the SSL certificate, then mark it live. The code, the status states and the errors to handle are below. Checked against Vercel's documentation, October 2026.
What is the difference between a free subdomain and a custom domain?
A free subdomain lives under your own domain, such as store-one.yourapp.com. A custom domain is one the customer owns, such as shop.com. They need different work on your side.
A subdomain needs only a unique name in your database, because Vercel's wildcard domain already points every first-level subdomain at your deployment under one wildcard certificate. A custom domain must be added to your Vercel project, pointed at Vercel through DNS, verified, and given its own certificate. The wildcard setup is covered in hostname-based multi-tenancy on one Vercel deployment, so this article focuses on the publish flow itself.
How should I publish to a free subdomain?
Validate the name, check it is free, save it, and mark the site published. No Vercel API call is involved. Do the validation on the server and enforce uniqueness with a database constraint, not only a check in the UI.
Subdomain rules worth enforcing: lowercase letters, digits and hyphens only, no leading or trailing hyphen, and at most 63 characters, since each DNS label has a 63-character limit. Reject reserved names such as www, app and api, which would collide with your own pages. A unique index on the subdomain column settles the race when two customers submit the same name at once.
create unique index sites_subdomain_key on sites (subdomain);What are the steps to publish to a custom domain?
Five steps, and each one needs a visible state in your UI so the customer knows what to do next.
- Add. Your server adds the domain to the Vercel project.
- Instruct. Your app shows the DNS records the customer must create.
- Verify. Vercel checks DNS, and ownership if the domain is in use elsewhere.
- Secure. Vercel issues an SSL certificate for the domain.
- Live. Your database marks the domain active and your app starts serving it.
Keep all Vercel calls in a server function, since they use your Vercel token. In BOWB, my AI store builder, domain provisioning runs in a backend edge function for this reason, and the browser never sees the token.
How do I add a customer's domain through the Vercel API?
Call addProjectDomain with your project, your team and the customer's domain. Normalize the input first: lowercase it and strip any protocol, path and port the customer pasted in.
import { Vercel } from '@vercel/sdk';
const vercel = new Vercel({ bearerToken: process.env.VERCEL_TOKEN });
function normalizeDomain(input: string) {
return input.trim().toLowerCase()
.replace(/^https?:\/\//, '').replace(/[/:?#].*$/, '');
}
export async function attachDomain(siteId: string, raw: string) {
const name = normalizeDomain(raw);
if (!/^([a-z0-9-]+\.)+[a-z]{2,}$/.test(name)) throw new Error('invalid_domain');
if (name === ROOT || name.endsWith(`.${ROOT}`)) throw new Error('reserved_domain');
const result = await vercel.projects.addProjectDomain({
idOrName: PROJECT, teamId: TEAM, requestBody: { name },
});
await db.from('site_domains').upsert({
site_id: siteId,
domain: name,
status: result.verified ? 'pending_dns' : 'needs_verification',
verification: result.verification ?? [],
});
}The ROOT check stops a customer from claiming your own platform domain. Vercel's documentation says that if the response returns verified: false, you should give the tenant one of the returned verification challenges, then call verifyProjectDomain after they configure the DNS record. A unique index on the domain column prevents two sites from claiming the same hostname in your own database.
What DNS records does the customer need to create?
It depends on the type of domain. Vercel's documentation says an apex domain such as example.com uses an A record, and a subdomain such as shop.example.com uses a CNAME record. Each project has its own CNAME value.
Do not hard-code the values in your help text. Show the values Vercel returns or displays for your project, since they can differ between projects. If the domain is already in use on another Vercel account, Vercel requires a TXT record to prove ownership, and a customer can set up only one such TXT record at a time. Tell customers to add both the apex and www versions, then redirect one to the other, since Vercel recommends adding both and redirecting.
Which status states should the publish screen show?
Use a small fixed set, so the screen always tells the customer one next action. Four states cover the flow.
| Status | Meaning | What the customer sees | |---|---|---| | needs_verification | Vercel returned verified: false | Add this TXT record, then press Verify | | pending_dns | Added, DNS not yet pointing at Vercel | Add this A or CNAME record | | active | Verified and serving | Live, with a link | | error | A call failed | The error and a retry button |
Poll getProjectDomain on a timer while a domain is pending, and update the row when verified turns true. A manual Verify button that calls verifyProjectDomain helps customers who have just changed DNS and do not want to wait for the next poll. Only serve a custom domain as a tenant once the row is active.
Which errors should I handle?
Four error codes cover most failures, and Vercel's reference lists them.
| Code | Meaning | What to do | |---|---|---| | domain_already_in_use | Another project uses the domain | Ask the customer for the TXT record | | invalid_domain | Bad format | Show a format hint | | forbidden | Token lacks permission | Fix the token scope; log it for yourself | | rate_limit_exceeded | Too many requests | Retry with backoff |
The rate limits are real for a busy platform: Vercel's limits page lists 100 domain additions per hour per team, 50 verifications per hour per team and 100 removals per hour per team. Queue domain additions and back off on rate_limit_exceeded instead of failing the customer's publish.
Why is a custom domain not working after setup?
The cause is usually DNS timing, a missing verification record, or a conflicting record. Vercel's troubleshooting guidance covers each.
DNS changes can take 24 to 48 hours to propagate, so tell customers that a correct setup can still look broken for a day. For failed verification, wait 5 to 10 minutes after adding the TXT record, check it with dig TXT _vercel.tenant1.com, make sure there are no trailing dots or duplicate TXT records, then retry. For a certificate error, finish verification first and check that no CAA record blocks Let's Encrypt. For an invalid configuration, look for conflicting A, AAAA or CNAME records on the same hostname.
How do I remove a domain when a customer unpublishes?
Detach it from the project and delete the row, in the same operation. Call removeProjectDomain to disassociate the domain from your project, and delete or deactivate the matching database row.
Vercel notes that this call does not remove account-level ownership of the domain. Do not skip the cleanup: a domain left attached to your project keeps routing to your deployment after the customer has stopped paying or has moved the domain elsewhere, which is a takeover risk.
How do I avoid duplicate content between the subdomain and the custom domain?
Redirect one to the other, or set a canonical URL. A store reachable at shop.yourapp.com and shop.com is the same content on two hosts. Vercel's documentation suggests redirecting the subdomain to the custom domain or setting a canonical URL pointing at the primary domain, and using that domain consistently in sitemaps.
Once a custom domain turns active, make the customer's custom domain the primary and redirect the free subdomain to it. Keep the subdomain working as a fallback if the custom domain is later removed.
What plan limits affect a SaaS publish flow?
Custom domain counts depend on the Vercel plan. Per Vercel's limits page, Hobby allows up to 50 custom domains per project, and Pro and Enterprise are unlimited with soft limits of 100,000 and 1,000,000 per project. Wildcard domains work on all plans. Check Vercel's current terms on commercial use before running a paid product on a Hobby plan.
If you want a publish flow like this built into your product, see my full-stack development work or get in touch with what your customers will publish.
Related
- Hostname-based multi-tenancy on one Vercel deployment
- Multi-tenant AI website builder: how I built BOWB
- Supabase RLS multi-tenant patterns
Sources
Frequently asked questions
Does a free subdomain for each customer need a Vercel API call?
No. If you have added a wildcard domain such as *.yourapp.com to the project, every first-level subdomain already resolves. Publishing to a subdomain is a database write, not a Vercel call.
How do I add a customer's custom domain to Vercel programmatically?
Call vercel.projects.addProjectDomain with the project, team and the customer's domain name. If the response has verified set to false, show the customer the returned verification challenge, then call verifyProjectDomain after they add the DNS record.
What DNS records does a customer add?
An apex domain uses an A record and a subdomain uses a CNAME record, using the values Vercel shows for your project. If the domain is in use on another Vercel account, a TXT record proves ownership.
How long until a custom domain works?
Vercel's docs say DNS changes can take 24 to 48 hours to propagate. After you add a TXT record, wait 5 to 10 minutes, then retry verification and SSL issuance.
What happens when a customer removes their domain?
Call removeProjectDomain to detach it from your project and delete the database row. Leaving it attached means your deployment keeps answering for a domain the customer no longer controls.
Building something like this?
Multi-tenant architecture, custom domains, one deployment serving every customer — I have built it end to end, solo. Tell me what you're working on and I'll tell you honestly what it takes.


