---
title: "Routing and Multitenancy"
description: "Dub employs a sophisticated edge-based routing and multitenancy architecture built on Next.js middleware and App Router zones. This system handles global request interception, hostname classificati..."
last_updated: "2026-10-05T05:07:35.174967+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/routing-and-multitenancy"
---

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

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

- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts)
- [apps/web/lib/middleware/link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts)
- [apps/web/lib/middleware/app.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/app/ee/api/track/application/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/track/application/route.ts)
- [apps/web/app/api/domains/domain/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts)
- [apps/web/lib/middleware/admin.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts)
- [apps/web/lib/middleware/partners.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts)
- [apps/web/lib/middleware/embed.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/embed.ts)
- [apps/web/app/domain/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx)
- [apps/web/lib/middleware/api.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/api.ts)
- [apps/web/app/app.dub.co/marketplace/...segments/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/marketplace/%5B%5B...segments%5D%5D/page.tsx)
- [apps/web/app/domain/notfound/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/notfound/page.tsx)
- [packages/utils/src/constants/main.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/main.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/page-client.tsx)
- [apps/web/app/api/oauth/userinfo/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/oauth/userinfo/route.ts)
- [apps/web/next.config.js](https://github.com/blade47/dub/blob/HEAD/apps/web/next.config.js)
- [apps/web/lib/middleware/utils/parse.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/parse.ts)
- [apps/web/app/sitemap.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts)
- [apps/web/app/domain/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/layout.tsx)
- [packages/utils/src/constants/middleware.ts](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/middleware.ts)
- [apps/web/lib/middleware/workspaces.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.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/api/domains/default/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts)
- [apps/web/app/app.dub.co/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/layout.tsx)
- [apps/web/lib/middleware/create-link.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/create-link.ts)
- [apps/web/ui/program-marketplace/marketplace-router.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-router.tsx)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/program/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/page.tsx)
- [apps/web/app/ee/partners.dub.co/layout.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/layout.tsx)
- [apps/web/app/app.dub.co/redirects/slug/domains/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(redirects)/%5Bslug%5D/domains/page.tsx)
</details>

## Overview

Dub employs a sophisticated edge-based routing and multitenancy architecture built on Next.js middleware and App Router zones. This system handles global request interception, hostname classification, tenant isolation across specialized subdomains, and dynamic custom domain link resolution. By routing requests efficiently at the edge, Dub separates core application dashboards, partner portals, administrative interfaces, and short link redirection flows while maintaining high-performance caching and robust security boundaries. 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-224](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L224), [apps/web/lib/middleware/app.ts:26-130](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L26-L130), [apps/web/lib/middleware/admin.ts:8-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L38), [apps/web/lib/middleware/partners.ts:25-122](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L25-L122)

## Edge Middleware Dispatcher and Request Parsing

### Edge Middleware Dispatcher and Request Parsing

The Next.js edge middleware acts as the global request interception and routing dispatcher for Dub. Configured to run on the `nodejs` runtime with an explicit path matcher, the middleware excludes internal Next.js routes (`/_next/`), proxies (`/_proxy/`), API routes (`/api/`), and static metadata files (`favicon.ico`, `sitemap.xml`, `robots.txt`, `manifest.webmanifest`) while intercepting all other inbound traffic. 

Sources: [apps/web/middleware.ts:20-32](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L20-L32)

### Request Parsing and Normalization

When a request arrives at `middleware()`, it is first processed by the `parse(req)` utility function. This function extracts the host header and pathname, normalizes the domain by removing `www.` prefixes and converting characters to lowercase, and resolves domain aliases via `DOMAIN_REDIRECTS`. It also checks for End-to-End (E2E) redirect test requests—identifying local `dub.localhost:8888` environments or preview deployments carrying the `x-e2e-redirect-test` header—and routes them to test or short domains accordingly. Finally, query parameters and decoded URL segments (`key` and `fullKey`) are extracted before returning a standardized parsed context object.

Sources: [apps/web/middleware.ts:35-35](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L35-L35), [apps/web/lib/middleware/utils/parse.ts:1-55](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/parse.ts#L1-L55)

```typescript
export const parse = (req: NextRequest) => {
  let domain = req.headers.get("host") as string;
  let path = req.nextUrl.pathname;
  domain = domain.replace(/^www./, "").toLowerCase();
  if (DOMAIN_REDIRECTS[domain]) {
    domain = DOMAIN_REDIRECTS[domain];
  }
  const key = decodeURIComponent(path.split("/")[1]);
  const fullKey = decodeURIComponent(path.slice(1));
  return { domain, path, key, fullKey, ... };
};
```
Sources: [apps/web/lib/middleware/utils/parse.ts:5-54](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/parse.ts#L5-L54)

### Routing Dispatcher Execution Flow

Once parsing completes, the dispatcher executes logging via Axiom (`logger.info` and `ev.waitUntil(logger.flush())`) and evaluates the normalized domain and path against a sequential series of conditional branches to hand off execution to specialized middleware handlers.

```
middleware(req, ev) 
  → parse(req) 
  → isAppHostname(domain) ? AppMiddleware(req) 
  → API_HOSTNAMES.has(domain) ? ApiMiddleware(req) 
  → path.startsWith("/stats/") ? NextResponse.rewrite(...) 
  → path.startsWith("/.well-known/") ? NextResponse.rewrite(...) 
  → domain === "dub.sh" && DEFAULT_REDIRECTS[key] ? NextResponse.redirect(...) 
  → ADMIN_HOSTNAMES.has(domain) ? AdminMiddleware(req) 
  → PARTNERS_HOSTNAMES.has(domain) ? PartnersMiddleware(req) 
  → isValidUrl(fullKey) ? CreateLinkMiddleware(req) 
  → LinkMiddleware(req, ev)
```
Sources: [apps/web/middleware.ts:34-89](https://github.com/blade47/dub/blob/HEAD/apps/web/middleware.ts#L34-L89)

### Hostname Constants and Default Redirects

The dispatcher relies on centralized collections of hostnames and default short-domain redirection rules defined across utility constants.

| Hostname Constant / Rule | Target / Value Set | Purpose |
| :--- | :--- | :--- |
| `API_HOSTNAMES` | `api.dub.co`, `api-staging.dub.co`, `api.dub.sh`, `api.localhost:8888`, `api.localhost` | Identifies API server requests for `ApiMiddleware`. |
| `ADMIN_HOSTNAMES` | `admin.dub.co`, `admin.localhost:8888`, `admin.localhost` | Identifies internal administration requests for `AdminMiddleware`. |
| `PARTNERS_HOSTNAMES` | `partners.dub.co`, `partners-staging.dub.co`, `partners.localhost:8888`, `partners.localhost` | Identifies partner portal requests for `PartnersMiddleware`. |
| `DEFAULT_REDIRECTS` | `home`, `dub`, `signin`, `login`, `register`, `signup`, `app`, `dashboard`, `links`, `settings`, `welcome`, `discord` | Provides fallback shortcut redirects on the `dub.sh` short domain. |

Sources: [packages/utils/src/constants/main.ts:3-29](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/main.ts#L3-L29), [packages/utils/src/constants/middleware.ts:1-14](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/middleware.ts#L1-L14)

> [!NOTE]
> Preview environments evaluate `isAppHostname` by verifying whether the hostname starts with `dub-` and ends with `.dub.co`, whereas production environments strictly match against `app.dub.co`, `localhost:8888`, and `localhost`.
Sources: [packages/utils/src/constants/main.ts:59-65](https://github.com/blade47/dub/blob/HEAD/packages/utils/src/constants/main.ts#L59-L65)

## Application and Workspace Subdomain Routing

### Overview

The `AppMiddleware` function manages traffic arriving on `app.dub.co` and application subdomains. It processes incoming requests by parsing URL attributes, authenticating user sessions via tokens, managing onboarding flows for newly registered accounts, resolving default workspaces, and orchestrating rewrites or redirects into the Next.js App Router layout.

Sources: [apps/web/lib/middleware/app.ts:26-130](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L26-L130), [apps/web/app/app.dub.co/layout.tsx:1-12](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/layout.tsx#L1-L12)

### Authentication and Public Path Interception

When a request enters `AppMiddleware`, it parses the request components and inspects embed paths, bypassing authentication for predefined public routes or redirecting unauthenticated users to `/login`.

```
AppMiddleware(req) 
  → parse(req) 
  → path.startsWith("/embed") ? EmbedMiddleware(req) 
  → getUserViaToken(req) 
  → (!user && !isPublicPath(path)) ? NextResponse.redirect(/login?next=...) 
  → user && !isPublicPath(path) ? [Onboarding or Workspace Routing]
```
Sources: [apps/web/lib/middleware/app.ts:26-52](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L26-L52)

Public paths that bypass authentication checks are verified via the `isPublicPath` helper function, which matches exact paths and path prefixes.

| Path Match Type | Target Paths and Prefixes | Purpose |
| :--- | :--- | :--- |
| Exact Match | `/marketplace` | Allows public access to marketplace listings. |
| Prefix Match | `/marketplace/`, `/share/`, `/deeplink/`, `/unsubscribe/`, `/auth/reset-password/` | Permits unauthenticated handling for shared links, redirects, preference management, and password resets. |

Sources: [apps/web/lib/middleware/app.ts:16-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L16-L24)

### Onboarding Redirect Logic

For authenticated users within the onboarding window (`ONBOARDING_WINDOW_SECONDS`), the middleware evaluates whether the user has a default workspace, pending project invites, or a completed onboarding status via `onboardingStepCache`.

```
User creation check (createdAt < ONBOARDING_WINDOW_SECONDS)
  → path not in [/onboarding, /account, /workspaces]
  → !(getDefaultWorkspace(user))
  → !(hasPendingInvites(req, user))
  → onboardingStepCache != "completed"
      → step missing? → /onboarding
      → step == "completed" → WorkspacesMiddleware(req, user)
      → defaultWorkspace exists? → /onboarding/${step}?workspace=${defaultWorkspace}
      → else → /onboarding
```
Sources: [apps/web/lib/middleware/app.ts:65-91](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L65-L91)

> [!WARNING]
> The onboarding window condition evaluates `new Date(user.createdAt).getTime() > Date.now() - ONBOARDING_WINDOW_SECONDS * 1000`. If a user was created outside this active timeframe, onboarding redirects are bypassed in favor of standard workspace resolution.
Sources: [apps/web/lib/middleware/app.ts:66-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/app.ts#L66-L67)

### Workspace Tenant Resolution (`WorkspacesMiddleware`)

When requests target core application dashboard routes (`/`, `/links`, `/analytics`, `/events`, `/upgrade`, `/guides`, `/wrapped`, `/programs`, `/program`, `/customers`, `/settings`) or when onboarding completes, control passes to `WorkspacesMiddleware`. 

```typescript
export async function WorkspacesMiddleware(req: NextRequest, user: UserProps) {
  const { path, searchParamsObj, searchParamsString } = parse(req);

  if (
    searchParamsObj.next &&
    isValidInternalRedirect({
      redirectPath: searchParamsObj.next,
      currentUrl: req.url,
    })
  ) {
    return NextResponse.redirect(new URL(searchParamsObj.next, req.url));
  }

  const defaultWorkspace = await getDefaultWorkspace(user);

  if (defaultWorkspace) {
    let redirectPath = path;
    if (["/", "/login", "/register"].includes(path)) {
      redirectPath = "";
    } else if (isTopLevelSettingsRedirect(path)) {
      redirectPath = `/settings/${path}`;
    }

    if (!redirectPath) {
      const product = await getWorkspaceProduct(defaultWorkspace);
      redirectPath = `/${product}`;
    }

    return NextResponse.redirect(
      new URL(
        `/${defaultWorkspace}${redirectPath}${searchParamsString}`,
        req.url,
      ),
    );
  }

  const projectInvite = await prismaEdge.projectInvite.findFirst({
    where: { email: user.email },
    select: { project: { select: { slug: true } } },
  });

  if (projectInvite) {
    return NextResponse.redirect(
      new URL(`/${projectInvite.project.slug}/invite`, req.url),
    );
  }

  return NextResponse.redirect(new URL("/onboarding/workspace", req.url));
}
```
Sources: [apps/web/lib/middleware/workspaces.ts:10-70](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.ts#L10-L70)

> [!TIP]
> `WorkspacesMiddleware` checks `searchParamsObj.next` using `isValidInternalRedirect` before routing users to their default tenant workspace, preventing open redirect vulnerabilities across internal application routes.
Sources: [apps/web/lib/middleware/workspaces.ts:13-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/workspaces.ts#L13-L22)

### Program Onboarding Sub-Step Verification

During the onboarding sequence, the step page for program configuration (`ProgramPage`) validates workspace ownership and user session tokens before rendering client components.

```typescript
export default async function ProgramPage({
  searchParams,
}: {
  searchParams: Promise<{ workspace?: string }>;
}) {
  const { workspace: slug } = await searchParams;

  if (!slug) redirect("/onboarding");

  const { user } = await getSession();

  const workspace = await prisma.project.findUniqueOrThrow({
    where: {
      slug,
      users: {
        some: {
          userId: user.id,
        },
      },
    },
    select: {
      id: true,
      domains: {
        orderBy: {
          createdAt: "desc",
        },
        take: 1,
      },
    },
  });

  const data = await redis.get<{ domain: string; userId: string }>(
    `onboarding-domain:${workspace.id}`,
  );

  const onboardingDomain =
    data && data.domain && data.userId === user.id ? data.domain : null;

  const domain = onboardingDomain || workspace.domains[0]?.slug;

  if (!domain)
    redirect(`/onboarding/domain?workspace=${slug}&product=partners`);

  return <ProgramPageClient domain={domain} />;
}
```
Sources: [apps/web/app/app.dub.co/onboarding/onboarding/steps/program/page.tsx:7-51](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/page.tsx#L7-L51)

## Partners and Admin Subdomain Isolation

### Partners and Admin Subdomain Isolation

Subdomain isolation for administrative tools and partner portals is managed by dedicated edge middleware functions. These intercept requests destined for restricted domains, evaluate authorization credentials against edge database records, and enforce role-based access before rewriting or redirecting traffic.

### Administrative Middleware Execution (`AdminMiddleware`)

The `AdminMiddleware` function handles incoming requests targeting administrative boundaries. It parses the request URL, retrieves user authentication tokens via `getUserViaToken`, and enforces strict project-membership checks against the `DUB_WORKSPACE_ID` constant using `prismaEdge`.

```typescript
export async function AdminMiddleware(req: NextRequest) {
  const { path } = parse(req);

  const user = await getUserViaToken(req);

  if (!user && path !== "/login") {
    return NextResponse.redirect(new URL("/login", req.url));
  } else if (user) {
    const isAdminUser = await prismaEdge.projectUsers.findUnique({
      where: {
        userId_projectId: {
          userId: user.id,
          projectId: DUB_WORKSPACE_ID,
        },
      },
    });

    if (!isAdminUser) {
      return NextResponse.next(); // throw 404 page
    } else if (
      path === "/login" ||
      !canAccessAdminPath({ userId: user.id, pathname: path })
    ) {
      return NextResponse.redirect(new URL("/", req.url));
    }
  }

  return NextResponse.rewrite(
    new URL(`/admin.dub.co${path === "/" ? "" : path}`, req.url),
  );
}
```
Sources: [apps/web/lib/middleware/admin.ts:8-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L38)

> [!CAUTION]
> If a user is authenticated but lacks membership in the designated admin project (`DUB_WORKSPACE_ID`), `AdminMiddleware` returns `NextResponse.next()` which results in a 404 error page rather than an authorization error, concealing the existence of the admin portal.
Sources: [apps/web/lib/middleware/admin.ts:16-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L16-L26)

### Partner Portal Routing and Middleware (`PartnersMiddleware`)

The partner portal middleware governs access to program discovery, affiliate links, and earnings management on `partners.dub.co`. It enforces authentication on specific paths defined by `AUTHENTICATED_PATHS` and manages redirection flows based on partner profile status.

```typescript
const AUTHENTICATED_PATHS = [
  "/programs",
  "/marketplace",
  "/onboarding",
  "/settings",
  "/profile",
  "/messages",
  "/payouts",
  "/account",
  "/invite",
  "/rewind",
];
```
Sources: [apps/web/lib/middleware/partners.ts:12-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L12-L23)

When an unauthenticated request targets an authenticated path, `PartnersMiddleware` intercepts the request. If the path targets a specific program route like `/programs/`, it extracts the program slug to route the user directly to that program's login page; otherwise, it appends a `?next=` query parameter pointing to the original destination.

```typescript
  if (!user && isAuthenticatedPath) {
    if (path.startsWith("/programs/")) {
      const programSlug = path.split("/")[2];
      return NextResponse.redirect(new URL(`/${programSlug}/login`, req.url));
    }

    return NextResponse.redirect(
      new URL(
        `/login${path === "/" ? "" : `?next=${encodeURIComponent(fullPath)}`}`,
        req.url,
      ),
    );
  }
```
Sources: [apps/web/lib/middleware/partners.ts:63-74](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L63-L74)

> [!TIP]
> `PartnersMiddleware` validates internal redirects using `isValidInternalRedirect` when processing `?next=` query parameters, ensuring that open redirect attacks are mitigated before returning the response.
Sources: [apps/web/lib/middleware/partners.ts:93-102](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/partners.ts#L93-L102)

### Marketplace Routing and Dynamic Layouts

The marketplace subdomain infrastructure relies on slug-based segmentation. The marketplace page loader processes URL segments dynamically via `generateMetadata` and `MarketplaceRouter`, supporting categories and individual program listings.

```typescript
export function MarketplaceRouter({ segments = [] }: { segments?: string[] }) {
  if (segments.length === 0) {
    return (
      <PageContent title="Program marketplace">
        <PageWidthWrapper className="pb-10">
          <MarketplaceHomePage />
        </PageWidthWrapper>
      </PageContent>
    );
  }

  if (segments.length === 1 && segments[0] === "all") {
    return <ListPage title={<MarketplaceListTitle />} />;
  }

  if (segments.length === 2 && segments[0] === "c") {
    const category = slugToCategory(segments[1]);

    if (category) {
      return <ListPage title={<MarketplaceListTitle category={category} />} />;
    }
  }

  if (segments.length === 1) {
    return <MarketplaceProgramPage programSlug={segments[0]} />;
  }

  notFound();
}
```
Sources: [apps/web/ui/program-marketplace/marketplace-router.tsx:38-66](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/program-marketplace/marketplace-router.tsx#L38-L66)

The partners layout wraps client children within NextAuth's `SessionProvider` alongside a React `Suspense` boundary to preserve session state across partner routes.

```typescript
export default function PartnersLayout({ children }: { children: ReactNode }) {
  return (
    <SessionProvider>
      <Suspense>{children}</Suspense>
    </SessionProvider>
  );
}
```
Sources: [apps/web/app/ee/partners.dub.co/layout.tsx:1-12](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/partners.dub.co/layout.tsx#L1-L12)

## Custom Domain Link Resolution and Rewriting

### Overview

The custom domain link resolution and rewriting pipeline executes within `LinkMiddleware`, governing incoming requests across custom and shortened domains. It handles normalization, case sensitivity checks, inspection mode toggles, edge cache lookups, database fallbacks, Bitly crawls, and click identity minting before executing redirects or rewrites.
Sources: [apps/web/lib/middleware/link.ts:43-224](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L224)

### Request Lifecycle and Resolution Call Chain

When a request arrives at `LinkMiddleware`, it proceeds through a strict sequence of validation and lookup stages. The call chain runs as follows: `parse(req)` → `punyEncode()` → `isCaseSensitiveDomain()` → `isUnsupportedKey()` → `linkCache.get()` → `getLinkViaEdge()` → `formatRedisLink()` → `resolveABTestURL()` → click identifier minting.
Sources: [apps/web/lib/middleware/link.ts:44-211](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L44-L211)

```typescript
export async function LinkMiddleware(req: NextRequest, ev: NextFetchEvent) {
  let { domain, fullKey: originalKey, fullPath, searchParamsObj } = parse(req);

  if (!domain) {
    return NextResponse.next();
  }

  let key = punyEncode(originalKey);

  if (!isCaseSensitiveDomain(domain)) {
    key = key.toLowerCase();
  }
...
```
Sources: [apps/web/lib/middleware/link.ts:43-56](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L43-L56)

> [!NOTE]
> Links on Dub are case-insensitive by default. When `isCaseSensitiveDomain(domain)` returns false, keys are automatically lowercased after puny-encoding, whereas case-sensitive domains preserve original casing.
Sources: [apps/web/lib/middleware/link.ts:50-56](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L50-L56)

### Bitly Crawl Fallback

When a link lookup via edge storage (`getLinkViaEdge`) misses and the target domain is explicitly `buff.ly`, the middleware invokes `crawlBitly`. This utility queries the Bitlink API, validates key characters against Bitly's unsupported character rules, and creates links on-demand in the database under a dedicated buffer workspace.
Sources: [apps/web/lib/middleware/link.ts:100-109](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L100-L109), [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)

```typescript
const BUFFER_WORKSPACE_ID = "cm05wnnpo000711ztj05wwdbu";
const BUFFER_USER_ID = "cm05wnd49000411ztg2xbup0i";
const BUFFER_FOLDER_ID = "fold_1JNQBVZV8P0NA0YGB11W2HHSQ";
const BUFFER_BITLY_API_KEY = process.env.BUFFER_BITLY_API_KEY;

export const crawlBitly = async (req: NextRequest, ev: NextFetchEvent) => {
  const { domain, fullKey: key } = parse(req);
  const invalidBitlyKeyRegex = /[`~,.<>;':"/\\[\]^{}()=+!*@&$£?%#|]/;

  if (key && !invalidBitlyKeyRegex.test(key)) {
    const link = await fetchBitlyLink({ domain, key });
    if (link) {
      const sanitizedUrl = getUrlFromStringIfValid(link.long_url);
      if (sanitizedUrl) {
        ev.waitUntil(
          prisma.link.create({
            data: {
              id: createId({ prefix: "link_" }),
              domain,
              key: encodeKeyIfCaseSensitive({ domain, key }),
              url: sanitizedUrl,
              shortLink: linkConstructorSimple({ domain, key }),
              projectId: BUFFER_WORKSPACE_ID,
              userId: BUFFER_USER_ID,
              folderId: BUFFER_FOLDER_ID,
              createdAt: new Date(link.created_at),
            },
          }),
        );
      }
      return NextResponse.redirect(link.long_url, { headers: DUB_HEADERS, status: 302 });
    }
  }
  return NextResponse.redirect("https://buffer.com", { status: 302 });
};
```
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15-81](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L81)

> [!WARNING]
> If a Bitly lookup hits a rate limit or returns a non-OK response status, `fetchBitlyLink` logs the rate limit event and returns `null`, causing `crawlBitly` to fall back to redirecting users to `https://buffer.com`.
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:71-105](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L71-L105)

### Design Choices in Link Resolution

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| **Redis & Edge Caching (`linkCache`)** | Minimizes database queries for high-throughput link redirection | Requires background revalidation and cache synchronization handling |
| **Redis Failover Detection** | Prevents request timeouts and cascading failures during Redis outages | Bypasses click tracking and cookie persistence temporarily |
| **Inspect Mode (`+` suffix)** | Enables quick diagnostics and metadata inspection without redirecting | Requires parsing and stripping trailing characters from keys |
| **On-Demand Bitly Crawling** | Automatically migrates and provisions legacy links on `buff.ly` | Depends on external API rate limits and upstream availability |

Sources: [apps/web/lib/middleware/link.ts:58-98](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L58-L98), [apps/web/lib/middleware/link.ts:124-148](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/link.ts#L124-L148), [apps/web/lib/middleware/utils/crawl-bitly.ts:20-111](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L111)

## App Router Directory Structure Multitenancy

### Overview

Dub's App Router directory structure implements multi-zone and multitenant routing across custom domains, application subdomains, and partner subdomains. Dynamic route groups handle custom domains via `[domain]`, while legacy workspace paths utilize redirects to enforce clean URL structures.
Sources: [apps/web/app/domain/page.tsx:1-86](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx#L1-L86), [apps/web/app/app.dub.co/redirects/slug/domains/page.tsx:1-10](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(redirects)/%5Bslug%5D/domains/page.tsx#L1-L10)

### Custom Domain Layout and Rendering

The `[domain]` directory handles requests landing on user-owned custom domains. The layout component (`apps/web/app/[domain]/layout.tsx`) wraps all external custom pages in a flexible column container configured with standard navigation elements (`NavMobile`, `Nav`) and a page footer (`Footer`).
Sources: [apps/web/app/domain/layout.tsx:1-16](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/layout.tsx#L1-L16)

```typescript
export default function ExternalPagesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen flex-col justify-between bg-neutral-50/80">
      <NavMobile />
      <Nav maxWidthWrapperClassName="max-w-screen-lg lg:px-4 xl:px-0" />
      {children}
      <Footer className="max-w-screen-lg border-0 bg-transparent lg:px-4 xl:px-0" />
    </div>
  );
}
```
Sources: [apps/web/app/domain/layout.tsx:3-16](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/layout.tsx#L3-L16)

The root page (`apps/web/app/[domain]/page.tsx`) disables static generation parameters (`generateStaticParams` returns `[]`) and caches responses indefinitely via `export const revalidate = false`. Metadata is dynamically constructed using `generateMetadata`, embedding the uppercase domain name into the page title and description.
Sources: [apps/web/app/domain/page.tsx:11-28](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx#L11-L28)

```typescript
export async function generateMetadata(props: {
  params: Promise<{ domain: string }>;
}) {
  const params = await props.params;
  const title = `${params.domain.toUpperCase()} - A Dub Custom Domain`;
  const description = `${params.domain.toUpperCase()} is a custom domain on Dub - the modern link attribution platform for short links, conversion tracking, and affiliate programs.`;

  return constructMetadata({
    title,
    description,
  });
}
```
Sources: [apps/web/app/domain/page.tsx:13-24](https://github.com/blade47/dub/blob/HEAD/apps/web/app/%5Bdomain%5D/page.tsx#L13-L24)

> [!NOTE]
> When a custom domain link lookup fails or points to a non-existent path, `NotFoundLinkPage` queries Prisma for `domainData`. If a custom `notFoundUrl` is configured on the domain model, the request immediately redirects to that target URL.
Sources: [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)

### Sitemap and Workspace Redirects

The sitemap generator (`apps/web/app/sitemap.ts`) inspects incoming request host headers to differentiate between standard domains, partner hostnames (`PARTNERS_HOSTNAMES`), and core application hostnames (`isAppHostname`). When handling partner domains, it queries published program lander data to construct partner sitemap entries.
Sources: [apps/web/app/sitemap.ts:11-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts#L11-L44)

```typescript
  if (PARTNERS_HOSTNAMES.has(domain)) {
    const programs = await prisma.program.findMany({
      where: {
        groups: {
          some: {
            slug: "default",
            landerData: {
              not: Prisma.AnyNull,
            },
            landerPublishedAt: {
              not: null,
            },
          },
        },
      },
      orderBy: {
        slug: "asc",
      },
    });

    return programs.map((program) => ({
      url: `https://partners.dub.co/${program.slug}`,
      lastModified: new Date(),
    }));
  }
```
Sources: [apps/web/app/sitemap.ts:20-44](https://github.com/blade47/dub/blob/HEAD/apps/web/app/sitemap.ts#L20-L44)

Legacy workspace domain routes under `app.dub.co` are automatically normalized through redirect handlers. For instance, `OldWorkspaceDomains` intercepts requests to `/[slug]/domains` and permanently redirects clients to the updated settings path.
Sources: [apps/web/app/app.dub.co/redirects/slug/domains/page.tsx:1-10](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(redirects)/%5Bslug%5D/domains/page.tsx#L1-L10)

```typescript
export default async function OldWorkspaceDomains(props: {
  params: Promise<{
    slug: string;
  }>;
}) {
  const params = await props.params;
  redirect(`/${params.slug}/settings/domains`);
}
```
Sources: [apps/web/app/app.dub.co/redirects/slug/domains/page.tsx:3-10](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(redirects)/%5Bslug%5D/domains/page.tsx#L3-L10)

## Custom Domain Provisioning and Lifecycle

### Overview

Custom domain provisioning and lifecycle operations are handled through workspace-authenticated API endpoints and dashboard client components. The system manages domain records via Prisma transactions, coordinates provisioning with Vercel when running in production environments (`process.env.VERCEL === "1"`), and tracks default platform short domains assigned to individual workspaces.
Sources: [apps/web/app/api/domains/route.ts:23-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L23-L173), [apps/web/app/api/domains/default/route.ts:9-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L9-L82)

### Domain Creation and Vercel Integration

When a new domain is registered via `POST /api/domains`, the request body is parsed and validated against `createDomainBodySchemaExtended`. Free plan workspaces are restricted from configuring advanced branding parameters such as custom QR code logos, expiration URLs, not found URLs, Asset Links, Apple App Site Association, or Deep View configurations.
Sources: [apps/web/app/api/domains/route.ts:97-137](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L97-L137)

The lifecycle execution sequence for adding a domain follows a strict operational call-chain:
`parseRequestBody()` → `createDomainBodySchemaExtended.parseAsync()` → `validateDomain()` → `addDomainToVercel()` → `storage.upload()` → `prisma.$transaction()`
Sources: [apps/web/app/api/domains/route.ts:99-184](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L99-L184)

> [!NOTE]
> During Vercel domain provisioning (`addDomainToVercel`), if Vercel returns an error code equal to `domain_already_in_use`, the API explicitly ignores the error to allow multi-workspace or pre-existing DNS attachments to resolve gracefully.
Sources: [apps/web/app/api/domains/route.ts:164-173](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L164-L173)

### Default Domain Mappings and Workspace Settings

Workspaces can query and update their assigned default platform domains (such as `dub.sh`, `chatg.pt`, `spti.fi`, `git.new`, `cal.link`, `amzn.id`, `ggl.link`, and `fig.page`) using dedicated API routes.
Sources: [apps/web/app/api/domains/default/route.ts:1-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L1-L82)

| Default Domain Key | Domain Name | Database Column |
| :--- | :--- | :--- |
| `dubsh` | `dub.sh` | `dubsh` |
| `chatgpt` | `chatg.pt` | `chatgpt` |
| `sptifi` | `spti.fi` | `sptifi` |
| `gitnew` | `git.new` | `gitnew` |
| `callink` | `cal.link` | `callink` |
| `amznid` | `amzn.id` | `amznid` |
| `ggllink` | `ggl.link` | `ggllink` |
| `figpage` | `fig.page` | `figpage` |

Sources: [apps/web/app/api/domains/default/route.ts:18-25](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L18-L25), [apps/web/app/api/domains/default/route.ts:66-73](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L66-L73)

### Domain Lifecycle API Endpoints

The domain settings surface area exposes several endpoints for listing, updating, and removing domains within a workspace context.

| Endpoint Route | HTTP Method | Required Permission | Description |
| :--- | :--- | :--- | :--- |
| `/api/domains` | `GET` | `domains.read` | Retrieve all domains associated with the workspace with optional search, pagination, and link inclusion. |
| `/api/domains` | `POST` | `domains.write` | Add a new custom domain, enforce plan limits, and register with Vercel. |
| `/api/domains/[domain]` | `GET` | `domains.read` | Retrieve details for a specific workspace domain. |
| `/api/domains/[domain]` | `PATCH` | `domains.write` | Edit configuration, update redirection URLs, or archive a workspace domain. |
| `/api/domains/default` | `GET` | `domains.read` | Fetch enabled default platform short domains for the workspace. |
| `/api/domains/default` | `PATCH` | `domains.write` | Update active default platform short domains configuration. |

Sources: [apps/web/app/api/domains/route.ts:22-94](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L22-L94), [apps/web/app/api/domains/route.ts:96-98](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L96-L98), [apps/web/app/api/domains/domain/route.ts:28-42](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L28-L42), [apps/web/app/api/domains/domain/route.ts:44-46](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/%5Bdomain%5D/route.ts#L44-L46), [apps/web/app/api/domains/default/route.ts:8-48](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L8-L48), [apps/web/app/api/domains/default/route.ts:54-82](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts#L54-L82)

> [!WARNING]
> Claiming a `.dub.link` subdomain enforces strict constraints: workspaces can claim at most one `.dub.link` subdomain, and non-onboarding requests require an active `defaultProgramId` or will throw a 403 forbidden error.
Sources: [apps/web/app/api/domains/route.ts:186-210](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts#L186-L210)

## Related

- [Link Resolution and Redirection](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-resolution-and-redirection)
- [Custom Domains](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/custom-domains)


## Sitemap

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