Building Custom Domain Management with Vercel API: The Good, The Bad, and The DNS Propagation

Published: (January 1, 2026 at 10:52 AM EST)
8 min read
Source: Dev.to

Source: Dev.to

When I started building WikiBeem, custom domains seemed straightforward. Just add a domain to Vercel, show the user some DNS records, and boom—done. Right?

Nope. Turns out there’s a whole world of edge cases, timing issues, and DNS propagation delays that will make you question your life choices at 2 AM.

Below is how I actually built it, what broke, and what I learned.

How I built a custom domain management system for WikiBeem using Vercel's API. Learn about DNS verification, SSL certificates, multi-tenant routing, and the edge cases that drove me crazy.

Why Vercel’s API?

I’m hosting WikiBeem on Vercel, so using their domain‑management API made sense. It:

  • Handles SSL certificates automatically
  • Manages DNS routing
  • Integrates directly with Vercel’s infrastructure

The alternative would have been to build everything from scratch with AWS Route 53 or Cloudflare—way more work for basically the same result.

Vercel provides an official SDK (@vercel/sdk) that wraps the API, which made the integration cleaner. The documentation, however, required a lot of trial and error to figure out what actually works in production.

The Basic Flow

When a user wants to add a custom domain to their site, the following steps are required:

  1. User enters their domain (e.g., docs.yourcompany.com)
  2. Add the domain to Vercel via API
  3. Vercel returns DNS records that need to be configured
  4. User configures DNS at their registrar
  5. Poll Vercel to check if DNS has propagated
  6. SSL certificate is issued once verification succeeds
  7. Site works on the custom domain

Simple in theory. In practice? Not so much.

Setting Up the Vercel Client

I created a thin wrapper around Vercel’s SDK. This gives me a single place to handle errors and ensure credentials are configured correctly.

import { Vercel } from '@vercel/sdk'

export class VercelClient {
  private vercel: Vercel
  private projectId: string
  private teamId?: string

  constructor() {
    const token = process.env.VERCEL_TOKEN || ''
    this.projectId = process.env.VERCEL_PROJECT_ID || ''
    this.teamId = process.env.VERCEL_TEAM_ID

    if (!token || !this.projectId) {
      console.warn(
        'Vercel credentials not configured. Custom domain features will not work.'
      )
    }

    this.vercel = new Vercel({ bearerToken: token })
  }
}

Tip: Always check that the required environment variables exist before trying to use them. Missing credentials produce cryptic errors that are painful to debug.

Adding a Domain

The first API call is straightforward—just tell Vercel you want to add a domain:

async addDomain(domain: string) {
  const response = await this.vercel.projects.addProjectDomain({
    idOrName: this.projectId,
    requestBody: { name: domain },
    ...(this.teamId && { teamId: this.teamId })
  })

  return response
}

Handling Inconsistent Verification Records

Vercel returns verification records, but they aren’t always in the same place. Sometimes they’re in the response object directly; other times you need to fetch the domain config separately. I ended up checking both:

// Try to get verification records from domain config first
let verificationRecords: { type: string; name: string; value: string }[] = []

try {
  const domainConfig = await vercelClient.getDomainConfig(domain)
  if (domainConfig?.verification) {
    verificationRecords = domainConfig.verification.map(v => ({
      type: v.type,
      name: v.domain || domain,
      value: v.value
    }))
  }
} catch (e) {
  // Fallback to response verification if config fails
  if (vercelDomain.verification) {
    verificationRecords = vercelDomain.verification.map(v => ({
      type: v.type,
      name: v.domain || domain,
      value: v.value
    }))
  }
}

Lesson: API responses can be inconsistent. Having a fallback saves you from mysterious failures.

DNS Verification: The Waiting Game

This is where users get frustrated. They configure DNS records at their registrar, click Verify, and… nothing happens—at least not immediately.

  • DNS propagation can take minutes to 48 hours.
  • Subdomains are usually faster (5‑30 min).
  • Apex domains can be much slower.

I built a polling mechanism that checks verification status every few seconds, but I also made sure not to hammer Vercel’s API.

// Front‑end polling every 5 seconds
const pollDomainStatus = async () => {
  const response = await fetch(`/api/domain?siteId=${siteId}`)
  const data = await response.json()

  if (data.domain?.isVerified) {
    setPolling(false) // Stop polling when verified
    return
  }

  setTimeout(pollDomainStatus, 5000) // Check again in 5 seconds
}

I also added a manual Check Status button because users don’t want to wait passively. Giving them control improves the experience.

The SSL Certificate Race

Once DNS is verified, Vercel automatically provisions an SSL certificate, but there’s another delay—certificates can take a few minutes to issue even after verification succeeds.

I track SSL status separately from verification status:

const sslStatus = vercelDomain.verified ? 'issued' : 'pending'

In practice, the API can be a step behind: it may report verified: true while the certificate is still being issued. The UI should therefore show a “pending” state until the certificate is confirmed as active.

Takeaways

AreaWhat I Learned
Vercel SDKGreat for quick integration, but documentation gaps require experimentation.
Credential handlingAlways guard against missing env vars; fail fast with a clear warning.
API inconsistenciesVerify data from multiple endpoints and provide fallbacks.
PollingBalance frequency to avoid rate‑limits while keeping users informed.
User experienceAdd manual “Check status” actions and clear UI states for “verifying”, “SSL pending”, and “ready”.
Edge casesDNS propagation times vary wildly; design your flow to tolerate long waits.

Building a robust custom‑domain system is more about handling the unknown than writing the “happy path” code. With the patterns above, you can give users a smooth experience even when the internet decides to be slow.

SSL Provisioning Buffer

The SSL certificate isn’t actually ready yet, so I added some buffer time and show a “SSL provisioning” state to users.

Multi‑Tenant Routing: The Real Challenge

This was the trickiest part. When someone visits docs.yourcompany.com, how do we figure out which site to show?

Vercel handles the DNS routing and SSL, but the actual request routing is up to us. In Next.js middleware, I check the host header to see if it’s a custom domain:

// middleware.ts
import { NextResponse, NextRequest } from 'next/server';

export default function middleware(request: NextRequest) {
  const host = request.headers.get('host') || '';
  const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
  const mainDomain = new URL(appUrl).hostname;

  // Is this a custom domain?
  const isCustomDomain =
    host !== mainDomain &&
    !host.startsWith('localhost') &&
    !host.startsWith('127.0.0.1');

  // Pass host to pages so they can look up the site
  const response = NextResponse.next();
  response.headers.set('X-Host', host);
  return response;
}

Looking Up the Site in a Page Component

// page.tsx (or any server component)
import { headers } from 'next/headers';
import prisma from '@/lib/prisma';

export default async function Page() {
  // Get host from header
  const host = headers().get('x-host') || '';

  // Look up domain in database
  const domain = await prisma.domain.findUnique({
    where: { domain: host },
    select: { siteId: true },
  });

  // Get the site
  const site = await prisma.site.findUnique({
    where: { id: domain?.siteId },
  });

  // …render the page using `site`
}

Edge Cases

  • Domain not in DB – Show a 404.
  • DNS configured but domain not verified – Show a “domain not configured” message.

URL Structure Differences

Custom domains have a different URL structure than the default routing.

Route TypeExample
Defaultwikibeem.com/yoursite/docs/getting-started
Customdocs.yourcompany.com/docs/getting-started
  • On the main domain, the first segment (yoursite) is the site slug.
  • On a custom domain, there’s no site slug in the path—the domain itself identifies the site, so the document path starts right away.

I refactored the routing logic to handle both cases:

if (isCustomDomain) {
  // Custom domain: domain identifies the site, no site slug in path
  const domain = await prisma.domain.findUnique({
    where: { domain: host },
  });
  // Document slug is everything after the domain
  fullDocSlug = [siteSlug, ...docSlugArray].join('/');
} else {
  // Default route: siteSlug is first segment, rest is document path
  fullDocSlug = docSlugArray.join('/');
}

Routing edge cases are sneaky, and this took longer than I’d like to admit to get right.

Error Handling: Expect Everything to Break

Common Production Errors

ErrorHow to Handle
Domain already existsInform the user the domain is taken; avoid a generic 500.
DNS not configuredShow the required DNS records and prompt the user to add them.
Propagation timeoutAfter a few minutes of polling, display: “DNS propagation can take up to 48 hours. Check back later or verify your DNS settings.”
SSL certificate failureCheck SSL status separately and surface a clear error message.
Race conditionsClean up properly if a user removes a domain while verification is in progress; guard against concurrent ops.

What I’d Do Differently

  • Add webhooks – Vercel supports webhooks for domain events. Listening for verification events is cleaner than polling.
  • Better status messages – Be specific: “Checking DNS… → DNS verified, provisioning SSL… → SSL certificate issued, ready in 1‑2 minutes.”
  • Validate before API calls – Verify domain format, check for existing usage, and (where possible) confirm ownership before hitting Vercel’s API.
  • Retry logic – Implement exponential backoff for flaky API calls.
  • Testing – Use staging domains to test the full flow end‑to‑end. DNS propagation is annoying, but it’s worth the effort.

The Result

After all this work, custom domains work reliably. Users can:

  1. Add their domain.
  2. Configure DNS.
  3. Within a few minutes see their site live on the custom domain with SSL.

The UX can still improve—webhook support and richer status messages are on the roadmap—but the core flow is solid and users are happy.

If you want to see custom domains in action, check out WikiBeem. You can publish your ClickUp docs with your own domain in just a few clicks.

Back to Blog

Related posts

Read more »

My Experimental Portfolio is Live! 🚀

!Saurabh Kumarhttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...