Skip to main content
doc0blade47/dubdocumentation home
Sign in

Request Routing

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Request routing in Dub is powered by a central edge middleware layer that intercepts incoming HTTP requests across all domains and subdomains, parses hostnames and paths, and delegates execution to specialized middleware handlers. This routing architecture solves multi-tenant separation, short link resolution, partner program routing, and administrative security enforcement at the edge before requests reach application routes. Key design decisions include executing lightweight path parsing and telemetry logging via Axiom prior to domain-specific branching, leveraging Redis caching and edge lookups for fast link redirection, and applying workspace context and authentication checks for platform apps. It interacts closely with edge caching layers, database lookups, and authentication guard utilities to ensure low-latency routing and secure access control across all core services.

Sources: apps/web/middleware.ts:34-88, apps/web/lib/middleware/utils/parse.ts:5-54

Central Dispatch and Request Parsing

Middleware Entry and Request Matching

The root middleware acts as the primary public entry point for incoming HTTP traffic, operating on the Node.js runtime environment defined in apps/web/middleware.ts. Its routing scope is governed by a Next.js matcher configuration that intercepts all requests while explicitly excluding application programming interface endpoints, Next.js internal assets, third-party proxies, and standard metadata files such as favicons, sitemaps, robots declarations, and web manifests.

export const config = {
  runtime: "nodejs",
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api/ routes
     * 2. /_next/ (Next.js internals)
     * 3. /_proxy/ (proxies for third-party services)
     * 4. Metadata files: favicon.ico, sitemap.xml, robots.txt, manifest.webmanifest
     */
    "/((?!api/|_next/|_proxy/|favicon.ico|sitemap.xml|robots.txt|manifest.webmanifest).*)",
  ],
};

Sources: apps/web/middleware.ts:20-32

Upon receiving a request, the middleware immediately invokes telemetry collection through Axiom by transforming the request and dispatching asynchronous log flushes via ev.waitUntil() before evaluating branch logic.

  // Axiom logging
  logger.info(...transformMiddlewareRequest(req));
  ev.waitUntil(logger.flush());

Sources: apps/web/middleware.ts:37-40

Hostname Normalization and Parsing Utility

The parse utility function in apps/web/lib/middleware/utils/parse.ts extracts and normalizes core routing properties from the incoming NextRequest. It retrieves the host header, strips any leading www. prefix, converts the domain string to lowercase, and checks for domain-level mappings defined in DOMAIN_REDIRECTS.

export const parse = (req: NextRequest) => {
  let domain = req.headers.get("host") as string;
  // path is the path of the URL (e.g. dub.sh/stats/github -> /stats/github)
  let path = req.nextUrl.pathname;
 
  // remove www. from domain and convert to lowercase
  domain = domain.replace(/^www./, "").toLowerCase();
 
  if (DOMAIN_REDIRECTS[domain]) {
    domain = DOMAIN_REDIRECTS[domain];
  }

Sources: apps/web/lib/middleware/utils/parse.ts:1-15

Note

End-to-end test request detection inspects local development hostnames (dub.localhost:8888) as well as Vercel preview environments where the x-e2e-redirect-test header is set to true with an application hostname. When matched, case-sensitive tests override the domain to dub-internal-test.com, while general preview requests resolve to the standard SHORT_DOMAIN.

Sources: apps/web/lib/middleware/utils/parse.ts:17-34

Path components and search parameters are subsequently compiled into a structured routing context object. The primary link key and multi-level subpaths (fullKey) are decoded using decodeURIComponent to accommodate internationalized character sets such as Hebrew and Korean.

  // fullPath is the full URL path (along with search params)
  const searchParams = req.nextUrl.searchParams.toString();
  const searchParamsObj = Object.fromEntries(req.nextUrl.searchParams);
  const searchParamsString = searchParams.length > 0 ? `?${searchParams}` : "";
  const fullPath = `${path}${searchParamsString}`;
 
  // Here, we are using decodeURIComponent to handle foreign languages like Hebrew and Korean
  const key = decodeURIComponent(path.split("/")[1]); // key is the first part of the path (e.g. dub.sh/stats/github -> stats)
  const fullKey = decodeURIComponent(path.slice(1)); // fullKey is the full path without the first slash (to account for multi-level subpaths, e.g. d.to/github/repo -> github/repo)
 
  return {
    domain,
    path,
    fullPath,
    key,
    fullKey,
    shortLink: `https://${domain}/${fullKey}`,
    searchParamsObj,
    searchParamsString,
  };
};

Sources: apps/web/lib/middleware/utils/parse.ts:35-54

Dispatch Execution Flow

Once parsing concludes in apps/web/middleware.ts, the request is routed through a prioritized sequence of domain checks and path conditions before delegating to specialized handlers.

Sources: apps/web/middleware.ts:34-88

Application and Workspace Subdomain Routing

App Middleware Request Validation

Requests arriving at application domains pass through AppMiddleware in apps/web/lib/middleware/app.ts. The middleware first checks if the path matches /embed, immediately delegating to EmbedMiddleware. Otherwise, it identifies the authenticated user via getUserViaToken(req). If no user is present and the requested path is not public—such as /marketplace, paths starting with /share/, /deeplink/, /unsubscribe/, or /auth/reset-password/—the request redirects to /login with a ?next= query parameter preserving the original destination.

const isPublicPath = (path: string) =>
  ["/marketplace"].some((p) => path === p) ||
  [
    "/marketplace/",
    "/share/",
    "/deeplink/",
    "/unsubscribe/",
    "/auth/reset-password/",
  ].some((p) => path.startsWith(p));

Sources: apps/web/lib/middleware/app.ts:16-24

For authenticated users accessing non-public paths, visiting /new invokes NewLinkMiddleware. Users within their initial onboarding window—evaluated via ONBOARDING_WINDOW_SECONDS and cached in onboardingStepCache—undergo onboarding flow validation unless they visit /onboarding, /account, or /workspaces, or possess a default workspace or pending workspace invites.

Note

If an onboarding user already possesses a default workspace, the workspace onboarding step is automatically bypassed and advanced to link.

Sources: apps/web/lib/middleware/app.ts:65-91

Workspace Resolution and Path Mapping

When standard users request root or core application paths, WorkspacesMiddleware resolves their active context. It inspects query parameters for a validated next redirect path using isValidInternalRedirect. If the user has a default workspace retrieved via getDefaultWorkspace(user), the middleware rewrites top-level application views and settings routes into workspace-scoped paths.

Target Path PatternRoute ConditionResolved Workspace Destination
Root & Auth["/", "/login", "/register"].includes(path)/[defaultWorkspace][product] (derived via getWorkspaceProduct)
Top-Level SettingsisTopLevelSettingsRedirect(path)/[defaultWorkspace]/settings/[path]
Standard Pages/links, /analytics, /events, /settings, etc./[defaultWorkspace]/[path]

Sources: apps/web/lib/middleware/workspaces.ts:24-46

Warning

Top-level settings redirects are strictly governed by isTopLevelSettingsRedirect, matching /domains, /integrations, /webhooks, and their respective child subpaths.

Sources: apps/web/lib/middleware/utils/is-top-level-settings-redirect.ts:1-8

If no default workspace exists, WorkspacesMiddleware queries prismaEdge.projectInvite for matching user email records. If a pending project invite exists, the user redirects to /[projectSlug]/invite; otherwise, they route to /onboarding/workspace.

  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:48-70

Legacy Path Redirection

The appRedirect utility normalizes legacy URLs, account paths, and restructured partner program routes before rendering. Except for excluded paths like /workspaces, legacy endpoints map to their updated counterparts.

const EXCLUDED_PAGES = ["/workspaces"];
 
const APP_REDIRECTS = {
  "/account": "/account/settings",
  "/referrals": "/account/settings/referrals",
  "/onboarding": "/onboarding/welcome",
  "/welcome": "/onboarding/welcome",
  "/campaigns": "/program/campaigns",
  "/messages": "/program/messages",
  "/fraud": "/program/risks",
  "/risks": "/program/risks",
};

Sources: apps/web/lib/middleware/utils/app-redirect.ts:1-15

Additional regex patterns handle dynamic workspace product paths, upgrading billing routes (/upgrade to /settings/billing/upgrade), tracking guides, and migrating pluralized program routes (/programs/* to /program/*). Unmatched requests that bypass these rules fall through to a rewrite against app.dub.co.

Sources: apps/web/lib/middleware/app.ts:128-130, apps/web/lib/middleware/utils/app-redirect.ts:37-125

Partner Portal and Marketplace Routing

Partner Portal Authentication and Redirection Middleware

The partner portal middleware (PartnersMiddleware) manages request flow for partner domains, enforcing authentication on restricted paths such as /programs, /marketplace, /onboarding, /settings, /profile, /messages, /payouts, /account, /invite, and /rewind. When unauthenticated users request these paths, the middleware intercepts them. If the path begins with /programs/, it extracts the program slug to redirect to /[programSlug]/login; otherwise, it redirects to /login with an encoded ?next= parameter containing the original path.

const AUTHENTICATED_PATHS = [
  "/programs",
  "/marketplace",
  "/onboarding",
  "/settings",
  "/profile",
  "/messages",
  "/payouts",
  "/account",
  "/invite",
  "/rewind",
];

Sources: apps/web/lib/middleware/partners.ts:12-23

For authenticated users, the middleware verifies whether a default partner ID exists via getDefaultPartnerId(user). If no partner ID is found, and the path is not a partner invite, onboarding, or account route, the user redirects to /onboarding. Furthermore, standard redirects map deprecated or legacy partner paths to their current equivalents.

Legacy / Source Path PatternTarget Destination
/settings/profile
/settings/payouts/payouts
/settings/notifications/profile/notifications
/account/settings/notifications/profile/notifications
/profile/sites/profile
/rewind/rewind/2025
/onboarding/online-presence/onboarding/platforms
/onboarding/verify/onboarding/payouts

Sources: apps/web/lib/middleware/utils/partners-redirect.ts:1-10

Warning

When handling ?next= query parameters for authenticated users, the middleware validates destinations using isValidInternalRedirect to prevent open redirect vulnerabilities, explicitly omitting /onboarding paths from automatic immediate redirection to ensure prerequisite setup steps are completed.

Sources: apps/web/lib/middleware/partners.ts:91-102

Program Slug Aliasing and Marketplace Segments

The partners utility routing layer handles structural transformations for partner programs and marketplace categories. Specifically, it maps legacy program identifiers to their current canonical slugs, normalizing references during redirection.

Program AliasCanonical Slug
florafaunaflora
ship-30dwp
supercutaisupercut
teller-perpsteller
hundred-healthhundred
gitroom-shortpostiz
getviktor-comviktor
getviktorviktor
heynaviinavii
galaxyaimagica
voice-osvoiceos
speechify-incspeechifyai
ggms-labs-ltd-aimira
missioncontrolhqsquad-so
nozomiofolk
basecoinbasewallet

Sources: apps/web/lib/middleware/utils/partners-redirect.ts:36-53

Marketplace paths under app/app.dub.co/marketplace/[[...segments]]/page.tsx parse segment parameters to render either internal workspace views via MarketplaceRouter or external views via MarketplaceExternalRouter. The external router processes segments to determine layout rendering based on path depth and prefix matching.

Sources: apps/web/ui/program-marketplace/external/marketplace-external-router.tsx:7-37

Incoming short link requests are intercepted at the edge within LinkMiddleware (detailed further in Link Dispatch), where the request's hostname and path are parsed. The system handles case sensitivity by checking isCaseSensitiveDomain(domain), converting keys to lowercase if the domain is case-insensitive, and performing Punycode encoding via punyEncode. Inspect mode, triggered by a trailing + character on the key, is stripped for lookup and later routes the request to an inspection view if the link lacks password protection. Requests with unsupported keys, such as .php paths prone to high bot traffic, receive an immediate 302 redirect to the root domain accompanied by X-Robots-Tag: googlebot: noindex and a dub-no-track parameter.

if (isUnsupportedKey(key)) {
  return NextResponse.redirect(new URL("/?dub-no-track=1", req.url), {
    headers: {
      ...DUB_HEADERS,
      "X-Robots-Tag": "googlebot: noindex",
    },
    status: 302,
  });
}

Sources: apps/web/lib/middleware/link.ts:77-87

When a link key evaluates to an empty string, it is normalized to _root to service root domain links. The middleware first queries Redis via linkCache.get({ domain, key }) (documented in Caching & Rate Limiting). If the cache lookup misses, it falls back to PlanetScale using getLinkViaEdge({ domain, key }) (see Database Architecture). If the link is still missing and the domain is specifically buff.ly, the router delegates control to the Bitly migration handler. Otherwise, it issues a rewrite to /[domain]/notfound with static page caching headers configured for a s-maxage of 86400 seconds.

if (!cachedLink) {
  let linkData = await getLinkViaEdge({
    domain,
    key,
  });
 
  if (!linkData) {
    if (domain === "buff.ly") {
      return await crawlBitly(req, ev);
    }
 
    return NextResponse.rewrite(new URL(`/${domain}/notfound`, req.url), {
      headers: {
        ...DUB_HEADERS,
        ...STATIC_PAGES_CACHE_HEADERS,
      },
    });
  }

Sources: apps/web/lib/middleware/link.ts:100-117

Note

During Redis failover conditions, linkCache.get flags redisFailOver as true. The middleware automatically bypasses click tracking and click ID cookie lookups to protect downstream response latencies from timing out.

Sources: apps/web/lib/middleware/link.ts:89-99, apps/web/lib/middleware/link.ts:193-211

Bitly Migration Hooks and Crawler Delegation

When short link resolution encounters an unknown key on the buff.ly domain, crawlBitly executes a programmatic crawl against the Bitly API to fetch the backing long URL on-demand. Before querying Bitly, the handler validates that the key does not contain restricted Bitly characters matching the regular expression invalidBitlyKeyRegex.

// bitly doesn't support the following characters: ` ~ , . < > ; ‘ : " / \ [ ] ^ { } ( ) = + ! * @ & $ £ ? % # |
// @see: https://support.bitly.com/hc/en-us/articles/360030780892-What-characters-are-supported-when-customizing-links
const invalidBitlyKeyRegex = /[`~,.<>;':"/\\[\]^{}()=+!*@&$£?%#|]/;
 
if (key && !invalidBitlyKeyRegex.test(key)) {
  const link = await fetchBitlyLink({ domain, key });

Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:23-28

If a valid Bitly link object is returned, the long URL is sanitized using getUrlFromStringIfValid. The middleware then asynchronously provisions a new link record inside Prisma tied to a designated Buffer workspace, user, and folder, while concurrently recording the link and publishing a usage event via Upstash Redis streams. Finally, it redirects the client to the resolved long URL with a 302 status. If the Bitly API encounters a rate limit or returns no record, the utility defaults to a 302 redirection toward https://buffer.com with static CDN caching headers.

Constant NameIdentifier / ValuePurpose
BUFFER_WORKSPACE_IDcm05wnnpo000711ztj05wwdbuTarget workspace assignment for crawled Bitly links
BUFFER_USER_IDcm05wnd49000411ztg2xbup0iCreator user association for on-demand links
BUFFER_FOLDER_IDfold_1JNQBVZV8P0NA0YGB11W2HHSQDefault folder placement for migrated links
BUFFER_BITLY_API_KEYprocess.env.BUFFER_BITLY_API_KEYBearer token credential for Bitly v4 API requests

Sources: apps/web/lib/middleware/utils/crawl-bitly.ts:15-18, apps/web/lib/middleware/utils/crawl-bitly.ts:45-50

Well-Known Path Routing

Domain configuration files required by native applications and deep-linking services are resolved dynamically via the well-known route handler at app/wellknown/[domain]/[file]/route.ts. The route accepts requests parameterized by a domain slug and a file name, validating the requested file against a whitelist of supported files.

if (!supportedWellKnownFiles.includes(file)) {
  return NextResponse.json({ error: "File not supported" }, { status: 400 });
}

Sources: apps/web/app/wellknown/domain/file/route.ts:16-18

The handler queries PlanetScale for the domain's corresponding appleAppSiteAssociation and assetLinks fields. Depending on the requested file identifier, it constructs a JSON payload containing either Apple app site association data with default empty app links structure or Android asset links configuration, returning the response with public Vercel CDN caching headers.

let response: WellKnownConfig[SupportedWellKnownFiles];
switch (file) {
  case "apple-app-site-association":
    response =
      (appleAppSiteAssociation as WellKnownConfig["apple-app-site-association"]) || {
        applinks: {
          apps: [],
          details: [],
        },
      };
    break;
  case "assetlinks.json":
    response = (assetLinks as WellKnownConfig["assetlinks.json"]) || [];
    break;
}

Sources: apps/web/app/wellknown/domain/file/route.ts:34-48

Administrative Path Access Control

Middleware-Level Gatekeeping and Session Verification

The AdminMiddleware function guards internal moderation and tools in Admin Dashboard by intercepting incoming requests, parsing path parameters, and validating session tokens via getUserViaToken (integrated with Authentication). If no authenticated user session exists and the target path differs from /login, the request receives a 302 redirect toward /login. The file apps/web/app/ee/api/admin/impersonate/route.ts provides user and workspace lookup capabilities for the admin portal, documented in Admin Dashboard.

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));

Sources: apps/web/lib/middleware/admin.ts:8-14

When an authenticated user session is present, the middleware queries prismaEdge.projectUsers for a matching record where the user ID corresponds to the session and the project ID equals DUB_WORKSPACE_ID. If the user is absent from the designated administrative workspace, the handler passes the request through via NextResponse.next(), which surfaces a 404 error page.

} 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

Sources: apps/web/lib/middleware/admin.ts:15-26

Caution

Returning NextResponse.next() for non-workspace users intentionally triggers a 404 Not Found response rather than a 403 Forbidden error, preventing information disclosure regarding the existence of administrative endpoints.

Sources: apps/web/lib/middleware/admin.ts:25-26

Route-Level Authorization Guards

For workspace-verified administrators, further route-level validation applies. If the requested path is /login or fails the canAccessAdminPath guard check, the middleware redirects the client to the root URL /.

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

Sources: apps/web/lib/middleware/admin.ts:27-33

If all authorization checks pass, the middleware routes the request internally by rewriting the destination URL to the internal administrative subdomain admin.dub.co, preserving or normalizing the trailing path segments.

return NextResponse.rewrite(
  new URL(`/admin.dub.co${path === "/" ? "" : path}`, req.url),
);

Sources: apps/web/lib/middleware/admin.ts:35-37

Embedded Components and Quick-Action Handlers

Requests targeting embedded widgets, quick-action link creators, and referral portals are processed through distinct middleware and API routes. The EmbedMiddleware function parses incoming requests and inspects their path or search parameters. If a request begins with /embed/support-chat or includes a token search parameter, it rewrites the destination to the primary application domain at /app.dub.co. Requests lacking these valid parameters are redirected to the root URL /.

export function EmbedMiddleware(req: NextRequest) {
  const { path, searchParamsObj, fullPath } = parse(req);
 
  if (path.startsWith("/embed/support-chat")) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }
 
  if (searchParamsObj.token) {
    return NextResponse.rewrite(new URL(`/app.dub.co${fullPath}`, req.url));
  }
 
  // TODO: Show token expiry page
  return NextResponse.redirect(new URL("/", req.url));

Sources: apps/web/lib/middleware/embed.ts:4-16

Shortcuts that instantiate link generation from external contexts rely on the CreateLinkMiddleware. This utility extracts the domain and full path from the request, decodes the target URL string, and redirects the client to the /new endpoint on the application domain with link and domain query parameters attached.

export function CreateLinkMiddleware(req: NextRequest) {
  const { domain, fullPath } = parse(req);
 
  const url = getUrlFromString(fullPath.slice(1));
 
  const redirectURL = new URL(`${APP_DOMAIN}/new`);
  redirectURL.searchParams.append("link", url);
  redirectURL.searchParams.append("domain", domain);
 
  return NextResponse.redirect(redirectURL.toString());

Sources: apps/web/lib/middleware/create-link.ts:5-14

Once a user interacts with the link creation shortcut, NewLinkMiddleware resolves the user's default workspace using getDefaultWorkspace. If a workspace exists, it redirects the client to the workspace's link management page with query arguments passing the link parameters forward; otherwise, it directs unassigned users to the workspace creation flow.

export async function NewLinkMiddleware(req: NextRequest, user: UserProps) {
  const { fullPath } = parse(req);
 
  const defaultWorkspace = await getDefaultWorkspace(user);
 
  const searchParams = new URL(fullPath, APP_DOMAIN).searchParams;
 
  if (defaultWorkspace) {
    return NextResponse.redirect(
      new URL(
        `/${defaultWorkspace}/links?newLink=${searchParams.get("link") || true}${searchParams.has("domain") ? `&newLinkDomain=${searchParams.get("domain")}` : ""}`,
        req.url,
      ),
    );
  } else {
    return NextResponse.redirect(
      new URL(`/workspaces?newWorkspace=true`, req.url),
    );
  }

Sources: apps/web/lib/middleware/new-link.ts:7-25

Note

NewLinkMiddleware validates the presence of an active user workspace before constructing redirect URLs, ensuring users without a designated default workspace are forced through the /workspaces?newWorkspace=true onboarding path.

Sources: apps/web/lib/middleware/new-link.ts:14-25

Referral Widget API Routes

Referral program embeds interact with authenticated backend handlers to retrieve and provision tracking links. The endpoint at /api/embed/referrals/links uses withReferralsEmbedToken to govern both read and write operations.

HTTP MethodRoute HandlerAuthentication / GuardAction & Validation
GET/api/embed/referrals/linkswithReferralsEmbedTokenParses and returns existing partner links via ReferralsEmbedLinkSchema.
POST/api/embed/referrals/linkswithReferralsEmbedTokenVerifies active enrollment status, checks link count limits against group.maxPartnerLinks, and processes new links.
GET/api/embed/referrals/tokenwithReferralsEmbedTokenReturns the validated active embed token object.
POST/api/tokens/embed/referralswithWorkspace (Required: links.write, advanced/enterprise)Creates or retrieves program enrollment and provisions a new referrals embed token.

Sources: apps/web/app/ee/api/embed/referrals/links/route.ts:18-158, apps/web/app/ee/api/embed/referrals/token/route.ts:4-7, apps/web/app/api/tokens/embed/referrals/route.ts:15-121

When creating partner links via the embed referral POST handler, the system ensures the partner's enrollment status is included in ACTIVE_ENROLLMENT_STATUSES and that the requested URL satisfies group constraints via validatePartnerLinkUrl.

if (!ACTIVE_ENROLLMENT_STATUSES.includes(programEnrollment.status)) {
  throw new DubApiError({
    code: "forbidden",
    message:
      "You cannot create links in this program because your enrollment is not active.",
  });
}
 
const { url, key } = createPartnerLinkSchema
  .pick({ url: true, key: true })
  .parse(await parseRequestBody(req));
 
if (!program.domain || !program.url) {
  throw new DubApiError({
    code: "bad_request",
    message:
      "This program needs a domain and URL set before creating a link.",
  });
}
 
if (links.length >= group.maxPartnerLinks) {
  throw new DubApiError({
    code: "bad_request",
    message: `You have reached the limit of ${group.maxPartnerLinks} program links.`,
  });
}
 
validatePartnerLinkUrl({ group, url });
 

Sources: apps/web/app/ee/api/embed/referrals/links/route.ts:28-56

Was this page helpful?