Getting Started
Analytics & Tracking
Background Processing
Administration & API
How It Works
Last updated Oct 12, 2026
<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/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts)
- [apps/web/app/ee/api/track/open/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/open/route.ts)
- [apps/web/app/ee/api/track/click/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/click/route.ts)
- [apps/web/middleware.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/middleware.ts)
- [apps/web/app/ee/api/track/visit/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/track/visit/route.ts)
- [apps/web/app/api/analytics/dashboard/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/analytics/dashboard/route.ts)
- [apps/web/app/domain/expired/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/expired/page.tsx)
- [apps/web/lib/middleware/utils/crawl-bitly.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts)
- [apps/web/app/domain/key/proxy/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx)
- [apps/web/app/domain/notfound/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/notfound/page.tsx)
- [apps/web/app/api/links/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/route.ts)
- [apps/web/app/ee/api/cron/import/bitly/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/(ee)/api/cron/import/bitly/route.ts)
- [apps/web/app/cloaked/url/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/cloaked/%5Burl%5D/page.tsx)
- [apps/web/lib/middleware/app.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/app.ts)
- [apps/web/lib/api/links/process-link.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/api/links/process-link.ts)
- [apps/web/lib/middleware/utils/get-final-url.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/get-final-url.ts)
- [apps/web/app/domain/banned/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/banned/page.tsx)
- [apps/web/app/domain/key/inspect/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx)
- [apps/web/app/api/callback/bitly/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/callback/bitly/route.ts)
- [apps/web/app/password/linkId/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx)
- [apps/web/app/api/links/iframeable/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/iframeable/route.ts)
- [apps/web/app/api/providers/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/providers/route.ts)
- [apps/web/app/api/links/metatags/route.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/metatags/route.ts)
- [apps/web/app/app.dub.co/share/share/dashboardId/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(share)/share/%5BdashboardId%5D/page.tsx)
- [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
- [apps/web/lib/middleware/create-link.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/create-link.ts)
- [apps/web/lib/middleware/partners.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/partners.ts)
- [apps/web/lib/integrations/slack/commands.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/integrations/slack/commands.ts)
- [apps/web/lib/middleware/embed.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/embed.ts)
- [apps/web/lib/middleware/new-link.ts](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/new-link.ts)
</details>
## Overview
Link dispatch powers the core request processing engine responsible for handling incoming short links across custom domains and root namespaces at the edge. By combining low-latency caching with edge middleware, Dub evaluates request criteria, resolves target destinations, and handles specialized routing rules such as conditional targeting, link cloaking, password challenges, and automated fallback behavior.
Sources: [apps/web/lib/middleware/link.ts:43-123](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L43-L123), [apps/web/middleware.ts:34-88](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/middleware.ts#L34-L88)
## Edge Link Resolution Pipeline
### Domain and Key Normalization
Incoming requests to short links and custom domains first enter the global edge middleware pipeline defined in `apps/web/middleware.ts`. The router parses the request to extract the target domain, path, and slug key, initializes Axiom logging via `transformMiddlewareRequest`, and dispatches requests across specialized handlers. Root management consoles, API endpoints, partner portals, and well-known files are routed accordingly, while standard link lookups fall through to `LinkMiddleware`.
\`\`\`typescript
export default async function middleware(req: NextRequest, ev: NextFetchEvent) {
const { domain, path, key, fullKey } = parse(req);
// Axiom logging
logger.info(...transformMiddlewareRequest(req));
ev.waitUntil(logger.flush());
// for app.dub.co
if (isAppHostname(domain)) {
return AppMiddleware(req);
}
// for api.dub.co
if (API_HOSTNAMES.has(domain)) {
return ApiMiddleware(req);
}
// for public stats pages (e.g. d.to/stats/try -> rewrite to [/domain]/[key]/stats)
if (path.startsWith("/stats/")) {
return NextResponse.rewrite(
new URL(
`/${domain}/${encodeURIComponent(path.replace("/stats/", ""))}/stats`,
req.url,
),
);
}
// for .well-known routes
if (path.startsWith("/.well-known/")) {
const file = path.split("/.well-known/").pop();
if (file && supportedWellKnownFiles.includes(file)) {
return NextResponse.rewrite(
new URL(`/wellknown/${domain}/${file}`, req.url),
);
}
}
// default redirects for dub.sh
if (domain === "dub.sh" && DEFAULT_REDIRECTS[key]) {
return NextResponse.redirect(DEFAULT_REDIRECTS[key]);
}
if (ADMIN_HOSTNAMES.has(domain)) {
return AdminMiddleware(req);
}
if (PARTNERS_HOSTNAMES.has(domain)) {
return PartnersMiddleware(req);
}
if (isValidUrl(fullKey)) {
return CreateLinkMiddleware(req);
}
return LinkMiddleware(req, ev);
\`\`\`
Sources: [apps/web/middleware.ts:34-88](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/middleware.ts#L34-L88)
When a request reaches `LinkMiddleware`, the domain and original key are extracted using `parse(req)`. If no domain is present, the middleware immediately yields control via `NextResponse.next()`. Otherwise, the key undergoes ASCII punycode encoding. Case sensitivity is evaluated against `isCaseSensitiveDomain(domain)`; if the domain is not case-sensitive, the key is converted to lowercase.
\`\`\`typescript
export async function LinkMiddleware(req: NextRequest, ev: NextFetchEvent) {
let { domain, fullKey: originalKey, fullPath, searchParamsObj } = parse(req);
if (!domain) {
return NextResponse.next();
}
// encode the key to ascii
// links on Dub are case insensitive by default
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/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L43-L56)
Inspect mode is triggered if the key ends with a `+` character. When active, the trailing `+` is stripped to query the underlying resource. If the resulting key is an empty string, it is reassigned to `_root` to resolve root domain link configurations.
> [!WARNING]
> Requests containing unsupported keys (such as `.php` extensions associated with excessive automated bot traffic) bypass normal resolution entirely. They are redirected to the root domain with a `?dub-no-track=1` query parameter and tagged with `X-Robots-Tag: googlebot: noindex` to prevent crawler indexing.
\`\`\`typescript
const inspectMode = key.endsWith("+");
// if inspect mode is enabled, remove the trailing `+` from the key
if (inspectMode) {
key = key.slice(0, -1);
}
// if key is empty string, set to _root (root domain link)
if (key === "") {
key = "_root";
}
const STATIC_PAGES_CACHE_HEADERS = {
"Vercel-CDN-Cache-Control": "public, s-maxage=86400",
"Vercel-Cache-Tag": linkCache._createStaticPagesCacheKeys({
domain,
key: originalKey || "_root",
}),
};
// we don't support .php links (too much bot traffic)
// hence we redirect to the root domain and add `dub-no-track` header to avoid tracking bot traffic
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:58-87](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L43-L56)
### Cache Lookups and Edge Fallback
The pipeline queries `linkCache.get({ domain, key })` to retrieve cached link metadata from Redis. If Redis encounters a failure, `redisFailOver` is set, and click tracking jobs are safely skipped to prevent edge execution timeouts.
\`\`\`typescript
let { cachedLink, redisFailOver } = await linkCache.get({ domain, key });
let isPartnerLink = Boolean(cachedLink?.programId && cachedLink?.partnerId);
// skip click tracking during Redis failover to avoid timeouts
const recordClick = redisFailOver
? async () => {
console.log("Redis failover detected, skipping click tracking...");
return;
}
: recordClickJob;
\`\`\`
Sources: [apps/web/lib/middleware/link.ts:89-99](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L89-L99)
If the link cache misses, the middleware invokes `getLinkViaEdge({ domain, key })` to fetch record data from PlanetScale. If the record remains unfound and the domain equals `buff.ly`, the request is handed off to the On-Demand Bitly Migration Crawling handler. For all other unknown domains and keys, the middleware responds with a not-found rewrite to `/${domain}/notfound` accompanied by static Vercel CDN cache control headers (`public, s-maxage=86400`) and cache tags generated via `linkCache._createStaticPagesCacheKeys`.
\`\`\`typescript
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](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L100-L117)
When edge lookup succeeds, partner status is evaluated from `programId` and `partnerId`, and the raw payload is formatted via `formatRedisLink`. Non-partner links are cached synchronously in the background using `ev.waitUntil()`. Partner links fetch additional enrollment and discount details via `getPartnerEnrollmentInfo` before backfilling the cache.
\`\`\`typescript
isPartnerLink = Boolean(linkData.programId && linkData.partnerId);
// format link to fit the RedisLinkProps interface
cachedLink = formatRedisLink(linkData as any);
ev.waitUntil(
(async () => {
if (!isPartnerLink) {
await linkCache.set(linkData as any, { skipRevalidateTag: true });
return;
}
const { partner, discount } = await getPartnerEnrollmentInfo({
programId: linkData.programId,
partnerId: linkData.partnerId,
linkId: linkData.id,
});
// we'll use this data on /track/click
await linkCache.set(
{
...(linkData as any),
...(partner && { partner }),
...(discount && { discount }),
},
{ skipRevalidateTag: true },
);
})(),
);
}
\`\`\`
Sources: [apps/web/lib/middleware/link.ts:118-148](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L118-L148)
## Destination Routing and Targeting Rules
### A/B Testing Variants and URL Resolution
When a link request successfully resolves, `LinkMiddleware` extracts targeting and routing properties from the cached link record (`cachedLink`), including `geo` (geo-targeting rules), `ios`, `android`, `testVariants`, and `testCompletedAt`. Experiment variant selection is evaluated by passing `testVariants` and `testCompletedAt` to `resolveABTestURL`. If an active A/B test variant is selected, `testUrl` overrides the default destination; otherwise, resolution falls back to `cachedLink.url`.
\`\`\`typescript
const {
id: linkId,
password,
trackConversion,
geo: geoTargeting,
proxy,
rewrite,
expiresAt,
disabledAt,
ios,
android,
expiredUrl,
doIndex,
testVariants,
testCompletedAt,
projectId: workspaceId,
} = cachedLink;
const testUrl = await resolveABTestURL({
testVariants,
testCompletedAt,
});
const url = testUrl || cachedLink.url;
\`\`\`
Sources: [apps/web/lib/middleware/link.ts:150-174](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/link.ts#L150-L174)
### Final URL Construction and Query Parameter Handling
The final destination URL is assembled using `getFinalUrl`, which injects tracking parameters, attribution identifiers, and handles platform-specific redirect parameters. If a `clickId` is present and `dub_client_reference_id` is set to `"1"` (custom query parameter configuration for Stripe payment links), `client_reference_id` is assigned `dub_id_${clickId}` and `dub_client_reference_id` is deleted. If `dub_client_reference_id` is not active and tracking is enabled, `dub_id` is appended to the query string.
\`\`\`typescript
export const getFinalUrl = (
url: string,
{
req,
clickId,
via,
}: {
req: NextRequest;
clickId?: string;
via?: string;
},
) => {
// query is the query string (e.g. d.to/github?utm_source=twitter -> ?utm_source=twitter)
const searchParams = req.nextUrl.searchParams;
const urlObj = new URL(url);
if (via) {
urlObj.searchParams.set("via", via);
}
if (clickId) {
/*
custom query param for stripe payment links:
- if there is a clickId and dub_client_reference_id is 1
- then set client_reference_id to dub_id_${clickId} and drop the dub_client_reference_id param
- our Stripe integration will then detect `dub_id_${clickId}` as the dubClickId in the `checkout.session.completed` webhook
- @see: https://github.com/dubinc/dub/blob/main/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
*/
if (urlObj.searchParams.get("dub_client_reference_id") === "1") {
urlObj.searchParams.set("client_reference_id", `dub_id_${clickId}`);
urlObj.searchParams.delete("dub_client_reference_id");
// if there's a clickId and no dub-no-track search param, then add clickId to the final url
// reasoning: if you're skipping tracking, there's no point in passing the clickId anyway
} else if (!searchParams.has("dub-no-track")) {
urlObj.searchParams.set("dub_id", clickId);
}
}
\`\`\`
Sources: [apps/web/lib/middleware/utils/get-final-url.ts:12-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/get-final-url.ts#L12-L50)
For mobile attribution and partner networks, `getFinalUrl` injects network-specific parameters. AppsFlyer tracking URLs receive hardcoded parameter `pid=dubinc_int`, along with `clickid`, `c`, and `af_siteid` when available. Singular tracking links populate `cl`, `ua`, and `ip`, and polyfill `wpcn` and `wpcl` parameters if set to `{via}` or `{dub_id}` placeholders. Google Play Store URLs decode existing `referrer` parameters and append `deepLink` values containing the short link and search parameters. Finally, incoming search parameters from the request are merged into the target URL, stripping internal control parameters such as `dub-no-track`, `redir_url`, `qr`, and `skip_deeplink_preview`.
\`\`\`typescript
// for AppsFlyer tracking links
if (isAppsFlyerTrackingUrl(url)) {
// set hardcoded query params
urlObj.searchParams.set("pid", "dubinc_int");
if (clickId) {
urlObj.searchParams.set("clickid", clickId);
}
// set dynamic params (if not exist)
if (!urlObj.searchParams.has("c") && via) {
urlObj.searchParams.set("c", via);
}
if (!urlObj.searchParams.has("af_siteid") && via) {
urlObj.searchParams.set("af_siteid", via);
}
}
// for Singular tracking links
if (isSingularTrackingUrl(url)) {
const ua = userAgent(req);
const ip = process.env.VERCEL === "1" ? ipAddress(req) : LOCALHOST_IP;
urlObj.searchParams.set("cl", clickId ?? "");
urlObj.searchParams.set("ua", ua?.ua ?? "");
urlObj.searchParams.set("ip", ip ?? "");
// Polyfill wpcn & wpcl params for Singular integration
const wpcn = urlObj.searchParams.get("wpcn");
const wpcl = urlObj.searchParams.get("wpcl");
if (wpcn && wpcn === "{via}") {
urlObj.searchParams.set("wpcn", via ?? "");
}
if (wpcl && wpcl === "{dub_id}") {
urlObj.searchParams.set("wpcl", clickId ?? "");
}
}
// for Google Play Store links
if (isGooglePlayStoreUrl(url)) {
const { shortLink, searchParamsString } = parse(req);
const existingReferrer = urlObj.searchParams.get("referrer");
const referrerSearchParam = new URLSearchParams(
existingReferrer ? decodeURIComponent(existingReferrer) : "",
);
referrerSearchParam.set("deepLink", `${shortLink}${searchParamsString}`);
urlObj.searchParams.set("referrer", referrerSearchParam.toString());
}
// if there are no query params, then return the target url as is (no need to parse it)
// @ts-ignore – until https://github.com/microsoft/TypeScript/issues/54466 is fixed
if (searchParams.size === 0) return urlObj.toString();
// if searchParams (type: `URLSearchParams`) has the same key as target url, then overwrite it
for (const [key, value] of searchParams) {
// we will pass everything except internal query params (dub-no-track and redir_url)
if (["dub-no-track", REDIRECTION_QUERY_PARAM].includes(key)) continue;
urlObj.searchParams.set(key, value);
}
// remove qr param from the final url if the value is "1" (only used for detectQr function)
if (urlObj.searchParams.get("qr") === "1") {
urlObj.searchParams.delete("qr");
}
// remove skip_deeplink_preview param from the final url (only used for internal redirection behavior)
if (urlObj.searchParams.get("skip_deeplink_preview") === "1") {
urlObj.searchParams.delete("skip_deeplink_preview");
}
return urlObj.toString();
\`\`\`
Sources: [apps/web/lib/middleware/utils/get-final-url.ts:52-124](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/get-final-url.ts#L52-L124)
### App Deep Link Previews and Platform Handlers
For mobile deep links, requests to short links on domains with configured deep link rules evaluate client device properties using `userAgent`. The platform state is categorized as either `ios` or `android` based on the operating system name.
\`\`\`tsx
// Detect language from Accept-Language header
const headersList = await headers();
const acceptLanguage = headersList.get("accept-language");
const language = getLanguage(acceptLanguage);
const t = getTranslations(language);
const ua = userAgent({ headers: headersList });
const platform: "ios" | "android" =
ua.os?.name === "Android" ? "android" : "ios";
\`\`\`
Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:52-61](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
The deep link resolution handler queries the database for domain and key assets, including short domain associations such as `appleAppSiteAssociation`, `assetLinks`, and `deepviewData`. If the target domain lacks deep link asset configuration for the user's platform, the preview is bypassed, redirecting immediately to the platform-specific URL (`link.ios` or `link.android`) or falling back to the canonical URL (`link.url`).
\`\`\`tsx
let link = await prisma.link.findUnique({
where: {
domain_key: {
domain,
key: encodedKey,
},
},
select: {
domain: true,
key: true,
shortLink: true,
url: true,
ios: true,
android: true,
shortDomain: {
select: {
appleAppSiteAssociation: true,
assetLinks: true,
deepviewData: true,
},
},
},
});
// if the link doesn't exist, we redirect to the root domain link
if (!link) {
redirect(`https://${domain}`);
}
const { appleAppSiteAssociation, assetLinks, deepviewData } =
link.shortDomain;
// if the domain isn't set up for deep linking on the user's platform, skip
// the preview and forward to the platform-specific URL (or the canonical URL)
if (platform === "android") {
if (!assetLinks || !deepviewData) {
redirect(link.android ?? link.url);
}
} else {
if (!appleAppSiteAssociation || !deepviewData) {
redirect(link.ios ?? link.url);
}
}
\`\`\`
Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:68-110](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
> [!NOTE]
> On Android platforms, `androidPackageName` is extracted by validating asset links where the target namespace equals `android_app` and the package name satisfies the strict regex pattern `/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/`.
\`\`\`tsx
let androidPackageName: string | null = null;
if (platform === "android" && Array.isArray(link.shortDomain.assetLinks)) {
for (const assetLink of link.shortDomain.assetLinks as Array<AssetLink>) {
const packageName = assetLink?.target?.package_name;
if (
assetLink?.target?.namespace === "android_app" &&
typeof packageName === "string" &&
ANDROID_PACKAGE_NAME_REGEX.test(packageName)
) {
androidPackageName = packageName;
break;
}
}
}
\`\`\`
Sources: [apps/web/app/app.dub.co/deeplink/deeplink/domain/...key/page.tsx:122-137](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/app.dub.co/(deeplink)/deeplink/%5Bdomain%5D/%5B%5B...key%5D%5D/page.tsx)
## On-Demand Bitly Migration Crawling
### Dynamic Crawling and Cache Backfilling
When a link lookup via edge storage (`getLinkViaEdge`) yields no matching record for the `buff.ly` domain, the link dispatch middleware delegates handling to the `crawlBitly` utility. This service intercepts legacy references to fetch long-form destination URLs dynamically from the Bitly v4 API (`https://api-ssl.bitly.com/v4/bitlinks/{domain}/{key}`) using `BUFFER_BITLY_API_KEY`.
\`\`\`typescript
export const crawlBitly = async (req: NextRequest, ev: NextFetchEvent) => {
const { domain, fullKey: key } = parse(req);
// 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 });
if (link) {
\`\`\`
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:20-29](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts#L20-L29)
> [!CAUTION]
> Keys containing invalid Bitly characters matching the regex `/[`~,.<>;':"/\\[\]^{}()=+!*@&$£?%#|]/` are rejected outright without querying the upstream API, immediately redirecting traffic to the default fallback landing page at `https://buffer.com`.
\`\`\`typescript
// 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)) {
…
return NextResponse.redirect("https://buffer.com", {
\`\`\`
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:23-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts#L23-L27), [apps/web/lib/middleware/utils/crawl-bitly.ts:71-71](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts#L71-L71)
### On-Demand Import and Workspace Assignment
Upon successfully retrieving link properties from Bitly, the long URL is sanitized via `getUrlFromStringIfValid`. If valid, Prisma persists the migrated record asynchronously using `ev.waitUntil` under designated system workspace and folder constants. Concurrently, click telemetry registers in Tinybird and workspace usage metrics publish to Upstash Redis streams.
\`\`\`typescript
const sanitizedUrl = getUrlFromStringIfValid(link.long_url);
if (sanitizedUrl) {
console.log(
`[Bitly] Creating link on-demand: ${domain}/${key} (createdAt: ${link.created_at})`,
);
const encodedKey = encodeKeyIfCaseSensitive({ domain, key });
ev.waitUntil(
prisma.link
.create({
data: {
id: createId({ prefix: "link_" }),
domain,
key: encodedKey,
url: sanitizedUrl,
shortLink: linkConstructorSimple({ domain, key: encodedKey }),
projectId: BUFFER_WORKSPACE_ID,
userId: BUFFER_USER_ID,
folderId: BUFFER_FOLDER_ID,
createdAt: new Date(link.created_at),
},
})
.then((data) =>
Promise.allSettled([
recordLink(data),
publishWorkspaceLinksUsageEvent({
workspaceId: BUFFER_WORKSPACE_ID,
linksCount: 1,
timestamp: new Date().toISOString(),
}),
]),
),
);
\`\`\`
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:30-61](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts#L30-L61)
| Constant Name | Source Value | Purpose / Entity Assignment |
| :--- | :--- | :--- |
| `BUFFER_WORKSPACE_ID` | `cm05wnnpo000711ztj05wwdbu` | Target workspace identifier for imported Bitly links |
| `BUFFER_USER_ID` | `cm05wnd49000411ztg2xbup0i` | Creator user identifier associated with migrated records |
| `BUFFER_FOLDER_ID` | `fold_1JNQBVZV8P0NA0YGB11W2HHSQ` | Default folder assignment for buffer-migrated assets |
\`\`\`typescript
const BUFFER_WORKSPACE_ID = "cm05wnnpo000711ztj05wwdbu";
const BUFFER_USER_ID = "cm05wnd49000411ztg2xbup0i";
const BUFFER_FOLDER_ID = "fold_1JNQBVZV8P0NA0YGB11W2HHSQ";
\`\`\`
Sources: [apps/web/lib/middleware/utils/crawl-bitly.ts:15-17](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/lib/middleware/utils/crawl-bitly.ts#L15-L17)
### OAuth Callback and Token Exchange
Bulk migrations and user-authenticated synchronizations are managed via the Bitly OAuth callback route at `/api/callback/bitly`. This endpoint extracts user sessions, executes `bitlyOAuthProvider.exchangeCodeForToken`, and parses the resulting token payload.
\`\`\`typescript
export async function GET(req: Request) {
try {
const session = await getSession();
if (!session?.user.id) {
throw new DubApiError({
code: "unauthorized",
message: "Unauthorized.",
});
}
const {
token: response,
contextId: { workspaceId, folderId },
} = await bitlyOAuthProvider.exchangeCodeForToken<{
workspaceId: string;
folderId?: string;
}>(req);
\`\`\`
Sources: [apps/web/app/api/callback/bitly/route.ts:10-27](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/callback/bitly/route.ts#L10-L27)
> [!NOTE]
> If the OAuth response contains errors or fails verification, the request redirects unconditionally to the root application domain (`APP_DOMAIN`).
\`\`\`typescript
if (!response || response.includes("error")) {
return NextResponse.redirect(APP_DOMAIN);
}
\`\`\`
Sources: [apps/web/app/api/callback/bitly/route.ts:29-31](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/callback/bitly/route.ts#L29-L31)
Successfully validated access tokens are stored in Redis under `import:bitly:{workspaceId}`, and workspace slug queries determine the final dashboard redirect URL accompanied by query parameters specifying `import=bitly` and the active `folderId`.
\`\`\`typescript
const params = new URLSearchParams(response);
const [workspace, _] = await Promise.all([
// get workspace slug from workspaceId
prisma.project.findUnique({
where: {
id: workspaceId,
},
select: {
slug: true,
},
}),
// store access token in redis
redis.set(`import:bitly:${workspaceId}`, params.get("access_token")),
]);
const queryParams = new URLSearchParams({
import: "bitly",
...(folderId ? { folderId } : {}),
});
const redirectUrl = workspace
? `${APP_DOMAIN}/${workspace.slug}?${queryParams.toString()}`
: APP_DOMAIN;
return NextResponse.redirect(redirectUrl);
\`\`\`
Sources: [apps/web/app/api/callback/bitly/route.ts:33-59](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/callback/bitly/route.ts#L33-L59)
## Cloaking and Proxy Destination Rendering
### Cloaked URL Masking and Iframe Encapsulation
The route handler located at `apps/web/app/cloaked/[url]/page.tsx` renders cloaked destination URLs inside an HTML `iframe`. When requests hit the page, `getCloakedDestinationUrl` evaluates whether the incoming parameter needs secondary decoding, checking if the string matches the regex pattern `/^https?%3A/i` and safely applying `decodeURIComponent` if required.
\`\`\`tsx
function getCloakedDestinationUrl(param: string): string {
// Next.js decodes dynamic route segments once. Only decode again when the
// param is still fully encoded.
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/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/cloaked/%5Burl%5D/page.tsx#L8-L20)
\`\`\`tsx
export default async function CloakedPage(props: {
params: Promise<{ url: string }>;
}) {
const params = await props.params;
const url = getCloakedDestinationUrl(params.url);
return <iframe src={url} className="min-h-screen w-full border-none" />;
\`\`\`
Sources: [apps/web/app/cloaked/url/page.tsx:40-46](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/cloaked/%5Burl%5D/page.tsx#L40-L46)
> [!NOTE]
> The cloaked page component renders a full-screen, borderless iframe element containing the resolved destination URL while injecting specific Open Graph metadata tags.
\`\`\`tsx
return <iframe src={url} className="min-h-screen w-full border-none" />;
\`\`\`
Sources: [apps/web/app/cloaked/url/page.tsx:46-46](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/cloaked/%5Burl%5D/page.tsx#L46-L46)
### Open Graph Metadata Injection and Proxy Rendering
The proxy page located at `apps/web/app/[domain]/[key]/proxy/page.tsx` queries edge storage via `getLinkViaEdge` using the provided domain and key parameters. If the target record lacks proxy capabilities (`!data?.proxy`), the handler executes an immediate HTTP redirect to the destination URL or triggers `notFound()` if the link record is missing entirely.
\`\`\`tsx
export default async function ProxyPage(props: {
params: Promise<{ domain: string; key: string }>;
}) {
const params = await props.params;
const domain = params.domain;
const key = decodeURIComponent(params.key);
const data = await getLinkViaEdge({ domain, key });
// if the link doesn't exist
if (!data) {
notFound();
// if the link does not have proxy enabled, redirect to the original URL
} else if (!data?.proxy) {
redirect(data.url);
}
\`\`\`
Sources: [apps/web/app/domain/key/proxy/page.tsx:36-52](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx#L36-L52)
When proxy rendering is active, `generateMetadata` constructs custom Open Graph headers using `constructMetadata`, setting `noIndex: true` and mapping the favicon source to Google's favicon service combined with the apex domain extracted via `getApexDomain`.
\`\`\`tsx
export async function generateMetadata(props: {
params: Promise<{ domain: string; key: string }>;
}) {
const params = await props.params;
const domain = params.domain;
const key = decodeURIComponent(params.key); // key can potentially be encoded
const data = await getLinkViaEdge({ domain, key });
if (!data?.proxy) {
return;
}
const apexDomain = getApexDomain(data.url);
return constructMetadata({
title: unescape(data.title || ""),
description: unescape(data.description || ""),
image: data.image,
video: data.video,
icons: `${GOOGLE_FAVICON_URL}${apexDomain}`,
noIndex: true,
});
}
\`\`\`
Sources: [apps/web/app/domain/key/proxy/page.tsx:11-34](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/proxy/page.tsx#L11-L34)
### Metadata and Iframeable API Endpoints
Edge-runtime API route endpoints support external verification of link properties and metatag extraction. The metatags endpoint at `apps/web/app/api/links/metatags/route.ts` evaluates CORS headers for `.dub.co` origins, enforces IP rate limiting via `ratelimitOrThrow`, and sets cache headers allowing public caching for 300 seconds and Vercel CDN stale-while-revalidate for 86400 seconds.
\`\`\`typescript
export const runtime = "edge";
export async function GET(req: NextRequest) {
try {
const origin = req.headers.get("origin");
// Validate the origin header and set CORS headers accordingly
const corsHeaders = {
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
};
if (origin && origin.endsWith(".dub.co")) {
corsHeaders["Access-Control-Allow-Origin"] = origin;
}
// Validate URL parameter
const { url } = getUrlQuerySchema.parse({
url: req.nextUrl.searchParams.get("url"),
});
// Rate limit by IP
await ratelimitOrThrow(req, "metatags");
// Get metatags
const metatags = await getMetaTags(url);
// Return response
return NextResponse.json(
{
...metatags,
poweredBy: "Dub - The Modern Link Attribution Platform",
},
{
headers: {
...corsHeaders,
"Cache-Control": "public, max-age=300",
"Vercel-CDN-Cache-Control":
"s-maxage=3600, stale-while-revalidate=86400",
},
},
);
\`\`\`
Sources: [apps/web/app/api/links/metatags/route.ts:7-47](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/metatags/route.ts#L7-L47)
| API Endpoint Path | Runtime | Rate Limit Key | Cache-Control Header |
| :--- | :--- | :--- | :--- |
| `apps/web/app/api/links/iframeable/route.ts` | `edge` | `iframeable` | *Not cached* |
| `apps/web/app/api/links/metatags/route.ts` | `edge` | `metatags` | `public, max-age=300` |
\`\`\`typescript
export const runtime = "edge";
\`\`\`
Sources: [apps/web/app/api/links/iframeable/route.ts:10-10](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/iframeable/route.ts#L10-L10)
\`\`\`typescript
export const runtime = "edge";
\`\`\`
Sources: [apps/web/app/api/links/metatags/route.ts:7-7](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/api/links/metatags/route.ts#L7-L7)
## Password Protection Gateways
### Password Gate Interception and Cookie Validation
Incoming requests to links configured with a password attribute are intercepted when the edge link resolver evaluates `cachedLink`. The password protection page at `apps/web/app/password/[linkId]/page.tsx` runs on the `edge` runtime with dynamic rendering enforced via `export const dynamic = "force-dynamic"`. When loaded, the component queries `prismaEdge.link.findUnique` to retrieve link properties including `id`, `domain`, `key`, `password`, `shortLink`, and workspace project details.
\`\`\`tsx
export const dynamic = "force-dynamic";
export const runtime = "edge";
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:11-12](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L11-L12)
\`\`\`tsx
export default async function PasswordProtectedLinkPage(props: {
params: Promise<{ linkId: string }>;
}) {
const params = await props.params;
const link = await prismaEdge.link.findUnique({
where: {
id: params.linkId,
},
select: {
id: true,
domain: true,
key: true,
password: true,
shortLink: true,
project: {
select: {
name: true,
logo: true,
plan: true,
},
},
},
});
\`\`\`
Sources: [apps/web/app/password/[linkId]/page.tsx:58-80](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L80-L80)
> [!WARNING]
> If a link record is not found during the password gate lookup, the handler executes `notFound()` immediately, returning a 404 status before any challenge form is rendered.
\`\`\`tsx
if (!link) {
notFound();
}
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:82-84](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L82-L84)
The verification logic checks whether `link.password` is empty or if the incoming cookie matching `dub_password_${link.id}` equals `link.password`. If validation succeeds, the user is redirected to `link.shortLink`. Otherwise, the edge renders a secure challenge interface containing project branding or a fallback lock icon.
\`\`\`tsx
if (
!link.password ||
(await cookies()).get(`dub_password_${link.id}`)?.value === link.password
) {
redirect(link.shortLink);
}
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:86-91](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L86-L91)
\`\`\`tsx
return (
<>
<NewBackground />
<main className="relative mb-10 flex w-screen flex-col items-center">
<Wordmark className="mt-6 h-8" />
<div className="z-10 mt-8 w-full max-w-[400px] overflow-hidden rounded-2xl border border-neutral-200 shadow-sm md:mt-24">
<div className="flex flex-col items-center justify-center gap-3 border-b border-neutral-200 bg-white px-4 py-6 text-center">
{link.project?.logo ? (
<BlurImage
src={link.project.logo}
alt={link.project.name}
width={48}
height={48}
className="size-12 rounded-full"
/>
) : (
<div className="flex size-12 items-center justify-center rounded-full bg-neutral-100">
<Lock className="size-4 text-neutral-600" />
</div>
)}
<h3 className="mt-1 text-lg font-semibold">Password required</h3>
<p className="w-full max-w-xs text-pretty text-sm text-neutral-500">
{description}
</p>
</div>
<PasswordForm />
</div>
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:93-119](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L93-L119)
### Edge Metadata and Challenge View Parameters
Metadata generation for password-protected links inspects the workspace plan and domain configuration using `generateMetadata`. When the domain is a Dub domain or the workspace plan is set to `free`, the title appends `- Dub` to the default description string. For custom domains on paid tiers, custom project logos and icons are injected into the metadata response with indexing disabled via `noIndex: true`.
\`\`\`tsx
export async function generateMetadata(props: {
params: Promise<{ linkId: string }>;
}) {
const params = await props.params;
const link = await prismaEdge.link.findUnique({
where: {
id: params.linkId,
},
select: {
domain: true,
project: {
select: {
logo: true,
plan: true,
},
},
},
});
if (!link) {
notFound();
}
return constructMetadata({
title:
isDubDomain(link.domain) || link.project?.plan === "free"
? `${title} - Dub`
: title,
description,
image,
...(!isDubDomain(link.domain) &&
link.project?.plan !== "free" &&
link.project?.logo && {
icons: link.project.logo,
}),
noIndex: true,
});
}
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:19-56](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L19-L56)
| Property Name | Target Value / Source | Purpose |
| :--- | :--- | :--- |
| `dynamic` | `force-dynamic` | Disables static caching for password verification gates |
| `runtime` | `edge` | Executes resolution directly on edge workers |
| `image` | `https://assets.dub.co/misc/password-protected.png` | Default Open Graph preview asset |
| `noIndex` | `true` | Prevents search engine indexing on challenge pages |
\`\`\`tsx
export const dynamic = "force-dynamic";
export const runtime = "edge";
const title = "Password Required";
const description =
"This link is password protected. Enter the password to view it.";
const image = "https://assets.dub.co/misc/password-protected.png";
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:11-17](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L11-L17)
\`\`\`tsx
noIndex: true,
\`\`\`
Sources: [apps/web/app/password/linkId/page.tsx:54-54](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/password/%5BlinkId%5D/page.tsx#L54-L54)
## Edge Exception and Fallback Pages
### Expired, Banned, and Not Found Route Handling
When link resolution fails due to expiration, violation bans, or missing targets, Dub serves dedicated fallback pages and custom redirection rules under `apps/web/app/[domain]/`. For expired links, the page at `apps/web/app/[domain]/expired/page.tsx` checks domain records via Prisma and immediately redirects the client to `domainData.expiredUrl` if a custom fallback is defined. Otherwise, it renders an indefinite cache view (`revalidate = false`) featuring a `CircleHalfDottedClock` icon and marketing parameters (`utm_source: "Expired Link"`, `utm_medium: "Expired Link Page"`).
\`\`\`tsx
export const revalidate = false; // cache indefinitely
export const metadata = constructMetadata({
title: "Expired Link",
description:
"This link has expired. Please contact the owner of this link to get a new one.",
noIndex: true,
});
const UTM_PARAMS = {
utm_source: "Expired Link",
utm_medium: "Expired Link Page",
};
export function generateStaticParams() {
return [];
}
export default async function ExpiredLinkPage(props: {
params: Promise<{ domain: string }>;
}) {
const { domain } = await props.params;
const domainData = await prisma.domain.findUnique({
where: {
slug: domain,
},
});
if (domainData?.expiredUrl) {
redirect(domainData.expiredUrl);
}
\`\`\`
Sources: [apps/web/app/domain/expired/page.tsx:12-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/expired/page.tsx#L12-L42)
\`\`\`tsx
return (
<div>
<Hero>
<div className="relative mx-auto flex w-full max-w-md flex-col items-center">
<BubbleIcon>
<CircleHalfDottedClock className="size-12" />
</BubbleIcon>
<h1
className={cn(
"font-display mt-10 text-center text-4xl font-medium text-neutral-900 sm:text-5xl sm:leading-[1.15]",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:20px] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
Expired link
</h1>
<p
className={cn(
"mt-5 text-pretty text-base text-neutral-700 sm:text-xl",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:10px] [animation-delay:200ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
This link has expired. Please contact the owner of this link to get
a new one.
</p>
</div>
<div
className={cn(
"xs:flex-row relative mx-auto mt-8 flex max-w-fit flex-col items-center gap-4",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:5px] [animation-delay:300ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
<ButtonLink variant="primary" href="https://app.dub.co/register">
Try Dub today
</ButtonLink>
<LearnMoreButton utmParams={UTM_PARAMS} />
</div>
</Hero>
<div className="mt-20">
<FeaturesSection utmParams={UTM_PARAMS} />
</div>
<div className="mt-32">
<CTA utmParams={UTM_PARAMS} />
</div>
</div>
);
}
\`\`\`
Sources: [apps/web/app/domain/expired/page.tsx:44-90](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/expired/page.tsx#L44-L90)
Similarly, not found routes handled by `apps/web/app/[domain]/notfound/page.tsx` query `prisma.domain.findUnique` for a `notFoundUrl` property. If configured, requests are redirected to the workspace's designated URL; otherwise, a link not found screen with a `GlobeSearch` placeholder and Open Graph metadata (`notfoundlink.jpg`, `noIndex: true`) is displayed.
\`\`\`tsx
export const revalidate = false; // cache indefinitely
export const metadata = constructMetadata({
title: "Link Not Found",
description:
"This link does not exist on Dub. Please check the URL and try again.",
image: "https://assets.dub.co/misc/notfoundlink.jpg",
noIndex: true,
});
const UTM_PARAMS = {
utm_source: "Link Not Found",
utm_medium: "Link Not Found Page",
};
export function generateStaticParams() {
return [];
}
export default async function NotFoundLinkPage(props: {
params: Promise<{ domain: string }>;
}) {
const { domain } = await props.params;
const domainData = await prisma.domain.findUnique({
where: {
slug: domain,
},
});
if (domainData?.notFoundUrl) {
redirect(domainData.notFoundUrl);
}
\`\`\`
Sources: [apps/web/app/domain/notfound/page.tsx:12-43](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L12-L43)
\`\`\`tsx
return (
<main className="flex min-h-screen flex-col justify-between">
<Hero>
<div className="relative mx-auto flex w-full max-w-md flex-col items-center">
<BubbleIcon>
<GlobeSearch className="size-12" />
</BubbleIcon>
<h1
className={cn(
"font-display mt-10 text-center text-4xl font-medium text-neutral-900 sm:text-5xl sm:leading-[1.15]",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:20px] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
Link not found
</h1>
<p
className={cn(
"mt-5 text-pretty text-base text-neutral-700 sm:text-xl",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:10px] [animation-delay:200ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
This link does not exist on Dub. Please check the URL and try again.
</p>
</div>
<div
className={cn(
"xs:flex-row relative mx-auto mt-8 flex max-w-fit flex-col items-center gap-4",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:5px] [animation-delay:300ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
<ButtonLink variant="primary" href="https://app.dub.co/register">
Try Dub today
</ButtonLink>
<LearnMoreButton utmParams={UTM_PARAMS} />
</div>
</Hero>
<div className="mt-20">
<FeaturesSection utmParams={UTM_PARAMS} />
</div>
<div className="mt-32">
<CTA utmParams={UTM_PARAMS} />
</div>
</main>
);
}
\`\`\`
Sources: [apps/web/app/domain/notfound/page.tsx:45-90](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L45-L90)
For links violating terms of service, `apps/web/app/[domain]/banned/page.tsx` renders a static `ShieldSlash` icon layout with `Banned Link` metadata without querying database redirection attributes.
\`\`\`tsx
export const revalidate = false; // cache indefinitely
export const metadata = constructMetadata({
title: "Banned Link",
description: "This link has been banned for violating our terms of service.",
noIndex: true,
});
const UTM_PARAMS = {
utm_source: "Banned Link",
utm_medium: "Banned Link Page",
};
export function generateStaticParams() {
return [];
}
\`\`\`
Sources: [apps/web/app/domain/banned/page.tsx:10-25](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/banned/page.tsx#L10-L25)
\`\`\`tsx
export default function BannedLinkPage() {
return (
<div>
<Hero>
<div className="relative mx-auto flex w-full max-w-sm flex-col items-center">
<BubbleIcon>
<ShieldSlash className="size-12" />
</BubbleIcon>
<h1
className={cn(
"font-display mt-10 text-center text-4xl font-medium text-neutral-900 sm:text-5xl sm:leading-[1.15]",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:20px] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
Banned link
</h1>
<p
className={cn(
"mt-5 text-pretty text-base text-neutral-700 sm:text-xl",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:10px] [animation-delay:200ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
This link has been banned for violating our terms of service.
</p>
</div>
<div
className={cn(
"xs:flex-row relative mx-auto mt-8 flex max-w-fit flex-col items-center gap-4",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:5px] [animation-delay:300ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
<ButtonLink variant="primary" href="https://app.dub.co/register">
Try Dub today
</ButtonLink>
<LearnMoreButton utmParams={UTM_PARAMS} />
</div>
</Hero>
<div className="mt-20">
<FeaturesSection utmParams={UTM_PARAMS} />
</div>
<div className="mt-32">
<CTA utmParams={UTM_PARAMS} />
</div>
</div>
);
\`\`\`
Sources: [apps/web/app/domain/banned/page.tsx:27-72](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/banned/page.tsx#L27-L72)
> [!NOTE]
> All fallback and exception page routes define `generateStaticParams()` returning an empty array to prevent unnecessary static pre-rendering across multi-tenant domains.
\`\`\`tsx
export function generateStaticParams() {
return [];
}
\`\`\`
Sources: [apps/web/app/domain/expired/page.tsx:26-28](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/expired/page.tsx#L26-L28)
\`\`\`tsx
export function generateStaticParams() {
return [];
}
\`\`\`
Sources: [apps/web/app/domain/notfound/page.tsx:27-29](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L27-L29)
\`\`\`tsx
export function generateStaticParams() {
return [];
}
\`\`\`
Sources: [apps/web/app/domain/banned/page.tsx:23-25](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/banned/page.tsx#L23-L25)
### Link Inspector and Edge Verification Routes
The link inspector located at `apps/web/app/[domain]/[key]/inspect/page.tsx` operates explicitly on the `edge` runtime (`export const runtime = "edge"`). It decodes the URL path key and retrieves link metadata via `getLinkViaEdge({ domain, key })`. If the record does not exist, the handler invokes `notFound()` to abort rendering.
\`\`\`tsx
export const runtime = "edge";
\`\`\`
Sources: [apps/web/app/domain/key/inspect/page.tsx:15-15](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L15-L15)
\`\`\`tsx
export default async function InspectPage(props: {
params: Promise<{ domain: string; key: string }>;
}) {
const params = await props.params;
const domain = params.domain;
const key = decodeURIComponent(params.key);
const data = await getLinkViaEdge({ domain, key });
// if the link doesn't exist
if (!data) {
notFound();
}
\`\`\`
Sources: [apps/web/app/domain/key/inspect/page.tsx:41-53](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L41-L53)
Metadata generation for the inspection view extracts the apex domain of the target URL using `getApexDomain(data.url)` and constructs Google favicon icons combined with unescaped link titles and descriptions.
\`\`\`tsx
export async function generateMetadata(props: {
params: Promise<{ domain: string; key: string }>;
}) {
const params = await props.params;
const domain = params.domain;
const key = decodeURIComponent(params.key);
const data = await getLinkViaEdge({ domain, key });
if (!data) {
return;
}
const apexDomain = getApexDomain(data.url);
return constructMetadata({
title: unescape(data.title || ""),
description: unescape(data.description || ""),
image: data.image,
icons: `${GOOGLE_FAVICON_URL}${apexDomain}`,
noIndex: true,
});
}
\`\`\`
Sources: [apps/web/app/domain/key/inspect/page.tsx:17-39](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L17-L39)
| Fallback Route File | Target Condition | Action / Render Component |
| :--- | :--- | :--- |
| `expired/page.tsx` | `domainData.expiredUrl` present | Redirects to custom URL; otherwise renders `CircleHalfDottedClock` |
| `notfound/page.tsx` | `domainData.notFoundUrl` present | Redirects to custom URL; otherwise renders `GlobeSearch` |
| `banned/page.tsx` | Terms of service violation | Renders `ShieldSlash` icon and restriction warning |
| `inspect/page.tsx` | Missing link record (`!data`) | Executes `notFound()`; otherwise renders `LinkInspectorCard` |
\`\`\`tsx
if (domainData?.expiredUrl) {
redirect(domainData.expiredUrl);
}
\`\`\`
Sources: [apps/web/app/domain/expired/page.tsx:40-42](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/expired/page.tsx#L40-L42)
\`\`\`tsx
if (domainData?.notFoundUrl) {
redirect(domainData.notFoundUrl);
}
\`\`\`
Sources: [apps/web/app/domain/notfound/page.tsx:41-43](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/notfound/page.tsx#L41-L43)
\`\`\`tsx
<p
className={cn(
"mt-5 text-pretty text-base text-neutral-700 sm:text-xl",
"animate-slide-up-fade motion-reduce:animate-fade-in [--offset:10px] [animation-delay:200ms] [animation-duration:1s] [animation-fill-mode:both]",
)}
>
This link has been banned for violating our terms of service.
</p>
\`\`\`
Sources: [apps/web/app/domain/banned/page.tsx:43-50](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/banned/page.tsx#L43-L50)
\`\`\`tsx
// if the link doesn't exist
if (!data) {
notFound();
}
\`\`\`
Sources: [apps/web/app/domain/key/inspect/page.tsx:50-53](https://github.com/blade47/dub/blob/7c93d97abf0ecd37fb669c6a68a1f7ef9109daea/apps/web/app/%5Bdomain%5D/%5Bkey%5D/inspect/page.tsx#L50-L53)
## Related
- [Request Routing](/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/core-architecture/request-routing)
- [A/B Testing](/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-attribution/a-b-testing)
- [Tinybird Analytics](/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-pipeline/tinybird-analytics)
Was this page helpful?