Most B2B SaaS founders hear the same question as they start closing bigger deals: "Can we use our own domain?" A prospect loves your product but needs it to appear at portal.theircompany.com instead of yourapp.com/acme.
You have two options. Say no and lose the deal. Or say yes -- and charge 2x to 5x more for it.
Custom domain support crosses from "nice to have" to "deal requirement" in enterprise sales faster than almost any other feature. The good news: in Next.js, the core routing pattern is a few dozen lines of middleware. The hard part is not the code -- it is knowing which approach to take and where it breaks.
Before writing a line of code, make one decision: will customers get a subdomain on your domain (e.g. acme.yourapp.com) or a fully custom domain they own (e.g. portal.acme.com)?
Subdomains are easier to start with:
*.yourapp.com DNS entryhost headerCustom domains are what enterprise customers actually want:
portal.acme.com to your Vercel projectBoth patterns use the same Next.js middleware hook. Start with subdomains to validate the feature, then add custom domain support as an upgrade.
Next.js middleware runs before every request. You can read the hostname and rewrite the URL internally -- without redirecting the user -- so a tenant's dashboard renders at their domain while your code stays on a single deployment.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const host = req.headers.get('host') ?? ''
const subdomain = host.split('.')[0]
// rewrite to /tenants/[slug] internally -- user sees their domain
if (subdomain && subdomain !== 'www' && subdomain !== 'yourapp') {
return NextResponse.rewrite(
new URL(`/tenants/${subdomain}${req.nextUrl.pathname}`, req.url)
)
}
return NextResponse.next()
}
For custom domains, replace the subdomain parse with a database lookup that maps portal.acme.com to a tenant slug. Cache the result in a short-lived cookie or edge KV store to avoid a database round-trip on every request.
If you are using the multi-tenancy pattern already in this boilerplate, add a custom_domain column to your organization table:
// modules/organization/organization.schema.ts
export const organizationTable = pgTable('organizations', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
customDomain: text('custom_domain').unique(), // null = no custom domain
createdAt: timestamp('created_at').defaultNow().notNull(),
})
In middleware, query by the incoming host and rewrite to the org slug:
const org = await db.query.organizationTable.findFirst({
where: eq(organizationTable.customDomain, host),
columns: { slug: true },
})
if (org) {
return NextResponse.rewrite(
new URL(`/tenants/${org.slug}${req.nextUrl.pathname}`, req.url)
)
}
The .unique() constraint on custom_domain automatically creates an index in Neon DB so this lookup stays fast even with thousands of tenants.
When a customer submits their custom domain through your settings page, you need to register it with Vercel so TLS is provisioned. Call the Vercel API from your service layer after saving the domain to the database:
await fetch(
`https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VERCEL_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: customDomain }),
}
)
Vercel validates the CNAME and provisions TLS automatically -- usually within a few minutes. The customer just needs to add one CNAME record in their DNS panel.
Custom domain support is a natural premium feature. Gate it in your service layer the same way you gate any plan-specific capability:
if (org.plan !== 'enterprise') {
throw new HttpError(403, 'Custom domains require the Enterprise plan')
}
Pair this with the usage limits and plan gating pattern so free-tier customers cannot reach the endpoint directly, even if they know the URL.
Before you ship this to production, check these:
Cookie scope. If you use JWT stored in cookies, set Domain=.yourapp.com for subdomain customers. Custom-domain customers get a separate cookie scope -- you may need to re-issue the session when they visit their domain the first time.
CORS. If you expose an API, make sure your CORS allowlist accepts the customer's custom domain alongside your own. Hardcoding one origin in Access-Control-Allow-Origin will break API calls from portal.acme.com.
Middleware latency. Edge middleware is fast, but a synchronous database query on every request adds latency. Cache the domain-to-slug mapping in a cookie or at the Vercel edge after the first lookup so repeated visits do not hit the database.
Custom domain support turns a self-serve tool into something an enterprise buyer can justify to their IT team and their brand team. The middleware pattern is straightforward once you see it -- the Next.js SaaS boilerplate gives you the auth, multi-tenancy, and database layer so the only thing you are adding is the domain mapping and the Vercel API call.
Get the boilerplate and you can have the core working in an afternoon. Then raise your enterprise price.