---
title: "Custom Domains"
description: "Custom domains enable workspaces to elevate brand recognition, boost click-through rates by routing links through dedicated branding, and fulfill program requirements across short links and partner..."
last_updated: "2026-10-05T05:07:35.15703+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/custom-domains"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/api/domains/claim-dot-link-domain.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts)
- [apps/web/app/api/domains/domain/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/lib/dynadot/register-domain.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dynadot/register-domain.ts)
- [apps/web/lib/api/domains/get-domain-response.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-response.ts)
- [apps/web/app/domain/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx)
- [apps/web/app/api/domains/domain/forward-instructions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/forward-instructions/route.ts)
- [apps/web/app/api/domains/domain/verify/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/verify/route.ts)
- [apps/web/app/api/domains/domain/validate/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/validate/route.ts)
- [apps/web/lib/api/domains/finalize-premium-domain-registration.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/finalize-premium-domain-registration.ts)
- [apps/web/scripts/customers/annature/import-domains.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/annature/import-domains.ts)
- [apps/web/app/ee/api/admin/domains/register-premium/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/domains/register-premium/route.ts)
- [apps/web/app/ee/api/domains/register/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/register/route.ts)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/custom/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/custom/page.tsx)
- [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx)
- [apps/web/lib/api/domains/get-domain-search-availability.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-search-availability.ts)
- [apps/web/lib/api/domains/verify-domain.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/verify-domain.ts)
- [apps/web/ui/domains/domain-configuration.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx)
- [apps/web/ui/domains/domain-card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-card.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/page-client.tsx)
- [apps/web/lib/api/domains/add-domain-vercel.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts)
- [apps/web/app/ee/api/domains/status/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/status/route.ts)
- [packages/utils/src/functions/domains.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/functions/domains.ts)
- [apps/web/ui/partners/program-link-configuration.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/program-link-configuration.tsx)
- [apps/web/lib/api/domains/initiate-premium-domain-registration.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/initiate-premium-domain-registration.ts)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/default-domain-selector.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx)
- [apps/web/lib/domain-connect/constants.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/domain-connect/constants.ts)
- [apps/web/lib/api/domains/get-config-response.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-config-response.ts)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/page.tsx)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/register/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/register/page.tsx)
</details>

## Overview

Custom domains enable workspaces to elevate brand recognition, boost click-through rates by routing links through dedicated branding, and fulfill program requirements across short links and partner networks. The custom domains module unifies domain lifecycle management, handling API-driven routing operations, registrar coordination with Dynadot for availability searches and purchases, automated provisioning of complimentary `.link` domains, Vercel infrastructure registration, SSL certificate management, DNS validation routines including Domain Connect, and intuitive dashboard configuration interfaces.

Sources: [apps/web/app/domain/page.tsx:16-19](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx#L16-L19), [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/page.tsx:32-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/page.tsx#L32-L38), [apps/web/ui/partners/program-link-configuration.tsx:243-243](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/program-link-configuration.tsx#L243-L243), [apps/web/lib/api/domains/claim-dot-link-domain.ts:15-163](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts#L15-L163), [apps/web/app/api/domains/route.ts:96-235](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L96-L235), [apps/web/app/api/domains/domain/route.ts:44-208](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L44-L208), [apps/web/lib/dynadot/register-domain.ts:24-65](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dynadot/register-domain.ts#L24-L65), [apps/web/lib/api/domains/get-domain-search-availability.ts:4-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-search-availability.ts#L4-L32), [apps/web/lib/api/domains/finalize-premium-domain-registration.ts:12-109](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/finalize-premium-domain-registration.ts#L12-L109), [apps/web/lib/api/domains/add-domain-vercel.ts:5-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts#L5-L39), [apps/web/app/api/domains/domain/verify/route.ts:16-139](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/verify/route.ts#L16-L139), [apps/web/ui/domains/domain-card.tsx:64-132](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-card.tsx#L64-L132)

## Domain Routing and REST Surface

### Overview

The REST surface for domains provides public and workspace-scoped endpoints for querying, creating, updating, validating, and checking the status of custom domains. These endpoints enforce workspace permissions, plan-tier access constraints, validation checks, and Vercel infrastructure integration.

Sources: [apps/web/app/api/domains/route.ts:22-235](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L22-L235), [apps/web/app/api/domains/domain/route.ts:28-217](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L28-L217), [apps/web/app/api/domains/domain/validate/route.ts:8-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/validate/route.ts#L8-L48), [apps/web/app/ee/api/domains/status/route.ts:13-87](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/status/route.ts#L13-L87)

### API Endpoint Reference

| Method & Route | Access Level | Required Permissions / Plan | Description |
| --- | --- | --- | --- |
| `GET /api/domains` | Workspace-scoped | `domains.read` | Retrieves all domains for a workspace with pagination, search filtering, and optional root link inclusion. |
| `POST /api/domains` | Workspace-scoped | Workspace membership | Creates a new domain, validates syntax, registers with Vercel if configured, and enforces plan domain limits. |
| `GET /api/domains/[domain]` | Workspace-scoped | `domains.read` | Fetches a single domain's configuration and properties by its slug. |
| `PATCH /api/domains/[domain]` | Workspace-scoped | Workspace membership | Updates an existing domain's configuration fields, logo, or handles a domain name change. |
| `GET /api/domains/[domain]/validate` | Session-scoped | Session required | Validates domain format, checks against `www` prefixes, reserved `.dub.link` subdomains, and conflicting records. |
| `GET /api/domains/status` | Workspace-scoped | `domains.read`, Enterprise plan | Checks availability status of one or more domains against registrar availability APIs. |

Sources: [apps/web/app/api/domains/route.ts:23-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L23-L94), [apps/web/app/api/domains/route.ts:96-235](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L96-L235), [apps/web/app/api/domains/domain/route.ts:28-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L28-L42), [apps/web/app/api/domains/domain/route.ts:44-217](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L44-L217), [apps/web/app/api/domains/domain/validate/route.ts:8-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/validate/route.ts#L8-L48), [apps/web/app/ee/api/domains/status/route.ts:13-87](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/status/route.ts#L13-L87)

### Domain Validation and Status Checks

The validation endpoint (`GET /api/domains/[domain]/validate`) evaluates incoming domain strings through a sequence of checks. It tests syntax via `isValidDomain()`, rejects domains starting with `www.`, and evaluates reserved subdomain rules. It checks if the domain already exists via `domainExists()`, and inspects active sites using a dual approach: a 3-second timeout HTTP `HEAD` request against both `https://` and `http://` URLs, falling back to a DNS resolution lookup if the HTTP probe fails. For enterprise workspaces, the `GET /api/domains/status` route checks bulk availability through Dynadot integration while filtering out domains already verified on Dub.

> [!NOTE]
> Subdomains ending with `.dub.link` bypass the active site configuration check during validation.

Sources: [apps/web/app/api/domains/domain/validate/route.ts:8-95](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/validate/route.ts#L8-L95), [apps/web/app/ee/api/domains/status/route.ts:13-87](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/status/route.ts#L13-L87)

### Plan-Tier Enforcement

Workspaces on the `free` plan are restricted from configuring advanced domain features. When processing `POST` or `PATCH` requests on domains, the API checks workspace plan limits and throws a `DubApiError` with code `forbidden` if free-tier workspaces attempt to set restricted properties.

```typescript
if (workspace.plan === "free") {
  if (
    logo ||
    expiredUrl ||
    notFoundUrl ||
    assetLinks ||
    appleAppSiteAssociation ||
    isNonEmptyJson(deepviewData)
  ) {
    throw new DubApiError({
      code: "forbidden",
      message: `You can only set Pro features on a Pro plan and above.`,
    });
  }
}
```

Sources: [apps/web/app/api/domains/route.ts:112-137](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L112-L137), [apps/web/app/api/domains/domain/route.ts:73-98](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L73-L98)

## Dynadot Search and Domain Purchasing

### Overview

The Dynadot integration layer manages domain search availability, quote calculations, and premium registration orchestration. When checking domain search availability, the system verifies whether a domain is registered on Dub; if verified, it returns an unavailable status. Otherwise, it queries Dynadot for availability across the primary domain and alternative suggestion forms (`get${domain}`, `try${domain}`, `use${domain}`).

Sources: [apps/web/lib/api/domains/get-domain-search-availability.ts:4-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-search-availability.ts#L4-L31)

### Premium Domain Registration Call Chain

Premium domain registration is coordinated through an administrative API route that enforces workspace plan and billing constraints, initiates payment collection via Stripe, and finalizes the registration record.

The execution walkthrough proceeds as follows:
1. `POST /api/admin/domains/register-premium` parses and normalizes domain inputs, checks that the domain ends with `.link`, and verifies the workspace exists.
2. `initiatePremiumDomainRegistration()` validates workspace billing constraints (rejecting free plans and active trials), ensures a Stripe payment method is attached, checks availability via `searchDomainsAvailability()`, and ensures the domain is not already registered.
3. A database transaction creates a `domainRenewal` invoice with status `processing` for the exact registration price in cents.
4. `createPaymentIntent()` generates a Stripe payment intent with idempotency keys tied to the invoice. If payment creation fails, the invoice is marked as `failed` and a `DubApiError` is thrown.
5. Upon successful payment charge, `finalizePremiumDomainRegistration()` checks domain availability, invokes `registerDomain({ domain, premium: true })` (which returns a mock success response with a 1-year expiration), removes any unverified domain variants, creates the verified domain record with renewal fees, provisions a root link, configures Vercel nameservers and ingress, and sends notification emails to workspace owners.

Sources: [apps/web/app/ee/api/admin/domains/register-premium/route.ts:16-85](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/domains/register-premium/route.ts#L16-L85), [apps/web/lib/api/domains/initiate-premium-domain-registration.ts:10-165](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/initiate-premium-domain-registration.ts#L10-L165), [apps/web/lib/api/domains/finalize-premium-domain-registration.ts:12-109](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/finalize-premium-domain-registration.ts#L12-L109), [apps/web/lib/dynadot/register-domain.ts:24-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dynadot/register-domain.ts#L24-L38)

### Dynadot Error Handling and Registration Statuses

Standard domain registrations interact with the Dynadot client using a 1-year duration, USD currency, and a preset coupon constant. Non-success statuses returned by Dynadot throw mapped exceptions or custom API errors.

| Status Code / Response | Description / Error Message |
| :--- | :--- |
| `success` | Domain registered successfully or mock response returned for premium domains. |
| `error` | Dynadot-specific error message passed directly to the client. |
| `not_available` | `"Domain not available."` |
| `system_busy` | `"System is busy. Please try again."` |
| `insufficient_funds` | `"Insufficient funds. Please add more funds to your account."` |
| `over_quota` | Triggered when Dynadot detects unusually high registration call frequency within a short timeframe. |
| `order_pending_process` | Order created for command, pending manual team investigation and processing. |

Sources: [apps/web/lib/dynadot/register-domain.ts:4-64](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/dynadot/register-domain.ts#L4-L64)

> [!WARNING]
> Free-tier workspaces and workspaces with active billing trials are strictly blocked from registering `.link` domains. Attempting to initiate registration without an attached Stripe payment method throws a `forbidden` API error.

Sources: [apps/web/lib/api/domains/initiate-premium-domain-registration.ts:22-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/initiate-premium-domain-registration.ts#L22-L44)

> [!TIP]
> The database transaction in `initiatePremiumDomainRegistration` prevents duplicate concurrent charges by checking for existing `processing` invoices associated with the target domain slug before generating a Stripe payment intent.

Sources: [apps/web/lib/api/domains/initiate-premium-domain-registration.ts:83-102](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/initiate-premium-domain-registration.ts#L83-L102)

## Free Dot Link Domain Claiming

### Overview

Complimentary `.link` domains can be claimed by eligible paid workspaces during onboarding or partner program link configuration flows. The provisioning process enforces strict workspace checks, validates custom domain terms against edge config rules, registers the domain via Dynadot, provisions Vercel ingress and nameservers asynchronously, and dispatches notification emails to workspace owners.

Sources: [apps/web/lib/api/domains/claim-dot-link-domain.ts:15-163](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts#L15-L163), [apps/web/ui/partners/program-link-configuration.tsx:197-202](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/program-link-configuration.tsx#L197-L202)

### Call-Chain Execution Walkthrough

The claim workflow executes through a sequential series of validation, registration, and asynchronous background tasks:

1. `claimDotLinkDomain()` inspects workspace properties unless `skipWorkspaceChecks` is enabled. It verifies that the workspace plan is not `free`, a Stripe payment method (`stripeId`) exists, `dotLinkClaimed` is false, and billing trials are inactive via `isWorkspaceBillingTrialActive()`.
2. Edge config retrieves `customDomainTerms`, compiling them into a regular expression to validate that the requested domain does not violate prohibited terms.
3. `Promise.all` executes three operations concurrently: calling `registerDomain({ domain })`, counting workspace domains via `prisma.domain.count()`, and searching for an unverified domain match via `prisma.domain.findFirst()`.
4. If an unverified domain match is found, `markDomainAsDeleted()` cleans up the conflicting record.
5. A subsequent `Promise.all` creates the verified workspace domain record (setting `primary` if `totalDomains === 0` and creating a nested `registeredDomain` entry) and initializes a root redirect link using `createLink()` with `_root` key and `DEFAULT_LINK_PROPS`.
6. `waitUntil()` queues background tasks: adding the domain to Vercel via `addDomainToVercel()` followed by `configureVercelNameservers()`, sending notification emails via `sendDomainClaimedEmails()` (unless skipped), and setting `dotLinkClaimed: true` on the workspace project.

Sources: [apps/web/lib/api/domains/claim-dot-link-domain.ts:15-160](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts#L15-L160)

> [!WARNING]
> Free workspaces, workspaces lacking a Stripe ID, and workspaces currently undergoing a billing trial are blocked from claiming a free `.link` domain, returning a `forbidden` API error with specific failure messages.

Sources: [apps/web/lib/api/domains/claim-dot-link-domain.ts:29-57](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts#L29-L57)

### API Route and Workspace Enforcement

Enterprise API routes expose domain registration endpoints that bypass standard workspace trial restrictions under controlled administrative contexts.

| Route / Handler | Required Permissions | Required Plan | Validation & Behavior |
| :--- | :--- | :--- | :--- |
| `POST /api/domains/register` | `domains.write` | `enterprise` | Validates request body via `registerDomainSchema`, restricts execution to workspace IDs listed in `DOMAIN_REGISTRATION_ELIGIBLE_WORKSPACES`, and invokes `claimDotLinkDomain` with `skipWorkspaceChecks: true`. Returns HTTP 201 with registration response. |

Sources: [apps/web/app/ee/api/domains/register/route.ts:10-35](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/register/route.ts#L10-L35)

> [!TIP]
> When `skipWorkspaceChecks` is set to `true` (such as in enterprise registration routes), workspace billing plans and trial checks are bypassed, but input validation schemas and restricted workspace ID arrays are still strictly enforced.

Sources: [apps/web/app/ee/api/domains/register/route.ts:14-27](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/domains/register/route.ts#L14-L27), [apps/web/lib/api/domains/claim-dot-link-domain.ts:29-57](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/claim-dot-link-domain.ts#L29-L57)

## Vercel Ingress and SSL Provisioning

### Overview

Custom domains integration with Vercel infrastructure relies on automated REST API calls to provision domain records, manage SSL certificates, and configure nameservers. The system handles standard apex domains and wildcard subdomains, checking proxied status and configuring redirection rules.

Sources: [apps/web/lib/api/domains/add-domain-vercel.ts:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts#L1-L39), [apps/web/lib/api/domains/get-domain-response.ts:1-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-response.ts#L1-L34), [apps/web/lib/api/domains/get-config-response.ts:1-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-config-response.ts#L1-L33)

### Vercel API Call-Chain Walkthrough

The domain lookup, configuration, and addition process follows specific execution paths when interacting with Vercel's REST endpoints:

1. `getDomainResponse(domain)` first evaluates `isProxiedDomain(domain)`. If true, it short-circuits and returns `{ verified: true }`.
2. Otherwise, it extracts the apex domain using `getApexDomain("https://" + domain)`. If the apex domain differs from the requested domain, it constructs a wildcard domain (`*.${apexDomain}`) and queries `getVercelDomainResponse(wildcardDomain)`.
3. If the wildcard response is verified, it returns the wildcard response directly; otherwise, it falls back to querying `getVercelDomainResponse(domain)` against Vercel API v9 (`/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain}`).
4. `addDomainToVercel(domain, { redirectToApex })` performs a similar apex and wildcard check before issuing a `POST` request to Vercel API v10 (`/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains`), optionally attaching a `redirect` property pointing to the domain without `www` if `redirectToApex` is enabled.
5. `getConfigResponse(domain)` inspects configuration status by checking proxied domains or fetching from Vercel API v6 (`/v6/domains/${domain}/config`), falling back to wildcard configurations if the apex domain differs.

Sources: [apps/web/lib/api/domains/add-domain-vercel.ts:5-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts#L5-L39), [apps/web/lib/api/domains/get-domain-response.ts:4-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-response.ts#L4-L34), [apps/web/lib/api/domains/get-config-response.ts:4-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-config-response.ts#L4-L33)

> [!NOTE]
> If a wildcard subdomain response is already verified on Vercel, `getDomainResponse` and `addDomainToVercel` bypass creating or querying the specific subdomain individually, inheriting the wildcard verification status.

Sources: [apps/web/lib/api/domains/add-domain-vercel.ts:15-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts#L15-L22), [apps/web/lib/api/domains/get-domain-response.ts:25-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-response.ts#L25-L32)

### Vercel Integration Endpoints and Parameters

The codebase communicates with specific Vercel API versions using environment variables for authentication and project scoping.

| Function | Vercel API Endpoint | Method | Key Parameters & Headers |
| :--- | :--- | :--- | :--- |
| `getVercelDomainResponse` | `/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain}` | `GET` | Headers: `Authorization: Bearer ${process.env.VERCEL_API_KEY}`, `Content-Type: application/json`. Query param: `teamId=${process.env.TEAM_ID_VERCEL}`. |
| `addDomainToVercel` | `/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains` | `POST` | Body: `{ name: domain, redirect?: getDomainWithoutWWW(domain) }`. Query param: `teamId=${process.env.TEAM_ID_VERCEL}`. |
| `getVercelConfigResponse` | `/v6/domains/${domain}/config` | `GET` | Headers: `Authorization: Bearer ${process.env.VERCEL_API_KEY}`, `Content-Type: application/json`. Query param: `teamId=${process.env.TEAM_ID_VERCEL}`. |

Sources: [apps/web/lib/api/domains/get-domain-response.ts:4-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-domain-response.ts#L4-L16), [apps/web/lib/api/domains/add-domain-vercel.ts:23-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/add-domain-vercel.ts#L23-L38), [apps/web/lib/api/domains/get-config-response.ts:4-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/get-config-response.ts#L4-L14)

## DNS Verification and Domain Connect

### Overview

DNS verification polling and record validation depend on direct interaction with Vercel's verification APIs and internal state persistence. When clients invoke verification endpoints, the system queries domain status, inspects configuration conflicts, and triggers retry-backed verification routines against Vercel infrastructure.

Sources: [apps/web/app/api/domains/domain/verify/route.ts:1-47](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/verify/route.ts#L1-L47), [apps/web/lib/api/domains/verify-domain.ts:1-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/verify-domain.ts#L1-L41)

### Verification and Retry Call-Chain Walkthrough

The verification lifecycle follows an explicit sequence of calls when evaluating unverified domains:

1. `GET /api/domains/[domain]/verify` invokes `getDomainOrThrow` to validate workspace permissions and retrieve the target domain slug.
2. It executes `Promise.all([getDomainResponse(domain), getConfigResponse(domain)])` to fetch current Vercel domain and configuration metadata.
3. If `domainJson.verified` is false, it assigns status `"Pending Verification"` and calls `verifyDomainWithRetry(domain)`.
4. `verifyDomainWithRetry` loops up to `attempts` (defaulting to 3), waiting `delayMs` (defaulting to 2500ms) via `sleep(delayMs)` between iterations, and issues `verifyDomain(domain)`.
5. `verifyDomain` makes a `POST` request to Vercel API v9 (`/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain.toLowerCase()}/verify?teamId=${process.env.TEAM_ID_VERCEL}`) with bearer token authorization.
6. Once `verifyDomainWithRetry` returns a verified response, the verification route re-checks configuration via `getConfigResponse(domain)`. If conflicts or misconfigurations exist, it updates Prisma storage with `verified: false`; otherwise, it records `verified: true` and triggers automated Domain Connect discovery flows.

Sources: [apps/web/app/api/domains/domain/verify/route.ts:16-109](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/verify/route.ts#L16-L109), [apps/web/lib/api/domains/verify-domain.ts:1-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/domains/verify-domain.ts#L1-L41)

> [!WARNING]
> If Vercel reports a verified state but a subsequent `getConfigResponse` check detects misconfigurations, the domain state is forced back to unverified in the database, and the verification status becomes `"Invalid Configuration"`.

Sources: [apps/web/app/api/domains/domain/verify/route.ts:69-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/verify/route.ts#L69-L79)

### DNS Record Constants and Forwarding Instructions

The system defines specific custom DNS record values and constants for configuring A records, CNAME records, and Domain Connect identifiers.

| Constant Name | Value | Purpose |
| :--- | :--- | :--- |
| `DUB_CUSTOM_DOMAIN_A_RECORD` | `76.76.21.21` | Target IP address for custom domain apex A records. |
| `DUB_CUSTOM_DOMAIN_CNAME` | `cname.dub.co` | Target target host for custom domain CNAME records. |
| `DOMAIN_CONNECT_PROVIDER_ID` | `dub.co` | Provider identifier used during Domain Connect discovery flows. |
| `DOMAIN_CONNECT_KEY_HOST` | `_dck1` | Hostname prefix used for Domain Connect verification keys. |
| `DEFAULT_DC_SERVICE_APEX` | `links-apex` | Default Domain Connect service name for apex domain configurations. |
| `DEFAULT_DC_SERVICE_SUBDOMAIN` | `links-subdomain` | Default Domain Connect service name for subdomain configurations. |
| `DEFAULT_DC_SERVICE_EMAIL` | `email` | Default Domain Connect service name for email configurations. |

Sources: [apps/web/lib/domain-connect/constants.ts:1-8](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/domain-connect/constants.ts#L1-L8)

Clients can request formatted DNS instructions via `POST /api/domains/[domain]/forward-instructions`, which validates a JSON body containing an email address and `recordType` enum (`"A"` or `"CNAME"`). Rate limiting is enforced using Upstash policies (`forwardDnsInstructions` and `forwardDnsInstructionsTarget`), after which record arrays are compiled, appending A or CNAME records alongside any discovered TXT verification records, and delivered using `@dub/email` templates.

Sources: [apps/web/app/api/domains/domain/forward-instructions/route.ts:18-116](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/forward-instructions/route.ts#L18-L116)

## UI Lifecycle and Transfer Management

### Overview

The dashboard UI exposes domain management through client-side components including `DomainConfiguration`, `DomainCard`, onboarding wizards, and administrative domain renewal panels. These interfaces handle record selection, verification state polling, auto-configuration handoffs, and DNS record generation.

Sources: [apps/web/ui/domains/domain-configuration.tsx:27-39](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx#L27-L39), [apps/web/ui/domains/domain-card.tsx:64-90](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-card.tsx#L64-L90)

### Domain Configuration Call-Chain Walkthrough

The automated DNS configuration sequence coordinates client selections with Domain Connect endpoints:

1. `DomainConfiguration` initializes `recordType` based on `getSubdomain(domainJson.name, domainJson.apexName)` — defaulting to `"CNAME"` if a subdomain exists, or `"A"` for apex domains.
2. Clicking the auto-configure button triggers `handleAutoConfigure`, which issues a `POST` request to `/api/domains/${encodeURIComponent(domain)}/domain-connect/apply?workspaceId=${workspaceId}` with a JSON body specifying `returnTo`.
3. The response JSON is validated for an `applyUrl`. If present, `isAllowedSyncUXOrigin(json.applyUrl)` checks the origin against allowed Domain Connect SyncUX providers.
4. When validated, the browser assigns the URL via `window.location.assign(json.applyUrl)`, redirecting the user to their DNS provider's authorization screen.

Sources: [apps/web/ui/domains/domain-configuration.tsx:43-98](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx#L43-L98)

> [!WARNING]
> If the server returns an `applyUrl` from an unverified or unallowed origin, `isAllowedSyncUXOrigin` rejects the redirect, and a toast error is dispatched without navigating away.

Sources: [apps/web/ui/domains/domain-configuration.tsx:85-90](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx#L85-L90)

### Configuration Form States and Record Display

The `DomainConfiguration` component adapts its rendered DNS records and UI warnings depending on the domain verification status and record conflict checks.

| Data Status | Record Type Tab | Rendered DNS Instruction & Records | Warning / Notice |
| :--- | :--- | :--- | :--- |
| `Conflicting DNS Records` | Auto-selected based on conflict type (`A` or `CNAME`) | Lists conflicting records to remove, followed by the target DUB record (`76.76.21.21` or `cname.dub.co`). | None |
| `Unknown Error` | N/A | Renders `data.response.domainJson.error.message`. | None |
| `Pending Verification` / Default | `A` or `CNAME` tabs | Configures apex or subdomain with `DUB_CUSTOM_DOMAIN_A_RECORD` (`76.76.21.21`) or `DUB_CUSTOM_DOMAIN_CNAME` (`cname.dub.co`) with TTL `86400`, plus any discovered `TXT` verification records. | Ownership transfer warning if TXT verification is present; otherwise TTL propagation notice. |

Sources: [apps/web/ui/domains/domain-configuration.tsx:100-216](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx#L100-L216), [apps/web/ui/domains/domain-configuration.tsx:267-340](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-configuration.tsx#L267-L340)

### Onboarding Domain Selectors and Admin Operations

During user onboarding, `DefaultDomainSelector` renders selectable `DomainOption` cards for connecting custom domains, claiming free `.link` domains, or setting up `.dub.link` subdomains. Each option tracks user interaction via Plausible analytics and advances the onboarding flow using `continueTo(step)`.

Sources: [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/default-domain-selector.tsx:25-144](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx#L25-L144), [apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/default-domain-selector.tsx:173-248](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx#L173-248)

For administrative domain management, enterprise admin dashboards provide actions for premium `.link` domain registration invoices, subscription renewals, and domain refreshing (`Remove and re-add domain from Vercel`).

Sources: [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx:5-37](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx#L5-37)

## Related

- [Routing and Multitenancy](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/routing-and-multitenancy)
- [Link Resolution and Redirection](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-resolution-and-redirection)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
