Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
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, apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/page.tsx:32-38, apps/web/ui/partners/program-link-configuration.tsx:243-243, apps/web/lib/api/domains/claim-dot-link-domain.ts:15-163, apps/web/app/api/domains/route.ts:96-235, apps/web/app/api/domains/domain/route.ts:44-208, apps/web/lib/dynadot/register-domain.ts:24-65, apps/web/lib/api/domains/get-domain-search-availability.ts:4-32, apps/web/lib/api/domains/finalize-premium-domain-registration.ts:12-109, apps/web/lib/api/domains/add-domain-vercel.ts:5-39, apps/web/app/api/domains/domain/verify/route.ts:16-139, apps/web/ui/domains/domain-card.tsx:64-132
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, apps/web/app/api/domains/domain/route.ts:28-217, apps/web/app/api/domains/domain/validate/route.ts:8-48, apps/web/app/ee/api/domains/status/route.ts:13-87
Sources: apps/web/app/api/domains/route.ts:23-94, apps/web/app/api/domains/route.ts:96-235, apps/web/app/api/domains/domain/route.ts:28-42, apps/web/app/api/domains/domain/route.ts:44-217, apps/web/app/api/domains/domain/validate/route.ts:8-48, apps/web/app/ee/api/domains/status/route.ts:13-87
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, apps/web/app/ee/api/domains/status/route.ts:13-87
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.
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.`,
});
}
}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}).
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:
POST /api/admin/domains/register-premium parses and normalizes domain inputs, checks that the domain ends with .link, and verifies the workspace exists.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.domainRenewal invoice with status processing for the exact registration price in cents.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.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, apps/web/lib/api/domains/initiate-premium-domain-registration.ts:10-165, apps/web/lib/api/domains/finalize-premium-domain-registration.ts:12-109, apps/web/lib/dynadot/register-domain.ts:24-38
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.
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.
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.
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, apps/web/ui/partners/program-link-configuration.tsx:197-202
The claim workflow executes through a sequential series of validation, registration, and asynchronous background tasks:
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().customDomainTerms, compiling them into a regular expression to validate that the requested domain does not violate prohibited terms.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().markDomainAsDeleted() cleans up the conflicting record.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.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.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.
Enterprise API routes expose domain registration endpoints that bypass standard workspace trial restrictions under controlled administrative contexts.
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, apps/web/lib/api/domains/claim-dot-link-domain.ts:29-57
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, apps/web/lib/api/domains/get-domain-response.ts:1-34, apps/web/lib/api/domains/get-config-response.ts:1-33
The domain lookup, configuration, and addition process follows specific execution paths when interacting with Vercel's REST endpoints:
getDomainResponse(domain) first evaluates isProxiedDomain(domain). If true, it short-circuits and returns { verified: true }.getApexDomain("https://" + domain). If the apex domain differs from the requested domain, it constructs a wildcard domain (*.${apexDomain}) and queries getVercelDomainResponse(wildcardDomain).getVercelDomainResponse(domain) against Vercel API v9 (/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain}).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.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, apps/web/lib/api/domains/get-domain-response.ts:4-34, apps/web/lib/api/domains/get-config-response.ts:4-33
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, apps/web/lib/api/domains/get-domain-response.ts:25-32
The codebase communicates with specific Vercel API versions using environment variables for authentication and project scoping.
Sources: apps/web/lib/api/domains/get-domain-response.ts:4-16, apps/web/lib/api/domains/add-domain-vercel.ts:23-38, apps/web/lib/api/domains/get-config-response.ts:4-14
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, apps/web/lib/api/domains/verify-domain.ts:1-41
The verification lifecycle follows an explicit sequence of calls when evaluating unverified domains:
GET /api/domains/[domain]/verify invokes getDomainOrThrow to validate workspace permissions and retrieve the target domain slug.Promise.all([getDomainResponse(domain), getConfigResponse(domain)]) to fetch current Vercel domain and configuration metadata.domainJson.verified is false, it assigns status "Pending Verification" and calls verifyDomainWithRetry(domain).verifyDomainWithRetry loops up to attempts (defaulting to 3), waiting delayMs (defaulting to 2500ms) via sleep(delayMs) between iterations, and issues verifyDomain(domain).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.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, apps/web/lib/api/domains/verify-domain.ts:1-41
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".
The system defines specific custom DNS record values and constants for configuring A records, CNAME records, and Domain Connect identifiers.
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.
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, apps/web/ui/domains/domain-card.tsx:64-90
The automated DNS configuration sequence coordinates client selections with Domain Connect endpoints:
DomainConfiguration initializes recordType based on getSubdomain(domainJson.name, domainJson.apexName) — defaulting to "CNAME" if a subdomain exists, or "A" for apex domains.handleAutoConfigure, which issues a POST request to /api/domains/${encodeURIComponent(domain)}/domain-connect/apply?workspaceId=${workspaceId} with a JSON body specifying returnTo.applyUrl. If present, isAllowedSyncUXOrigin(json.applyUrl) checks the origin against allowed Domain Connect SyncUX providers.window.location.assign(json.applyUrl), redirecting the user to their DNS provider's authorization screen.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.
The DomainConfiguration component adapts its rendered DNS records and UI warnings depending on the domain verification status and record conflict checks.
Sources: apps/web/ui/domains/domain-configuration.tsx:100-216, apps/web/ui/domains/domain-configuration.tsx:267-340
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, apps/web/app/app.dub.co/onboarding/onboarding/steps/domain/default-domain-selector.tsx:173-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).