---
title: "Link Resolution and Redirection"
description: "Link resolution and redirection form the core traffic-routing engine of Dub, capturing inbound short-link requests at the edge and mapping them to destination URLs, target assets, or administrative..."
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-resolution-and-redirection"
---

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

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

- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts)
- [apps/web/app/ee/api/partners/links/upsert/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partners/links/upsert/route.ts)
- [apps/web/app/ee/api/track/click/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/click/route.ts)
- [apps/web/lib/middleware/utils/crawl-bitly.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts)
- [apps/web/app/ee/api/track/open/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts)
- [apps/web/app/domain/expired/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/expired/page.tsx)
- [apps/web/app/api/links/exists/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/exists/route.ts)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts)
- [apps/web/app/domain/notfound/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx)
- [apps/web/lib/planetscale/get-link-via-edge.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts)
- [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
- [apps/web/lib/api/links/process-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/process-link.ts)
- [apps/web/app/domain/key/proxy/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx)
- [apps/web/app/api/links/info/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/info/route.ts)
- [apps/web/lib/api/links/utils/process-key.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts)
- [apps/web/app/domain/banned/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/banned/page.tsx)
- [apps/web/app/ee/api/cron/domains/update/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts)
- [apps/web/lib/planetscale/get-link-with-partner.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-with-partner.ts)
- [apps/web/app/domain/key/inspect/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx)
- [apps/web/app/api/links/random/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/random/route.ts)
- [apps/web/app/api/links/iframeable/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts)
- [apps/web/app/ee/api/admin/links/ban/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts)
- [apps/web/app/cloaked/url/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx)
- [apps/web/lib/middleware/utils/get-final-url.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts)
- [apps/web/lib/api/partners/generate-partner-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/generate-partner-link.ts)
- [packages/cli/src/api/links.ts](https://github.com/blade47/dub/blob/HEAD/packages/cli/src/api/links.ts)
- [apps/web/app/api/callback/bitly/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/bitly/route.ts)
- [packages/utils/src/functions/link-constructor.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/functions/link-constructor.ts)
- [apps/web/lib/api/links/get-link-or-throw.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/get-link-or-throw.ts)
- [apps/web/app/domain/key/inspect/card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/card.tsx)
</details>

## Overview

Link resolution and redirection form the core traffic-routing engine of Dub, capturing inbound short-link requests at the edge and mapping them to destination URLs, target assets, or administrative fallback pages. This system addresses the challenges of low-latency distributed lookups, case sensitivity, internationalized domain names, and granular tracking across multi-tenant workspaces. Key design decisions include multi-tier caching with Redis and edge databases, on-demand fallback crawls for legacy providers, and flexible URL parameter preservation. By integrating directly with edge middleware, analytics, and partner attribution pipelines, the resolution subsystem ensures fast, secure, and context-aware request handling worldwide. Sources: [apps/web/lib/middleware/link.ts:43-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L122), [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39)

## Edge Link Resolution Pipeline

### Overview

Dub captures and resolves inbound traffic at the network edge using Next.js middleware and edge-optimized database queries. When a request hits the edge, the system first inspects the hostname and path through global routing checks before delegating link resolution to caching layers or directly querying the underlying relational datastore via PlanetScale. Sources: [apps/web/middleware.ts:34-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L89), [apps/web/lib/middleware/link.ts:43-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L104)

### Edge Request Lifecycle and Middleware Entry

Incoming requests enter through `middleware.ts`, which sets up a runtime environment and parses the request context using `parse(req)`. The request is evaluated against known hostnames—such as application dashboards, API endpoints, or administrative portals—before falling through to the short-link resolution pipeline in `LinkMiddleware`. Sources: [apps/web/middleware.ts:20-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L20-L89), [apps/web/lib/middleware/link.ts:43-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L44)

```mermaid
sequenceDiagram
    participant middleware.ts
    participant LinkMiddleware
    participant getLinkViaEdge
    participant getLinkViaEdgeHelper
    middleware.ts->>LinkMiddleware: POST / Inbound Request
    LinkMiddleware->>getLinkViaEdge: getLinkViaEdge({ domain, key })
    getLinkViaEdge->>getLinkViaEdgeHelper: getLinkViaEdgeHelper({ domain, key })
```

Sources: [apps/web/middleware.ts:34-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L89), [apps/web/lib/middleware/link.ts:43-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L104), [apps/web/lib/planetscale/get-link-via-edge.ts:10-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L68)

### Call-Chain Execution Walkthrough

The edge resolution path executes a precise sequence of functions to fetch link records from the edge database when cache misses occur:

1. `POST` (or incoming middleware invocation) receives the initial request payload or URL path. Sources: [apps/web/app/ee/api/track/open/route.ts:23-97](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L23-L97)
2. `getLinkViaEdge` checks an in-flight lookup map (`inFlightLinkLookups`) using a composite `${domain}:${key}` string to deduplicate concurrent requests for the same short link. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:41-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L41-L68)
3. `getLinkViaEdgeHelper` normalizes the domain's case sensitivity, applies punycode and URI decoding, and executes a prepared SQL query (`SELECT * FROM Link WHERE domain = ? AND \`key\` = ?`) against the database connection. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39)

> [!NOTE]
> `getLinkViaEdge` uses an in-memory `Map` called `inFlightLinkLookups` to prevent duplicate database queries when multiple concurrent requests arrive for the exact same uncached link, sharing the resulting promise across callers. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:41-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L41-L68)

### Core Resolution Functions and Parameters

The edge link resolution layer relies on specialized utility modules to normalize identifiers, handle deduplication, and query persistent storage.

| Function Name | File Location | Purpose & Behavior |
| :--- | :--- | :--- |
| `LinkMiddleware` | `apps/web/lib/middleware/link.ts` | Main entry point for short-link resolution, cache checks, and click tracking initialization. Sources: [apps/web/lib/middleware/link.ts:43-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L122) |
| `getLinkViaEdge` | `apps/web/lib/planetscale/get-link-via-edge.ts` | Deduplicates concurrent database lookups using an in-flight lookup cache. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:46-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L46-L68) |
| `getLinkViaEdgeHelper` | `apps/web/lib/planetscale/get-link-via-edge.ts` | Formats query keys based on domain case sensitivity and executes the MySQL link lookup. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39) |
| `POST` | `apps/web/app/ee/api/track/open/route.ts` | Handles deep link open tracking events, validating redis caches and invoking edge lookups on misses. Sources: [apps/web/app/ee/api/track/open/route.ts:23-110](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L23-L110) |

Sources: [apps/web/lib/middleware/link.ts:43-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L122), [apps/web/lib/planetscale/get-link-via-edge.ts:10-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L68), [apps/web/app/ee/api/track/open/route.ts:23-110](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L23-L110)

### Design Trade-Offs in Edge Resolution

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| In-flight lookup deduplication via local `Map` | Prevents database connection exhaustion during traffic spikes on popular uncached links. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:41-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L41-L68) | Transient memory overhead in the edge runtime node for active lookup promises. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:41-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L41-L68) |
| Dual caching via Redis and fallback PlanetScale queries | Ensures high availability and sub-millisecond lookups under normal operation while handling cache failures gracefully. Sources: [apps/web/lib/middleware/link.ts:89-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L104) | Increased architectural complexity managing synchronization and failover states. Sources: [apps/web/lib/middleware/link.ts:89-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L104) |
| Case-sensitivity domain checks prior to key encoding | Preserves case preservation options for custom enterprise domains where case matters. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:17-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L17-L24) | Requires conditional branching during query key formatting. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:17-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L17-L24) |

Sources: [apps/web/lib/middleware/link.ts:89-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L104), [apps/web/lib/planetscale/get-link-via-edge.ts:10-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L68)

## Domain Normalization and Key Encoding

### Overview

Domain normalization and key encoding dictate how raw URL path segments and hostnames are transformed into queries suitable for persistent storage and database lookups. Depending on whether a domain is case-sensitive, keys undergo distinct transformation pipelines involving URI decoding, Unicode normalization, and punycode encoding. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39), [apps/web/lib/api/links/utils/process-key.ts:8-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L8-L41)

### Key Processing and Sanitization Pipeline

The `processKey` utility handles validation and sanitization for link keys before they are stored or queried. It evaluates reserved routes, regular expression constraints, and default Dub domain specifics. Sources: [apps/web/lib/api/links/utils/process-key.ts:8-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L8-L41)

1. If the key equals `_root`, it returns immediately. Sources: [apps/web/lib/api/links/utils/process-key.ts:8-12](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L8-L12)
2. It validates the key against `validKeyRegex` and rejects any key starting with an underscore `_` (reserved for Dub internals) or flagged by `isUnsupportedKey`. Sources: [apps/web/lib/api/links/utils/process-key.ts:13-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L13-L25)
3. It strips all leading and trailing slashes using `key.replace(/^\/+|\/+$/g, "")`. Sources: [apps/web/lib/api/links/utils/process-key.ts:26-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L26-L28)
4. For default Dub domains, it applies Unicode normalization (`NFD`) and strips accents/diacritical marks (`/[\u0300-\u036f]/g`) to prevent phishing and typo squatting. Sources: [apps/web/lib/api/links/utils/process-key.ts:34-36](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L34-L36)
5. It encodes the resulting string to ASCII via `punyEncode`. Sources: [apps/web/lib/api/links/utils/process-key.ts:37-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L37-L40)

> [!WARNING]
> Keys starting with an underscore are strictly reserved for Dub internal routes and will cause `processKey` to return `null`, preventing custom links from utilizing leading underscores. Sources: [apps/web/lib/api/links/utils/process-key.ts:17-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L17-L20)

### Call-Chain Execution Walkthrough

When an edge lookup executes, domain case sensitivity determines how keys are prepared for querying:

1. `POST` extracts the hostname and pathname from the incoming request URL. Sources: [apps/web/app/ee/api/track/open/route.ts:25-79](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L25-L79)
2. `getLinkViaEdge` passes the domain and key into `getLinkViaEdgeHelper`. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:46-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L46-L68)
3. `getLinkViaEdgeHelper` evaluates `isCaseSensitiveDomain(domain)` to branch between case-sensitive encoding (`encodeKey(key)`) and non-case-sensitive punycode/URI decoding (`punyEncode(safeDecodeURIComponent(key))`). Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:10-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L24)

```mermaid
sequenceDiagram
    participant Route as POST (route.ts)
    participant Edge as getLinkViaEdge (get-link-via-edge.ts)
    participant Helper as getLinkViaEdgeHelper (get-link-via-edge.ts)
    Route->>Edge: Invoke with { domain, key }
    Edge->>Helper: Call helper function
    Helper->>Helper: Check isCaseSensitiveDomain(domain)
    Helper->>Helper: Encode or punyEncode(safeDecodeURIComponent(key))
    Helper->>Database: Execute SQL SELECT query
```

Sources: [apps/web/app/ee/api/track/open/route.ts:75-97](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/open/route.ts#L75-L97), [apps/web/lib/planetscale/get-link-via-edge.ts:10-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L68)

### Domain and Link Transformation Reference

| Function / Utility | Source File | Behavior & Transformation Purpose |
| :--- | :--- | :--- |
| `processKey` | `apps/web/lib/api/links/utils/process-key.ts` | Validates regex, rejects reserved underscores, strips slashes, and applies NFD normalization for Dub domains. Sources: [apps/web/lib/api/links/utils/process-key.ts:8-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L8-L41) |
| `linkConstructor` | `packages/utils/src/functions/link-constructor.ts` | Constructs full URLs with punycode-encoded domains and keys, optionally stripping protocols if `pretty` is true. Sources: [packages/utils/src/functions/link-constructor.ts:3-29](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/functions/link-constructor.ts#L3-L29) |
| `linkConstructorSimple` | `packages/utils/src/functions/link-constructor.ts` | Builds direct URLs without punycode transformations using raw domain and key inputs. Sources: [packages/utils/src/functions/link-constructor.ts:31-39](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/functions/link-constructor.ts#L31-L39) |
| `getLinkViaEdgeHelper` | `apps/web/lib/planetscale/get-link-via-edge.ts` | Formats query keys conditional on domain case sensitivity before executing MySQL lookups. Sources: [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39) |

Sources: [apps/web/lib/api/links/utils/process-key.ts:8-41](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/links/utils/process-key.ts#L8-L41), [packages/utils/src/functions/link-constructor.ts:3-39](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/functions/link-constructor.ts#L3-L39), [apps/web/lib/planetscale/get-link-via-edge.ts:10-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/planetscale/get-link-via-edge.ts#L10-L39)

## Final URL Construction and Parameters

### Overview

Final URL assembly integrates base targets, A/B test variant splits, attribution parameters, and incoming query strings at the edge. The resolution routine evaluates cached link properties, resolves variant destinations via `resolveABTestURL`, and constructs the outgoing redirection destination using `getFinalUrl`. Sources: [apps/web/lib/middleware/link.ts:168-173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L168-L173), [apps/web/lib/middleware/utils/get-final-url.ts:12-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L12-L23)

### Call-Chain Execution Walkthrough

The construction of a resolved destination URL flows through specific middleware and utility stages before returning a final redirect string:

1. `LinkMiddleware` extracts cached link properties including `testVariants` and `testCompletedAt`. Sources: [apps/web/lib/middleware/link.ts:150-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L150-L171)
2. `resolveABTestURL` evaluates the active variants to select a target URL if an A/B test is active. Sources: [apps/web/lib/middleware/link.ts:168-171](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L168-L171)
3. The resulting `testUrl` or `cachedLink.url` is passed to `getFinalUrl` along with the request object and `clickId`. Sources: [apps/web/lib/middleware/link.ts:173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L173)
4. `getFinalUrl` parses query parameters, injects attribution overrides (such as `dub_id` or Stripe parameters), and appends pass-through query parameters from the incoming request. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:25-125](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L25-L125)

```mermaid
sequenceDiagram
    participant Middleware as LinkMiddleware (link.ts)
    participant ABTest as resolveABTestURL
    participant FinalUrl as getFinalUrl (get-final-url.ts)
    Middleware->>ABTest: Evaluate testVariants & testCompletedAt
    ABTest-->>Middleware: Return testUrl (or fallback to cachedLink.url)
    Middleware->>FinalUrl: Invoke with target url, req, and clickId
    FinalUrl->>FinalUrl: Inject attribution & pass-through query parameters
    FinalUrl-->>Middleware: Return fully constructed URL string
```

Sources: [apps/web/lib/middleware/link.ts:168-173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L168-L173), [apps/web/lib/middleware/utils/get-final-url.ts:12-125](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L12-L125)

### Query Parameter Transformation Reference

| Parameter or Rule | Target URL Condition | Action & Transformation Behavior |
| :--- | :--- | :--- |
| `dub_client_reference_id` | Stripe payment links (`dub_client_reference_id === "1"`) | Replaced with `client_reference_id=dub_id_${clickId}` and the original key is deleted. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:41-44](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L41-L44) |
| `dub_id` | General links (when `dub-no-track` is absent) | Injected as `dub_id=${clickId}` to track user conversion attribution. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:47-49](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L47-L49) |
| `pid`, `clickid`, `c`, `af_siteid` | AppsFlyer tracking URLs (`isAppsFlyerTrackingUrl`) | Sets hardcoded `pid=dubinc_int`, passes `clickid`, and populates campaign/site IDs via `via` if missing. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:53-69](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L53-L69) |
| `cl`, `ua`, `ip`, `wpcn`, `wpcl` | Singular tracking URLs (`isSingularTrackingUrl`) | Injects click ID, user agent, IP address, and polyfills integration placeholders like `{via}` and `{dub_id}`. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:72-89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L72-L89) |
| `referrer` | Google Play Store URLs (`isGooglePlayStoreUrl`) | Prepends deep link parameters into the existing encoded referrer string. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:92-101](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L92-L101) |
| Pass-through parameters | All links (excluding `dub-no-track` and `redir_url`) | Appends or overwrites incoming search parameters from the request onto the destination URL. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:107-112](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L107-L112) |

Sources: [apps/web/lib/middleware/utils/get-final-url.ts:41-113](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L41-L113)

> [!WARNING]
> Internal query parameters like `dub-no-track` and redirection control parameters (`redir_url`) are explicitly filtered out during pass-through parameter iteration, preventing them from leaking into external destination URLs. Sources: [apps/web/lib/middleware/utils/get-final-url.ts:108-112](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/get-final-url.ts#L108-L112)

### Partner Link Generation and Attribution Overrides

For partner program enrollments and external integrations, partner links undergo generation via `generatePartnerLink` where keys are derived from usernames, names, or emails, and integration-specific parameters are appended. Sources: [apps/web/lib/api/partners/generate-partner-link.ts:16-40](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/generate-partner-link.ts#L16-L40)

> [!NOTE]
> When `appsFlyerParameters` are supplied during partner link generation, `generatePartnerLink` processes target URLs via `applyAppsFlyerParameters`, interpolating partner context names and link keys directly into the attribution stream. Sources: [apps/web/lib/api/partners/generate-partner-link.ts:147-161](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/partners/generate-partner-link.ts#L147-L161)

## Terminal States and Administrative Restrictions

### Overview

When a requested link fails resolution due to expiration, missing records, or administrative enforcement, Dub routes requests to dedicated terminal pages or executes administrative restriction pipelines. Expired and not-found pages support custom domain-level redirect configurations, whereas banned links invoke backend administrative actions that isolate resources under legal compliance entities. Sources: [apps/web/app/domain/expired/page.tsx:34-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/expired/page.tsx#L34-L42), [apps/web/app/domain/notfound/page.tsx:35-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L35-L43), [apps/web/app/ee/api/admin/links/ban/route.ts:14-46](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts#L14-L46)

### Terminal Pages and Custom Redirection Logic

The expired and not-found route handlers accept dynamic domain parameters, query the primary database via Prisma to inspect custom domain fallback properties, and conditionally trigger Next.js navigation redirects if configuration values exist. Sources: [apps/web/app/domain/expired/page.tsx:30-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/expired/page.tsx#L30-L42), [apps/web/app/domain/notfound/page.tsx:31-43](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L31-L43)

| Terminal Page | Route Path | Database Fallback Property | Default Metadata Title | UI Placeholder Icon |
| :--- | :--- | :--- | :--- | :--- |
| Expired Link | `/[domain]/expired` | `domainData.expiredUrl` | `Expired Link` | `CircleHalfDottedClock` |
| Link Not Found | `/[domain]/notfound` | `domainData.notFoundUrl` | `Link Not Found` | `GlobeSearch` |
| Banned Link | `/[domain]/banned` | None | `Banned Link` | `ShieldSlash` |

Sources: [apps/web/app/domain/expired/page.tsx:14-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/expired/page.tsx#L14-L49), [apps/web/app/domain/notfound/page.tsx:14-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L14-L50), [apps/web/app/domain/banned/page.tsx:12-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/banned/page.tsx#L12-L34)

> [!NOTE]
> All three terminal page components export `revalidate = false` to cache responses indefinitely, and configure `generateStaticParams()` to return an empty array for on-demand static generation. Sources: [apps/web/app/domain/expired/page.tsx:12-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/expired/page.tsx#L12-L28), [apps/web/app/domain/notfound/page.tsx:12-29](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L12-L29), [apps/web/app/domain/banned/page.tsx:10-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/banned/page.tsx#L10-L25)

### Administrative Link Banning Execution

The administrative link banning endpoint (`DELETE /api/admin/links/ban`) is protected by owner-level admin checks and processes incoming query parameters through the domain key schema to identify target resources. Sources: [apps/web/app/ee/api/admin/links/ban/route.ts:13-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts#L13-L20)

```typescript
export const DELETE = withAdmin(
  async ({ searchParams }) => {
    const { domain, key } = domainKeySchema.parse(searchParams);

    const link = await prisma.link.findUnique({
      where: { domain_key: { domain, key } },
    });

    if (!link) {
      return NextResponse.json({ error: "Link not found" }, { status: 404 });
    }

    const urlDomain = getDomainWithoutWWW(link.url);

    const response = await Promise.all([
      prisma.link.update({
        where: { id: link.id },
        data: {
          userId: LEGAL_USER_ID,
          projectId: LEGAL_WORKSPACE_ID,
        },
      }),
      linkCache.set({ ...link, projectId: LEGAL_WORKSPACE_ID }),
      urlDomain && updateConfig({ key: "domains", value: urlDomain }),
    ]);

    return NextResponse.json(response);
  },
  { requiredRoles: ["owner"] },
);
```

Sources: [apps/web/app/ee/api/admin/links/ban/route.ts:14-53](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts#L14-L53)

> [!WARNING]
> Banning a link reassigns its ownership properties (`userId` and `projectId`) to `LEGAL_USER_ID` and `LEGAL_WORKSPACE_ID`, immediately removing management access from standard workspace members while updating the edge cache and edge configuration domains. Sources: [apps/web/app/ee/api/admin/links/ban/route.ts:28-46](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts#L28-L46)

## Deep Linking and Cloaked Previews

### Overview

The deep linking and preview subsystem controls mobile OS routing, app store redirection, iframe cloaking, and metadata proxy inspection. Mobile deep link handling starts at `DeepLinkPreviewPage`, which parses request headers, evaluates the client operating system via Next.js `userAgent`, and queries Prisma for domain-level asset configurations. Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:45-90](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx#L45-L90)

### Mobile Deep Link Routing and Redirection

When a request enters the deep link handler, the system performs a multi-step platform validation to determine whether to display a deep view preview card or trigger an immediate platform redirect. Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:58-137](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx#L58-L137)

```mermaid
sequenceDiagram
  autonumber
  participant Client as Mobile Client
  participant Page as DeepLinkPreviewPage
  participant DB as Prisma Database

  Client->>Page: GET /deeplink/[domain]/[[...key]]
  Page->>Page: Detect OS via userAgent (iOS / Android)
  Page->>DB: prisma.link.findUnique (domain & encodedKey)
  DB-->>Page: Link & shortDomain data
  alt Link missing
    Page-->>Client: redirect(https://[domain])
  else Missing deep linking setup (assetLinks / AASA)
    Page-->>Client: redirect(platform fallback URL or link.url)
  else Valid Deep View setup
    Page->>Page: Parse deepviewData, validate Android package name
    Page-->>Client: Render Deep View preview page with badge & action button
  end
```

Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:58-137](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx#L58-L137)

> [!WARNING]
> If a short domain lacks `appleAppSiteAssociation` or `assetLinks` configurations combined with valid `deepviewData`, the preview page is bypassed entirely, immediately issuing a server-side redirect to `link.ios`, `link.android`, or the canonical `link.url`. Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:102-110](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx#L102-L110)

### Metadata Proxy and Link Inspector Architecture

Dub provides dedicated routes for metadata proxying and link inspection, allowing clients to examine short links or render masked link previews safely via edge runtimes. Sources: [apps/web/app/domain/key/proxy/page.tsx:1-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx#L1-L34), [apps/web/app/domain/key/inspect/page.tsx:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L1-L39), [apps/web/app/cloaked/url/page.tsx:1-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx#L1-L38)

| Route File Path | Runtime | Primary Function | Metadata Extraction Source |
| :--- | :--- | :--- | :--- |
| `apps/web/app/[domain]/[key]/proxy/page.tsx` | Node / Default | Renders proxy card with preview image and favicon | `getLinkViaEdge` Sources: [apps/web/app/domain/key/proxy/page.tsx:11-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx#L11-L34) |
| `apps/web/app/[domain]/[key]/inspect/page.tsx` | `edge` | Renders interactive `LinkInspectorCard` and `LinkPreview` | `getLinkViaEdge` Sources: [apps/web/app/domain/key/inspect/page.tsx:15-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L15-L39) |
| `apps/web/app/cloaked/[url]/page.tsx` | Node / Default | Renders full-screen iframe pointing to destination URL | `getMetaTags(url)` Sources: [apps/web/app/cloaked/url/page.tsx:22-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx#L22-L38) |
| `apps/web/app/api/links/iframeable/route.ts` | `edge` | Validates if a destination URL permits embedding in iframes | `isIframeable` Sources: [apps/web/app/api/links/iframeable/route.ts:10-22](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L10-L22) |

Sources: [apps/web/app/domain/key/proxy/page.tsx:1-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx#L1-L34), [apps/web/app/domain/key/inspect/page.tsx:1-39](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L1-L39), [apps/web/app/cloaked/url/page.tsx:1-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx#L1-L38), [apps/web/app/api/links/iframeable/route.ts:10-22](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L10-L22)

> [!TIP]
> The `/api/links/iframeable` endpoint enforces rate limiting via `ratelimitOrThrow(req, "iframeable")` prior to invoking `isIframeable` to prevent abuse of external target inspection. Sources: [apps/web/app/api/links/iframeable/route.ts:17-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/links/iframeable/route.ts#L17-L20)

### Cloaked URL Destination Resolution

When requests hit the cloaked URL handler at `apps/web/app/cloaked/[url]/page.tsx`, `getCloakedDestinationUrl` evaluates whether the dynamic route parameter requires double-decoding. Sources: [apps/web/app/cloaked/url/page.tsx:8-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx#L8-L20)

```typescript
function getCloakedDestinationUrl(param: string): string {
  if (/^https?%3A/i.test(param)) {
    try {
      return decodeURIComponent(param);
    } catch {
      return param;
    }
  }

  return param;
}
```

Sources: [apps/web/app/cloaked/url/page.tsx:8-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/cloaked/%5Burl%5D/page.tsx#L8-L20)

## Legacy Crawling and Resolution Fallbacks

### Overview

When an incoming link lookup results in a cache and edge database miss, the resolution pipeline invokes specialized fallback handlers to recover the link or ingest it dynamically. For legacy Bitly short links matching specific domains such as `buff.ly`, the link middleware delegates resolution to an on-demand crawler that queries the Bitly API, materializes the record into the local database, and issues a redirection. Sources: [apps/web/lib/middleware/link.ts:100-117](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L100-L117), [apps/web/lib/middleware/utils/crawl-bitly.ts:20-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L68)

```mermaid
sequenceDiagram
  autonumber
  participant Client as Client Request
  participant MW as LinkMiddleware
  participant DB as Edge DB / Cache
  participant Bitly as Bitly API
  participant PG as Prisma Database

  Client->>MW: Inbound Request (domain, key)
  MW->>DB: linkCache.get() / getLinkViaEdge()
  DB-->>MW: Miss (null)
  alt Domain is buff.ly
    MW->>Bitly: crawlBitly() → fetchBitlyLink()
    Bitly-->>MW: { long_url, created_at }
    MW->>PG: prisma.link.create() (Buffer Workspace)
    MW->>Client: NextResponse.redirect(long_url, 302)
  else Standard Domain Miss
    MW-->>Client: Rewrite to /[domain]/notfound
  end
```

Sources: [apps/web/lib/middleware/link.ts:100-117](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L100-L117), [apps/web/lib/middleware/utils/crawl-bitly.ts:20-68](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L68)

### Bitly Legacy Crawler and On-Demand Ingestion

The `crawlBitly` utility inspects the request parameters and validates the key against unsupported character patterns before querying the remote Bitly API. Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:20-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L28)

```typescript
const invalidBitlyKeyRegex = /[`~,.<>;':"/\\[\]^{}()=+!*@&$£?%#|]/;
```

If the key is valid and exists in Bitly's system, the crawler extracts the long URL and persists a new link record asynchronously using `ev.waitUntil`. The newly created link is assigned to a designated Buffer workspace, user, and folder configuration using fixed system IDs. Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:27-61](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L27-L61)

| Constant Name | Value | Purpose |
| :--- | :--- | :--- |
| `BUFFER_WORKSPACE_ID` | `cm05wnnpo000711ztj05wwdbu` | Workspace ID assigned to auto-ingested Bitly links Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15) |
| `BUFFER_USER_ID` | `cm05wnd49000411ztg2xbup0i` | System user ID associated with ingested links Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L16) |
| `BUFFER_FOLDER_ID` | `fold_1JNQBVZV8P0NA0YGB11W2HHSQ` | Default folder ID for ingested Bitly links Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L17) |

Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15-17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L17)

> [!WARNING]
> If the Bitly API rate limit is exceeded or the link cannot be found, `fetchBitlyLink` returns `null`, causing the crawler to redirect fallback traffic directly to `https://buffer.com` with a 24-hour cache control header. Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:71-81](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L71-L81)

### Workspace OAuth Integration and Domain Sync

For workspace-level migrations and bulk operations, Bitly integration tokens are exchanged via OAuth and stored securely in Redis. The callback route at `apps/web/app/api/callback/bitly/route.ts` handles token exchange and redirects users back to their workspace slug with query parameters. Sources: [apps/web/app/api/callback/bitly/route.ts:10-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/bitly/route.ts#L10-L59)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| **Edge-based Link Caching (`linkCache`)** | Sub-millisecond read performance and high availability during traffic spikes Sources: [apps/web/lib/middleware/link.ts:89-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L122) | Potential staleness requiring cache invalidation hooks upon updates Sources: [apps/web/app/ee/api/cron/domains/update/route.ts:103-104](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/domains/update/route.ts#L103-L104) |
| **Asynchronous Background Ingestion (`ev.waitUntil`)** | Keeps redirect latency minimal while writing back-fill audit logs and metrics Sources: [apps/web/lib/middleware/link.ts:124-147](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L124-L147) | Operations outside the main response thread can fail silently if not wrapped in `Promise.allSettled` |
| **Dedicated System Workspace Constants** | Isolates automated external crawl ingestion from user-created assets Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L18) | Hardcoded IDs require environment synchronization across deployments Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L18) |

Sources: [apps/web/lib/middleware/link.ts:89-147](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L89-L147), [apps/web/lib/middleware/utils/crawl-bitly.ts:15-61](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L61), [apps/web/app/api/callback/bitly/route.ts:20-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/bitly/route.ts#L20-L59)

## Related

- [Routing and Multitenancy](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/routing-and-multitenancy)
- [A/B Testing and Targeting](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/a-b-testing-and-targeting)
- [Tinybird Analytics Engine](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/tinybird-analytics-engine)


## Sitemap

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