Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
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, apps/web/lib/middleware/link.ts:43-224, apps/web/lib/middleware/app.ts:26-130, apps/web/lib/middleware/admin.ts:8-38, apps/web/lib/middleware/partners.ts:25-122
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
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.
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, ... };
};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
The dispatcher relies on centralized collections of hostnames and default short-domain redirection rules defined across utility constants.
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
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.
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
Public paths that bypass authentication checks are verified via the isPublicPath helper function, which matches exact paths and path prefixes.
Sources: apps/web/lib/middleware/app.ts:16-24
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
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
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.
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));
}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
During the onboarding sequence, the step page for program configuration (ProgramPage) validates workspace ownership and user session tokens before rendering client components.
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} />;
}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.
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.
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),
);
}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
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.
const AUTHENTICATED_PATHS = [
"/programs",
"/marketplace",
"/onboarding",
"/settings",
"/profile",
"/messages",
"/payouts",
"/account",
"/invite",
"/rewind",
];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.
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,
),
);
}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
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.
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();
}The partners layout wraps client children within NextAuth's SessionProvider alongside a React Suspense boundary to preserve session state across partner routes.
export default function PartnersLayout({ children }: { children: ReactNode }) {
return (
<SessionProvider>
<Suspense>{children}</Suspense>
</SessionProvider>
);
}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
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
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();
}
...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
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, apps/web/lib/middleware/utils/crawl-bitly.ts:20-68
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 });
};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
Sources: apps/web/lib/middleware/link.ts:58-98, apps/web/lib/middleware/link.ts:124-148, apps/web/lib/middleware/utils/crawl-bitly.ts:20-111
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, apps/web/app/app.dub.co/redirects/slug/domains/page.tsx:1-10
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
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
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
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
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
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
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
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
export default async function OldWorkspaceDomains(props: {
params: Promise<{
slug: string;
}>;
}) {
const params = await props.params;
redirect(`/${params.slug}/settings/domains`);
}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, apps/web/app/api/domains/default/route.ts:9-82
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
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
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
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
Sources: apps/web/app/api/domains/default/route.ts:18-25, apps/web/app/api/domains/default/route.ts:66-73
The domain settings surface area exposes several endpoints for listing, updating, and removing domains within a workspace context.
Sources: apps/web/app/api/domains/route.ts:22-94, apps/web/app/api/domains/route.ts:96-98, apps/web/app/api/domains/domain/route.ts:28-42, apps/web/app/api/domains/domain/route.ts:44-46, apps/web/app/api/domains/default/route.ts:8-48, apps/web/app/api/domains/default/route.ts:54-82
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