Ayan Ali.
9 min readUpdated

Hostname-Based Multi-Tenancy on One Vercel Deployment

One deployment can serve unlimited customer sites if you resolve the tenant from the hostname. Here is the setup: wildcard domain, custom domains, lookup code and the mistakes to avoid.

VercelMulti-tenantSaaSArchitecture
A single dark block at the centre with thin lime lines radiating outward to a ring of identical panels, the Vercel triangle at the centre of the block.

Short answer: point a wildcard domain (*.yourapp.com) at one Vercel project, let tenants attach their own domains through Vercel's API, and have your app read the request hostname to decide which tenant to show. You build and deploy once, and a new store needs a database row, not a new deployment. The wildcard setup, the domain API, the hostname code and the pitfalls are below. Checked against Vercel's documentation, October 2026.

What is hostname-based multi-tenancy?

It means one deployed app serves many customers, and the hostname of each request identifies the customer. store-one.yourapp.com, store-two.yourapp.com and shopname.com can all reach the same deployment, and the app shows a different site for each.

Vercel's documentation calls this a multi-tenant platform: one codebase, one deployment, multiple domains, shared infrastructure, and tenant-aware routing and data access. It fits products where content and branding differ per customer but the functionality is the same, such as website builders, documentation sites and SaaS dashboards.

How does the app identify the tenant?

There are three strategies, and Vercel's docs describe each one: the subdomain (tenant1.yourapp.com), a custom domain mapped to a tenant in your database (tenant1.com), or a path prefix (/tenant1/dashboard). Hostname-based routing combines the first two.

For a subdomain, the tenant slug is the first label of the hostname. For anything else, the hostname is looked up in a table of custom domains. Path-based routing is simpler to host but gives every tenant the same domain, so it suits dashboards better than public storefronts.

How did I do it in BOWB?

In BOWB, my AI store builder, one Vercel deployment serves the app and every customer storefront. A runtime function called detectSite() classifies the hostname as the app shell, a subdomain of the platform's domain, or a custom domain, and mounts the matching route tree. Custom domains are attached through the Vercel API, so no per-store build or deploy is needed.

BOWB is a Vite and React single-page app with Supabase for the database and Vercel serverless functions for the backend, so the tenant is resolved in the browser. The next sections show the same idea in simplified form. The code below is a sketch of the pattern, not BOWB's source.

What does the hostname detection code look like?

It is a small pure function that returns what kind of host the request came from. Keep it free of side effects so it is easy to test.

const ROOT = 'yourapp.com';                        // your platform's root domain
const RESERVED = new Set(['www', 'app', 'api']);   // names tenants must never get

export function detectSite(hostname = window.location.hostname) {
  const host = hostname.toLowerCase();

  if (host === ROOT || host === 'localhost') return { kind: 'app' };

  if (host.endsWith(`.${ROOT}`)) {
    const slug = host.slice(0, -(ROOT.length + 1));
    if (RESERVED.has(slug) || slug.includes('.')) return { kind: 'app' };
    return { kind: 'subdomain', slug };
  }

  return { kind: 'custom', domain: host };
}

Then look the tenant up. Only published sites should be readable by anonymous visitors, and that rule belongs in the database, not in the client.

const site = detectSite();
if (site.kind === 'app') return mountAppRoutes();

const column = site.kind === 'subdomain' ? 'subdomain' : 'custom_domain';
const value  = site.kind === 'subdomain' ? site.slug : site.domain;

const { data } = await supabase
  .from('sites')
  .select('*')
  .eq(column, value)
  .eq('published', true)
  .single();

return data ? mountStorefrontRoutes(data) : mountNotFound();

Table and column names here are illustrative. Reserve subdomain names such as www, app and api at signup, since a tenant who registers app would otherwise collide with your own dashboard.

What does vercel.json need for a single-page app?

A catch-all rewrite that serves index.html for every path, on every host. Without it, a tenant's deep link such as store-one.yourapp.com/products/shirt returns a 404 instead of loading the app.

{
  "rewrites": [{ "source": "/((?!api/).*)", "destination": "/index.html" }]
}

The (?!api/) part keeps requests to your serverless functions from being rewritten to the HTML shell. Because every host receives the same file, the tenant decision happens in JavaScript after load. That is the main trade-off of the single-page-app version, covered below.

How do I set up wildcard subdomains on Vercel?

Add the apex domain and the wildcard to the same project, with the domain on Vercel's nameservers. Vercel's documentation lists three steps: point the domain at ns1.vercel-dns.com and ns2.vercel-dns.com, add the apex domain (for example acme.com) in project settings, then add *.acme.com.

Every first-level subdomain then resolves to the deployment, and Vercel issues one wildcard certificate that covers all of them, so you do not need a certificate per tenant. A wildcard certificate covers only one subdomain level: tenant1.acme.com is covered, but docs.tenant1.acme.com is not, and would need its own *.tenant1.acme.com entry. Wildcard domains are supported on all plans. If you cannot change nameservers, Vercel documents a way to delegate the certificate challenge instead.

How do I let tenants bring their own domain?

Add the domain to your project through Vercel's SDK or REST API when the tenant requests it, then check that ownership is verified. The call in Vercel's docs is projectsAddProjectDomain.

import { VercelCore as Vercel } from '@vercel/sdk/core.js';
import { projectsAddProjectDomain } from '@vercel/sdk/funcs/projectsAddProjectDomain.js';

const vercel = new Vercel({ bearerToken: process.env.VERCEL_TOKEN });

await projectsAddProjectDomain(vercel, {
  idOrName: 'my-multi-tenant-app',
  teamId: 'team_1234',
  requestBody: { name: 'customacmesite.com' },
});

Run this on the server only, because it uses your Vercel token. The tenant then points DNS at Vercel, and if the domain is already in use on Vercel they must add a TXT record to prove ownership. Each custom domain gets its own certificate, validated by an HTTP challenge once the domain points to Vercel. Store the domain and its verification status in your database so your app only serves domains that are verified.

What are the limits on domains?

The limits depend on the plan. Per Vercel's multi-tenant 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 domains per project. Domain operations through the API are rate limited: 100 additions per hour per team, 50 verifications per hour per team, and 100 removals per hour per team.

DNS changes typically take 24 to 48 hours to propagate. Tell tenants this in your onboarding screen, since a domain that is added correctly can still look broken for a day. Multi-tenant preview URLs and custom SSL certificates are Enterprise-only.

What can go wrong with tenant subdomains?

Cookies are the one that matters most for security. Without a Public Suffix List entry, browsers treat tenant1.acme.com and tenant2.acme.com as the same site, so a tenant can set a cookie with Domain=acme.com that the browser then sends to every other tenant and to your dashboard.

Vercel's documentation recommends submitting your shared domain to the Public Suffix List if tenants can publish content or run code. While that is pending, put your dashboard and login on a different apex domain, prefix session cookies with __Host-, set Secure, HttpOnly and Path=/, and omit the Domain attribute. Also validate the Origin header or use a CSRF token on requests that change data.

How do I keep tenants' data apart?

Enforce isolation in the database, not only in the app. Hostname routing decides which tenant a visitor sees, but it does not stop a bug or a direct API call from reading another tenant's rows.

BOWB uses Supabase Row Level Security for this, in three tiers: owners access only their own rows, anonymous visitors can read rows of published sites, and storefront actions such as placing an order are open inserts. See Supabase RLS multi-tenant patterns for the policies. If you resolve the tenant in a proxy, Vercel's docs add one more rule: overwrite or delete any inbound x-tenant-* header so clients cannot supply tenant context themselves.

Should I resolve the tenant on the server instead?

Yes, if search engines need full tenant HTML on first response. In a single-page app, every host returns the same index.html, so a crawler that does not run JavaScript sees the same shell for every tenant.

Vercel's multi-tenant documentation shows the server-side pattern with Next.js Proxy: it reads the host header, resolves the tenant, and forwards an x-tenant-id request header to the page, so the page renders with tenant data on the server. The trade-off is a heavier framework. A Vite single-page app is simpler to build, and it suits the logged-in app and storefronts where first-load SEO matters less. I have not measured the search performance of BOWB storefronts.

What about duplicate content across subdomain and custom domain?

A store reachable at both shop.yourapp.com and shop.com is the same content on two hosts. Vercel's documentation suggests redirecting one to the other, or setting a canonical URL in the page head. Do the same for www and the apex domain: add both to the project and redirect one to the other.

When is one deployment the wrong choice?

Use a separate project per tenant when each customer needs custom code or isolated infrastructure. Vercel's own guidance splits the two: multi-tenant for the same functionality with different content and branding, and multi-project when each tenant needs something different, such as AI coding platforms that deploy user-generated apps.

If you want a multi-tenant app or website-builder backend built, see my full-stack development work or get in touch with what your tenants need.

  • Multi-tenant AI website builder: how I built BOWB
  • Supabase RLS multi-tenant patterns
  • What is multi-tenancy in SaaS?
  • Build vs buy a website builder SaaS

Sources

Frequently asked questions

Can one Vercel deployment serve many customer sites?

Yes. Vercel documents this as a multi-tenant platform: one codebase and one deployment serve every tenant, with each tenant on a subdomain of your domain or on their own custom domain.

Do I need Vercel's nameservers for wildcard subdomains?

Vercel needs to answer the DNS challenge for the wildcard certificate. Use Vercel's nameservers, or delegate certificate validation if you cannot change them.

How many custom domains can one Vercel project have?

Per Vercel's limits page: Hobby allows up to 50 custom domains per project. Pro and Enterprise are unlimited, subject to a soft limit of 100,000 domains on Pro and 1,000,000 on Enterprise.

How does the app know which tenant a request belongs to?

It reads the hostname. A subdomain maps to a tenant slug, and any other hostname is looked up as a custom domain in your database. Vercel's docs describe three strategies: subdomain, custom domain and path.

Is a single-page app a good fit for hostname-based multi-tenancy?

It works and is simple, but every host gets the same HTML shell and the tenant is resolved after JavaScript loads. If tenant pages must be fully server-rendered for search engines, resolve the tenant on the server instead, for example with Next.js Proxy.

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.

Read next

How to Add an AI Chatbot to Shopify Grounded in Your Product Catalogue
A frosted speech-bubble form above a grid of product boxes, connected by thin lime lines, with the Shopify logo set into the floor and the OpenAI logo on the bubble.
Shopify

How to Add an AI Chatbot to Shopify Grounded in Your Product Catalogue

A Shopify chatbot that does not invent products needs four parts: a synced catalogue index, retrieval, a grounded prompt, and a Liquid section that talks to your backend. Here is each part.

Read
How to Build an AI Product Finder Section for Shopify
A wide field of dark cubes receding into shadow with a narrow lime light picking out exactly three, and the Shopify logo on a plate in the foreground.
Shopify

How to Build an AI Product Finder Section for Shopify

A product finder takes 'a waterproof jacket for light hiking under $150' and returns matching products. It needs vector search and filters, and it does not need a chat model.

Read

Got a store or a build in mind?

Tell me what you're trying to ship — a Shopify section, a full storefront, a chatbot, or a web app. I'll tell you what it takes, honestly, before you commit.

ayanrjpoot@gmail.com