Dub is an open-source link attribution platform — short URLs, click analytics, geo-targeting, A/B testing, and an affiliate/partner program layer, all in a single TypeScript monorepo. It processes over 100M clicks a month in production and powers customers like Framer, Perplexity, and Vercel.
Why I starred it
Most link shorteners are dumb pipes. You send someone to a URL and you know they clicked. Dub's value proposition is that every redirect is an opportunity to collect structured attribution data — who clicked, from where, on what device, through which partner — and feed it back into a conversion funnel.
What caught me was the scope of the engineering. This isn't a CRUD app wrapped in a short link. It's a full middleware-based routing system with a multi-layer cache, bot detection, per-link A/B test assignment, deep link handling for iOS App Store URLs, and a webhook fan-out pipeline. That's a lot of decisions to get right in edge middleware.
How it works
The entry point is apps/web/middleware.ts. Every request that hits a Dub-owned domain — dub.sh, custom domains, partner hostnames — passes through a routing tree that dispatches to specialized middleware handlers. The request parsing happens in lib/middleware/utils/parse.ts, which extracts domain, key, and fullPath.
LinkMiddleware in lib/middleware/link.ts handles the actual redirect. The cache lookup is a three-tier fallback:
- In-process LRU cache (10,000 entries, 5-second TTL)
- Global Redis via Upstash
- MySQL on PlanetScale via direct edge query
The cache implementation in lib/api/links/cache.ts makes this explicit:
// Check LRU cache first
let cachedLink = linkLRUCache.get(cacheKey) || null;
if (cachedLink) {
console.log(`[LRU Cache HIT] ${cacheKey}`);
linkLRUCache.set(cacheKey, cachedLink); // refresh the LRU cache
return cachedLink;
}
// ...falls through to Redis, then Vercel runtime cache, then MySQL
When Vercel spins up new Fluid instances under traffic spikes, the LRU cache is cold. So there's a fourth tier: Vercel's runtime cache (getCache()) with a 5-minute TTL, used as a read-cheap fallback before hitting MySQL. The comment in the source is honest: "Vercel cache reads are 10x cheaper than writes."
Bot detection in lib/middleware/utils/detect-bot.ts layers three checks: User-Agent regex matching against a maintained bot list, referrer header matching, and CIDR range matching for known bot IP blocks. HEAD requests are treated as bots by default.
The A/B testing in lib/middleware/utils/resolve-ab-test-url.ts uses weighted random selection with cumulative weights — a clean implementation. It also persists the assigned variant in a dub_test_url cookie so repeat visitors stay in the same bucket. The test runs until testCompletedAt, which is a server-side timestamp on the link record.
// Calculate cumulative weights
for (i = 1; i < testVariants.length; ++i) {
weights[i] = testVariants[i].percentage + weights[i - 1];
}
// Generate a random number between 0 and total cumulative weight
const random = Math.random() * weights[weights.length - 1];
Click recording goes to Tinybird via lib/tinybird/record-click.ts, fired in ev.waitUntil() so it doesn't block the redirect. The function assembles geo data (from Vercel's geolocation()), UA, referer, a SHA-256 identity hash of IP + UA for deduplication, and then publishes to Upstash Redis streams for the webhook fan-out.
The identity hash is intentionally lossy:
// Combine IP + UA to create a unique identifier (for deduplication)
export async function getIdentityHash(req: Request) {
const ip = ipAddress(req) || LOCALHOST_IP;
const ua = userAgent(req);
return await hashStringSHA256(`${ip}-${ua.ua}`);
}
No fingerprinting beyond that — it's enough to deduplicate within a time window without storing PII.
Using it
The public API is straightforward:
# Create a short link
curl -X POST https://api.dub.co/links \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "domain": "dub.sh"}'
# Create with geo-targeting
curl -X POST https://api.dub.co/links \
-H "Authorization: Bearer <token>" \
-d '{"url": "https://example.com", "geo": {"US": "https://example.com/us", "DE": "https://example.de"}}'
Self-hosting uses Docker Compose with MySQL, Redis (Upstash-compatible), and Tinybird. The .env.example in apps/web/ documents around 40 environment variables — expect a half-day of setup before you have a running instance.
The TypeScript SDK (packages/utils) exposes the same abstractions the platform uses internally, which means any utility you find in the source is something you can call from your own integration.
Rough edges
Self-hosting is technically possible but obviously not the primary use case. The dependency list reads like a who's-who of managed services: PlanetScale, Upstash, Tinybird, Stripe, Resend, BoxyHQ, Axiom. Running this yourself means either substituting each one or paying for all of them.
The open-core license (AGPLv3 for core, commercial for /ee) means the enterprise features — SSO, advanced partner programs — are not available for self-hosters without a commercial agreement. The README is upfront about this.
The A/B test UI is gated to paid plans. The middleware logic is fully open, but setting up and managing tests through the dashboard requires a subscription.
Test coverage exists (Playwright + Vitest configured) but is thin outside of critical paths. The tests/ directory has integration tests but coverage of the middleware decision tree is not comprehensive.
Bottom line
If you're building a product with an affiliate or partner layer and need short-link attribution that actually closes the loop — click to conversion — Dub is the most complete open-source option in this space. If you just need a short link, it's overkill.
