---
title: "Link Creation and Builder UI"
description: "The Link Creation and Builder UI serves as the core interface and pipeline for generating, managing, and configuring shortened URLs, custom domains, and redirect rules across workspaces. It bridges..."
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/link-creation-and-builder-ui"
---

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

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

- [apps/web/ui/modals/link-builder/index.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/%5B...link%5D/page-client.tsx)
- [apps/web/app/ee/api/partners/links/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/route.ts)
- [apps/web/ui/links/link-builder/link-builder-provider.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-provider.tsx)
- [apps/web/lib/api/links/process-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts)
- [packages/ui/src/rich-text-area/link-modal.tsx](https://github.com/blade47/dub/blob/HEAD/packages/ui/src/rich-text-area/link-modal.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/groups/groupSlug/links/add-edit-group-additional-link-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/groups/%5BgroupSlug%5D/links/add-edit-group-additional-link-modal.tsx)
- [apps/web/ui/modals/link-builder/og-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/og-modal.tsx)
- [apps/web/ui/modals/partner-link-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/partner-link-modal.tsx)
- [apps/web/ui/links/link-builder/use-link-builder-submit.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/resources/program-brand-assets/add-link-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/resources/program-brand-assets/add-link-modal.tsx)
- [apps/web/ui/links/link-builder/link-preview.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx)
- [apps/web/app/ee/app.dub.co/embed/referrals/add-edit-link.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/add-edit-link.tsx)
- [apps/web/ui/modals/link-builder/utm-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx)
- [apps/web/ui/modals/add-partner-link-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/add-partner-link-modal.tsx)
- [apps/web/ui/links/link-builder/link-builder-header.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-header.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/links/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/page-client.tsx)
- [apps/web/ui/links/links-toolbar.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/links-toolbar.tsx)
- [apps/web/ui/modals/link-builder/targeting-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx)
- [apps/web/ui/modals/link-builder/webhooks-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/webhooks-modal.tsx)
- [apps/web/lib/api/links/create-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/create-link.ts)
- [apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/ab-testing/ab-testing-modal.tsx)
- [apps/web/ui/modals/link-builder/partners-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/partners-modal.tsx)
- [apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx)
- [apps/web/app/api/links/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts)
- [apps/web/app/api/links/upsert/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts)
- [apps/web/lib/api/links/index.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/index.ts)
- [apps/web/lib/api/links/case-sensitivity.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts)
- [apps/web/lib/api/links/utils/transform-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/transform-link.ts)
- [apps/web/lib/upstash/assert-rate-limit.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/assert-rate-limit.ts)
</details>

## Overview

The Link Creation and Builder UI serves as the core interface and pipeline for generating, managing, and configuring shortened URLs, custom domains, and redirect rules across workspaces. It bridges client-side form management with robust server-side mutation endpoints, enabling users to orchestrate complex link parameters such as Open Graph social previews, UTM tracking strings, geo-device targeting, webhooks, and A/B test variants. By abstracting plan-tier validations, security scans, and case-sensitivity routing behind unified providers and modal layers, the architecture delivers a seamless experience for creating standard, partner-affiliated, and embedded referral links alike. 

Sources: [apps/web/ui/modals/link-builder/index.tsx:58-75](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx#L58-L75), [apps/web/ui/links/link-builder/link-builder-provider.tsx:40-67](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-provider.tsx#L40-L67), [apps/web/lib/api/links/process-link.ts:61-120](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts#L61-L120), [apps/web/lib/api/links/create-link.ts:35-62](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/create-link.ts#L35-L62)

## Link Builder Architecture and State

The Link Builder architecture is structured around a centralized context provider, React Hook Form integration, and dual presentation modes that span both modal dialogs and dedicated full-page editing routes. At its core, `LinkBuilderProvider` wraps child components with a React Hook Form context (`FormProvider`) and a custom `LinkBuilderContext` that shares builder properties, modal flags, and metatag generation states across input controls and preview surfaces. 

Sources: [apps/web/ui/links/link-builder/link-builder-provider.tsx:40-67](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-provider.tsx#L40-L67)

When the builder initializes, `LinkBuilderProvider` configures form default values using either existing link properties, duplicated link configurations, or fallback defaults defined by `DEFAULT_LINK_PROPS`. Conversion tracking is automatically enabled if the workspace plan qualifies beyond free or pro tiers and conversion tracking is globally allowed. 

Sources: [apps/web/ui/links/link-builder/link-builder-provider.tsx:50-58](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-provider.tsx#L50-L58)

> [!NOTE]
> For new link creation flows where neither existing properties nor duplicate properties supply a custom domain, an `useEffect` hook monitors available workspace domains via `useAvailableDomains` and populates the form domain field with the primary domain once loading completes. 

Sources: [apps/web/ui/modals/link-builder/index.tsx:117-134](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx#L117-L134)

The link builder renders across different application entry points, including workspace dashboards, link list toolbars, and dedicated URL management views. The architecture bifurcates into two distinct rendering wrappers: modal presentation via `LinkBuilderOuter` and `LinkBuilderInner`, and dedicated page presentation inside `apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx` which renders `LinkBuilderProvider` with `modal={false}` directly inside the dashboard layout, attaching keyboard shortcuts for submission (`CMD+S` / `CTRL+S`) and navigation (`Escape`). 

Sources: [apps/web/ui/modals/link-builder/index.tsx:62-184](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx#L62-L184), [apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx:84-134](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/%5B...link%5D/page-client.tsx#L84-L134)

The following table summarizes the primary context properties, hooks, and form management primitives utilized across the link builder ecosystem.

| Context / Hook | Source File | Purpose & Behavior |
| :--- | :--- | :--- |
| `LinkBuilderProvider` | `apps/web/ui/links/link-builder/link-builder-provider.tsx` | Initializes form state with `useForm`, wraps children in `FormProvider` and `LinkBuilderContext`. |
| `useLinkBuilderContext` | `apps/web/ui/links/link-builder/link-builder-provider.tsx` | Consumes builder context, throwing an error if accessed outside `LinkBuilderProvider`. |
| `LinkBuilderHeader` | `apps/web/ui/links/link-builder/link-builder-header.tsx` | Renders folder breadcrumbs, debounce-tracked short link previews, and close controls. |
| `LinkBuilderInner` | `apps/web/ui/modals/link-builder/index.tsx` | Manages modal close actions, URL query param cleanup (`newLink`), and submission redirection. |
| `LinkBuilder` (Page) | `apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx` | Handles desktop/mobile layout splitting, clipboard copying, and keyboard shortcuts (`Escape`, `CMD+S`). |

Sources: [apps/web/ui/links/link-builder/link-builder-provider.tsx:30-67](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-provider.tsx#L30-L67), [apps/web/ui/links/link-builder/link-builder-header.tsx:24-134](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-builder-header.tsx#L24-L134), [apps/web/ui/modals/link-builder/index.tsx:77-154](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx#L77-L154), [apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx:93-134](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/%5B...link%5D/page-client.tsx#L93-L134)

## Form Submission and Mutation Flow

Link persistence and mutation flow through `useLinkBuilderSubmit`, which constructs payload bodies by normalizing form values—such as mapping tags array to `tagIds`, resolving `"unsorted"` folders to `null`, and handling partner parameters. The request executes against `/api/links` via `POST` for new links or `/api/links/[id]` via `PATCH` for updates. 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:12-58](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L12-L58)

When the form submission triggers, execution proceeds through a structured sequence of payload sanitization, network transmission, response evaluation, and cache invalidation steps:

1. `handleSubmit(onSubmit)` (React Hook Form) → triggers validation and invokes `useLinkBuilderSubmit` callback. 

Sources: [apps/web/ui/modals/link-builder/index.tsx:174](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/index.tsx#L174), [apps/web/app/app.dub.co/dashboard/slug/links/...link/page-client.tsx:218](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/links/%5B...link%5D/page-client.tsx#L218)

2. Body normalization → maps `tags` to `tagIds`, converts `"unsorted"` folder ID to `null`, clears empty optional fields (`expiredUrl`, `ios`, `android`, `externalId`, `tenantId`), and appends `programId` if `partnerId` is present. 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:23-47](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L23-L47)

3. HTTP Request → dispatches `fetch` using `POST` (`/api/links?workspaceId=...`) or `PATCH` (`/api/links/[id]?workspaceId=...`). 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:49-66](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L49-L66)

4. `res.status === 200` branch → invokes `onSuccess?.(data)`, redirects if domain or key changed, mutates SWR cache prefixes via `mutatePrefix`, copies short links to clipboard for new links, and updates workspace stats via `mutate('/api/workspaces/[slug]')`. 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:68-122](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L68-L122)

5. Error branch (`res.status !== 200`) → parses error message, triggers upsell gating if message includes `"Upgrade to "`, or maps validation errors to specific form fields (`root`, `key`, `url`). 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:123-157](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L123-L157)

Upon successful server response, the client executes cache invalidation across endpoints and handles error fields according to validation rules.

| Target Resource / Field | Action / Mutation Trigger | Error Routing / Condition |
| :--- | :--- | :--- |
| `/api/links` | `mutatePrefix(["/api/links", ...])` | Invalidates all link-related SWR query keys. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:80-84](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L80-L84) |
| `/api/domains` | Appended to `mutatePrefix` if `getValues("key") === "_root"` | Refreshes domain configuration when root domain links are modified. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:82-83](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L82-L83) |
| Workspace Usage | `mutate('/api/workspaces/[slug]')` | Updates workspace limits and usage metrics after creation or edit. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:121-122](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L121-L122) |
| Image Errors | `setError("root", { message: error.message })` | Triggered when error message includes `"image"` before URL checks. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:146-147](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L146-L147) |
| Key / Short Link Errors | `setError("key", { message: error.message })` | Triggered when error message includes `"key"`. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:148-149](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L148-L149) |
| Destination URL Errors | `setError("url", { message: error.message })` | Triggered when error message includes `"url"`. Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:150-151](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L150-L151) |

> [!WARNING]
> Image validation errors contain the word "URL" in their descriptions but lack a dedicated form field of their own. The error handler explicitly checks for the keyword `"image"` *before* evaluating `"url"` branches to prevent image errors from incorrectly attaching to the destination URL input field instead of form root. 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:141-147](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L141-L147)

When server responses return error payloads containing the substring `"Upgrade to "`, the client intercepts the error message, extracts the target plan name or defaults to workspace next plan name, and renders an `UpgradeRequiredToast` via `toast.custom`. 

Sources: [apps/web/ui/links/link-builder/use-link-builder-submit.tsx:126-137](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/use-link-builder-submit.tsx#L126-L137)

## Metatags Previewing and Open Graph

The link builder interface integrates real-time social preview rendering, image manipulation utilities, and an Open Graph (OG) modal override system. Users can cycle through platform-specific previews, customize metadata fields, upload or resize images, and enable proxy-based metatags. 

Sources: [apps/web/ui/links/link-builder/link-preview.tsx:44-175](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L44-L175), [apps/web/ui/modals/link-builder/og-modal.tsx:199-215](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/og-modal.tsx#L199-L215)

The `LinkPreview` component reads form values via `useWatch` for `proxy`, `title`, `description`, `image`, `url`, and `password`. It computes a debounced hostname (falling back to `dub.co` if password protection is enabled) and renders a tabbed interface supporting four distinct platforms. 

Sources: [apps/web/ui/links/link-builder/link-preview.tsx:75-88](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L75-L88)

| Tab Key | Display Title | Associated Icon | Component Handler |
| :--- | :--- | :--- | :--- |
| `default` | Default | `GlobePointer` | `DefaultOGPreview`. Sources: [apps/web/ui/links/link-builder/link-preview.tsx:47-69](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L47-L69) |
| `x` | X/Twitter | `Twitter` | `XOGPreview`. Sources: [apps/web/ui/links/link-builder/link-preview.tsx:51-70](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L51-L70) |
| `linkedin` | LinkedIn | `LinkedIn` | `LinkedInOGPreview`. Sources: [apps/web/ui/links/link-builder/link-preview.tsx:50-71](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L50-L71) |
| `facebook` | Facebook | `Facebook` | `FacebookOGPreview`. Sources: [apps/web/ui/links/link-builder/link-preview.tsx:49-72](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L49-L72) |

> [!NOTE]
> Pressing the `L` key triggers a registered keyboard shortcut, which immediately opens the Open Graph configuration modal for rapid metadata editing. 

Sources: [apps/web/ui/links/link-builder/link-preview.tsx:91](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L91), [apps/web/ui/modals/link-builder/og-modal.tsx:221-235](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/og-modal.tsx#L221-L235)

When users upload or select an image through file upload, Unsplash search, or a direct URL paste, the image undergoes client-side resizing via `resizeImage(file)`. If the workspace is on a paid plan (non-free), updating the image automatically toggles the `proxy` flag to `true` to ensure custom metatags are rendered publicly. 

Sources: [apps/web/ui/modals/link-builder/og-modal.tsx:250-321](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/og-modal.tsx#L250-L321), [apps/web/ui/links/link-builder/link-preview.tsx:95-98](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L95-L98)

> [!CAUTION]
> Custom Link Previews and proxy metadata overriding require a Pro plan or above. Free-tier workspaces attempting to toggle the proxy switch encounter a disabled state with an interactive upsell tooltip linking to checkout or upgrade routes. 

Sources: [apps/web/ui/links/link-builder/link-preview.tsx:113-133](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/links/link-builder/link-preview.tsx#L113-L133)

## Advanced Targeting and Feature Modals

The link builder interface features specialized configuration modals that manage granular targeting options, UTM tracking templates, webhooks, and advanced link identifiers. Each modal synchronizes its internal state with the parent `LinkFormData` context via `react-hook-form`, supporting quick keyboard shortcuts and real-time validation previews. 

Sources: [apps/web/ui/modals/link-builder/utm-modal.tsx:1-68](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L1-L68), [apps/web/ui/modals/link-builder/targeting-modal.tsx:1-54](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L1-L54)

The `UTMModal` component renders a dedicated UTM parameter builder alongside a real-time destination URL preview. When users modify UTM fields or load templates via `UTMTemplatesCombo`, the `updateTargeting` callback automatically propagates matching parameters to existing iOS, Android, and geographic targeting URLs. 

Sources: [apps/web/ui/modals/link-builder/utm-modal.tsx:33-149](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L33-L149)

The parameter propagation execution walkthrough proceeds as follows: `updateTargeting()` extracts parent destination and targeting URLs → `getParamsFromURL()` parses existing query parameters → `UTM_PARAMETERS.filter()` identifies parameters matching the root destination URL → `constructURLFromUTMParams()` reconstructs target URLs with updated UTM values → `setValueParent()` commits the modified target URLs back to the parent form with `{ shouldDirty: true }`. 

Sources: [apps/web/ui/modals/link-builder/utm-modal.tsx:82-147](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L82-L147)

> [!TIP]
> Pressing the `U` key invokes a registered keyboard shortcut that instantly opens the UTM Builder modal from anywhere in the link creation flow. 

Sources: [apps/web/ui/modals/link-builder/utm-modal.tsx:2](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L2), [apps/web/ui/modals/link-builder/utm-modal.tsx:178-192](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L178-L192)

The `TargetingModal` component allows redirection based on visitor locations using country comboboxes paired with Vercel flag CDN SVGs, sorting the United States to the top of the option list. Concurrently, the `WebhooksModal` and `WebhookSelect` components integrate workspace webhooks using `useWebhooks()`, rendering multi-select comboboxes with real-time badge counters and keyboard shortcuts mapped to the `W` key. 

Sources: [apps/web/ui/modals/link-builder/targeting-modal.tsx:25-217](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L25-L217), [apps/web/ui/modals/link-builder/webhooks-modal.tsx:17-237](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/webhooks-modal.tsx#L17-L237)

| Modal Component | Shortcut Key | Primary Form Fields | Associated Icon |
| :--- | :--- | :--- | :--- |
| `UTMModal` | `U` | `url`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` | `DiamondTurnRight`. Sources: [apps/web/ui/modals/link-builder/utm-modal.tsx:50-58](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L50-L58), [apps/web/ui/modals/link-builder/utm-modal.tsx:179-192](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/utm-modal.tsx#L179-L192) |
| `TargetingModal` | `G` | `ios`, `android`, `geo` | `Crosshairs3`, `Trash`. Sources: [apps/web/ui/modals/link-builder/targeting-modal.tsx:47-53](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L47-L53), [apps/web/ui/modals/link-builder/targeting-modal.tsx:117-130](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L117-L130) |
| `WebhooksModal` | `W` | `webhookIds` | `Webhook`. Sources: [apps/web/ui/modals/link-builder/webhooks-modal.tsx:52-55](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/webhooks-modal.tsx#L52-L55), [apps/web/ui/modals/link-builder/webhooks-modal.tsx:76-90](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/webhooks-modal.tsx#L76-L90) |
| `AdvancedLinkFeaturesModal` | `V` | `externalId`, `tenantId` | Info/Tooltips. Sources: [apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx:33-37](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx#L33-L37), [apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx:74-89](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx#L74-L89) |

> [!WARNING]
> When updating targeting URLs on blur, `getNewParams()` checks that `parentUrl` contains parameters that are missing on the target URL before injecting them, preventing overwrites of distinct custom query parameters already present on localized links. 

Sources: [apps/web/ui/modals/link-builder/targeting-modal.tsx:68-83](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/targeting-modal.tsx#L68-L83)

The `AdvancedLinkFeaturesModal` component manages system-level identifiers via `externalId` and `tenantId` fields, bound to the `V` keyboard shortcut. If either identifier is populated on the parent form, a quick-removal toggle button appears at the bottom-left of the form to reset `externalId` back to `null`. 

Sources: [apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx:14-153](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/modals/link-builder/advanced-link-features-modal.tsx#L14-L153)

## API Link Processing and Validation

Link processing and validation take place on the server during link creation via the `POST` route handler, which first evaluates workspace usage limits using `throwIfLinksUsageExceeded(workspace)` and parses request bodies against `createLinkBodySchemaAsync`. 

Sources: [apps/web/app/api/links/route.ts:58-64](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L58-L64)

```mermaid
sequenceDiagram
  participant route as apps/web/app/api/links/route.ts
  participant assertRateLimit as apps/web/lib/upstash/assert-rate-limit.ts
  participant formatRetryAfter as apps/web/lib/upstash/assert-rate-limit.ts
  route->>assertRateLimit: assertRateLimit({ policy: RATELIMIT_POLICIES.anonymousLinkCreate, identifier: ip })
  assertRateLimit->>formatRetryAfter: formatRetryAfter(reset)
  formatRetryAfter-->>assertRateLimit: returns human-friendly duration string
  assertRateLimit-->>route: throws DubApiError if rate limit exceeded
```

Sources: [apps/web/app/api/links/route.ts:66-72](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/route.ts#L66-L72), [apps/web/lib/upstash/assert-rate-limit.ts:28-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/assert-rate-limit.ts#L28-L34)

For unauthenticated requests where no session exists, the handler extracts the client IP address from the `x-forwarded-for` header (falling back to `LOCALHOST_IP`) and enforces rate limiting by invoking `assertRateLimit`. Inside `assertRateLimit`, if rate limiting is active, a Redis key is constructed by joining `policy.keyPrefix` and the identifier. It calls `ratelimit(policy.attempts, policy.window).limit(key)`. If success is false, `formatRetryAfter` calculates the remaining seconds until reset, formats a human-friendly duration using `pluralize`, evaluates custom policy messages or a default string, and throws a `DubApiError` with code `rate_limit_exceeded`. 

Sources: [apps/web/lib/upstash/assert-rate-limit.ts:8-66](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/assert-rate-limit.ts#L8-L66)

> [!IMPORTANT]
> When `shouldApplyRateLimit` is disabled in the local environment, `assertRateLimit` immediately returns without performing Redis checks or throwing rate limit errors. 

Sources: [apps/web/lib/upstash/assert-rate-limit.ts:35-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/assert-rate-limit.ts#L35-L37)

The payload is subsequently passed to `processLink`, which validates destination URLs and enforces workspace plan constraints. If `url` is provided, it is normalized via `getUrlFromString` and checked with `isValidUrl`; unparseable URLs return an unprocessable entity error with code `unprocessable_entity`. If `url` is absent and the key is not `_root`, processing stops with a bad request error. 

Sources: [apps/web/lib/api/links/process-link.ts:74-119](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts#L74-L119)

| Validation Step | Trigger Condition | Error Message / Outcome | Error Code |
| :--- | :--- | :--- | :--- |
| URL Validation | `url` is invalid via `isValidUrl` | `"Invalid destination URL"` | `unprocessable_entity` |
| Missing URL | `!url` and `key !== "_root"` | `"Missing destination URL"` | `bad_request` |
| Root Redirect (Free) | `workspace.plan === "free"`, `key === "_root"`, `url` set | `"You can only set a redirect for a root domain link on a Pro plan and above..."` | `forbidden` |
| Free Subdomain/Features | Free plan boundary checks fail | Error thrown by feature checks | `forbidden` |
| Pro Plan Subdomain | Pro plan boundary checks fail | Error thrown by feature checks | `forbidden` |
| Conversion Tracking | `!trackConversion && testVariants` | `"Conversion tracking must be enabled to use A/B testing."` | `unprocessable_entity` |
| Dub.link Plan Check | `domain === "dub.link"` on free plan | `"You can only use dub.link on a Pro plan and above..."` | `forbidden` |
| Session Expiration | `domain === "dub.sh"`, `userId` provided, user missing | `"Session expired. Please log in again."` | `not_found` |
| Malicious URL | `domain` is `dub.sh` or `dub.link` and URL is malicious | `"Malicious URL detected"` | `unprocessable_entity` |
| Restricted Domain Features | `isDubDomain(domain)` with geo, device, or A/B testing | `"You cannot use geo targeting, device targeting, or A/B testing on ${domain} links."` | `unprocessable_entity` |
| Dub Domain Hostname | URL domain/apex violates Dub domain allowed hostnames | `"Invalid destination URL. You can only create ${domain} short links for URLs with..."` | `unprocessable_entity` |
| Parent Subdirectory | `key` includes `/` but parent link project ID mismatches workspace | `"You do not have access to create links in the ${domain}/${parentKey}/ subdirectory."` | `forbidden` |
| Workspace Ownership | Domain does not belong to workspace domains | `"Domain does not belong to workspace."` | `forbidden` |

Sources: [apps/web/lib/api/links/process-link.ts:94-262](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts#L94-L262)

> [!WARNING]
> For Dub-owned domains like `chatg.pt` or `spti.fi`, usage of geo-targeting, device targeting, and A/B testing is strictly blocked, returning an unprocessable entity error. 

Sources: [apps/web/lib/api/links/process-link.ts:207-217](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts#L207-L217)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Centralized `processLink` validation function | Ensures uniform validation rules across API routes and UI actions | Couples disparate feature checks into a large conditional block |
| Upstash Redis rate-limiting per IP/identifier | Protects public endpoints from abuse with low latency overhead | Requires network roundtrips to Redis during unauthenticated creation |
| Strict domain ownership verification via Prisma queries | Prevents unauthorized link creation on custom workspace domains | Adds database query latency on every link creation request |
| Separate free/pro feature check functions | Granular enforcement of tiered subscription entitlements | Requires maintaining multiple feature gate functions across plan types |

Sources: [apps/web/lib/api/links/process-link.ts:132-262](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts#L132-L262), [apps/web/lib/upstash/assert-rate-limit.ts:28-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/upstash/assert-rate-limit.ts#L28-L67)

## Link Persistence and Upsert Pipelines

Link persistence and upsert operations manage the transition from validated payloads to stored database entities, handling case-sensitivity encoding, key transformations, and conditional record updates. When a request hits the upsert pipeline, the system evaluates existing records by workspace and destination URL to determine whether to execute a creation or an update routine. 

Sources: [apps/web/app/api/links/upsert/route.ts:22-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts#L22-L33)

When transforming and decoding links, data passes through the verified `PUT` → `transformLink` → `decodeLinkIfCaseSensitive` → `decodeKey` execution chain. The `PUT` upsert route handler (`apps/web/app/api/links/upsert/route.ts`) invokes `transformLink` (`apps/web/lib/api/links/utils/transform-link.ts`), which evaluates case sensitivity and calls `decodeLinkIfCaseSensitive` (`apps/web/lib/api/links/case-sensitivity.ts`). If the link domain is case-sensitive, `decodeLinkIfCaseSensitive` delegates directly to `decodeKey` (`apps/web/lib/api/links/case-sensitivity.ts`) to reverse the base64 encoding and XOR obfuscation applied by `XOR_SECRET_KEY`. 

Sources: [apps/web/app/api/links/upsert/route.ts:23-107](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts#L23-L107), [apps/web/lib/api/links/utils/transform-link.ts:35-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/transform-link.ts#L35-L44), [apps/web/lib/api/links/case-sensitivity.ts:30-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L30-L88)

```mermaid
sequenceDiagram
    participant PUT as apps/web/app/api/links/upsert/route.ts
    participant Transform as apps/web/lib/api/links/utils/transform-link.ts
    participant CaseSens as apps/web/lib/api/links/case-sensitivity.ts
    participant Decode as decodeKey

    PUT->>Transform: transformLink(link)
    Transform->>CaseSens: decodeLinkIfCaseSensitive(link)
    CaseSens->>Decode: decodeKey(link.key)
    Decode-->>CaseSens: originalKey
    CaseSens-->>Transform: decoded link object
    Transform-->>PUT: fully transformed response
```

Sources: [apps/web/app/api/links/upsert/route.ts:23-107](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts#L23-L107), [apps/web/lib/api/links/utils/transform-link.ts:35-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/transform-link.ts#L35-L44), [apps/web/lib/api/links/case-sensitivity.ts:30-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L30-L88)

Case-sensitive domains require key obfuscation because underlying storage or routing layers may normalize character casing. The system maintains an explicit array of case-sensitive domains and uses a fixed XOR secret string combined with base64 encoding to persist keys securely while retaining exact casing upon retrieval. 

Sources: [apps/web/lib/api/links/case-sensitivity.ts:1-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L1-L28)

| Domain Identifier | Status | Purpose / Behavior |
| :--- | :--- | :--- |
| `biltapp.link` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:4-5](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L4-L5) |
| `buff.ly` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:6](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L6) |
| `dub-internal-test.com` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L7) |
| `go.homeserve.fr` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:8](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L8) |
| `go.homeserve.be` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:9](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L9) |
| `jbbr.pro` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:10](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L10) |
| `new.biltapp.link` | Case-Sensitive | Encodes and decodes short keys via XOR and base64. Sources: [apps/web/lib/api/links/case-sensitivity.ts:11](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/case-sensitivity.ts#L11) |

> [!IMPORTANT]
> When evaluating whether key checks can be skipped during upsert operations, the system compares lowercased keys alongside domain matching to prevent redundant conflict queries when only casing is adjusted on non-sensitive domains. 

Sources: [apps/web/app/api/links/upsert/route.ts:118-121](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts#L118-L121)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| Deep equality check prior to mutations | Avoids unnecessary database writes and webhook triggers when payloads match | Computes deep object comparisons on existing database entities |
| Conditional skip flags (`skipKeyChecks`, `skipExternalIdChecks`) | Streamlines update execution by bypassing redundant uniqueness validation | Requires explicit boolean state coordination inside the upsert handler |
| Asynchronous background sync via `waitUntil` | Keeps HTTP response latency low for upsert and creation APIs | Defers cache updates, webhook publishing, and Tinybird event recording to post-response background tasks |

Sources: [apps/web/app/api/links/upsert/route.ts:81-165](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/upsert/route.ts#L81-L165), [apps/web/lib/api/links/create-link.ts:164-245](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/create-link.ts#L164-L245)

## Related

- [A/B Testing and Targeting](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/a-b-testing-and-targeting)
- [QR Code Generation](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/qr-code-generation)


## Sitemap

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