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's database architecture is built around a multi-file Prisma ORM setup configured for a MySQL datasource, connecting core multi-tenant workspaces, domains, and redirect links with advanced partner program management, attribution, and analytics pipelines. Sources: apps/web/prisma/schema/schema.prisma:1-9, apps/web/prisma/schema/workspace.prisma:1-105, apps/web/prisma/schema/link.prisma:1-107, apps/web/prisma/schema/program.prisma:1-179, apps/web/prisma/schema/commission.prisma:1-87, apps/web/prisma/schema/reward.prisma:1-81
The schema addresses the complexities of distributed edge lookups, multi-tenant link shortening, and high-volume referral attribution by segregating schema logic across specialized domain files while maintaining strict relational constraints and indexing strategies for optimized read and write performance. Sources: apps/web/prisma/schema/workspace.prisma:103-105, apps/web/prisma/schema/link.prisma:96-106, apps/web/prisma/schema/commission.prisma:73-86
Dub organizes its database schema using a multi-file layout under apps/web/prisma/schema/, separating core domain models into dedicated files such as schema.prisma, workspace.prisma, link.prisma, and misc.prisma. Sources: apps/web/prisma/schema/schema.prisma:1-9, apps/web/prisma/schema/workspace.prisma:1-154, apps/web/prisma/schema/link.prisma:1-108, apps/web/prisma/schema/misc.prisma:1-36
The primary connection and client code generation are defined in schema.prisma, establishing a MySQL provider with an environment variable-backed connection URL and Prisma relation mode. Sources: apps/web/prisma/schema/schema.prisma:1-9
Data fetching utilities across Dub leverage the generated Prisma client wrapped with React's cache utility to retrieve workspaces, default projects, and individual short links. Sources: apps/web/lib/fetchers/index.ts:1-68
Note
Database fetchers such as getDefaultWorkspace, getWorkspace, and getLink integrate session verification with React caching to avoid redundant queries during request lifecycles. Sources: apps/web/lib/fetchers/index.ts:5-67
The following example demonstrates the execution flow of fetching a link record by its domain and key composite identifier using the Prisma client instance:
export const getLink = cache(
async ({ domain, key }: { domain: string; key: string }) => {
return await prisma.link.findUnique({
where: {
domain_key: {
domain,
key,
},
},
});
},
);Sources: apps/web/lib/fetchers/index.ts:56-67
The core data architecture for multi-tenant URL shortening centers around the Project (workspace) model, which acts as the parent container for domains, short links, UTM templates, team memberships, and product usage limits. Individual short links belong to specific workspaces and map to verified custom or system domains, while UTM templates standardize parameter injection across campaigns. Sources: apps/web/prisma/schema/workspace.prisma:13-105, apps/web/prisma/schema/link.prisma:1-107, apps/web/prisma/schema/domain.prisma:1-28, apps/web/prisma/schema/utm.prisma:1-28
Workspaces configure billing tiers, subscription states, and quotas governing links, clicks, partner payouts, and AI features. The system tracks consumption against these limits to enforce billing policies and restrict over-quota operations. Sources: apps/web/prisma/schema/workspace.prisma:13-61
Note
Workspace usage recomputation executes asynchronously by aggregating raw event and invoice records over the active billing window defined by billingCycleStart. Sources: apps/web/lib/api/billing/recompute-workspace-usage.ts:8-50
The workspace billing and usage synchronization routine evaluates current consumption metrics by querying analytics stores and financial records in parallel. The execution path follows a strict function sequence:
recomputeWorkspaceUsage() receives the workspace object containing its id and billingCycleStart property. Sources: apps/web/lib/api/billing/recompute-workspace-usage.ts:8-10getBillingStartDate() calculates the exact starting timestamp based on the billing cycle day. Sources: apps/web/lib/api/billing/recompute-workspace-usage.ts:3-11Promise.all() concurrently invokes getWorkspaceUsage() twice (querying resource "events" for clicks and "links" for link creations) alongside prisma.invoice.aggregate() to sum completed partner payouts. Sources: apps/web/lib/api/billing/recompute-workspace-usage.ts:14-43sum() processes the mapped metric arrays to produce aggregate totals for usage, linksUsage, and payoutsUsage. Sources: apps/web/lib/api/billing/recompute-workspace-usage.ts:6-49export async function recomputeWorkspaceUsage(
workspace: Pick<Project, "id" | "billingCycleStart">,
) {
const billingStart = getBillingStartDate(workspace.billingCycleStart);
const billingEnd = new Date();
const [clicksData, linksData, payoutsUsage] = await Promise.all([
getWorkspaceUsage({
workspaceId: workspace.id,
resource: "events",
start: billingStart,
end: billingEnd,
}),
getWorkspaceUsage({
workspaceId: workspace.id,
resource: "links",
start: billingStart,
end: billingEnd,
}),
prisma.invoice.aggregate({
where: {
workspaceId: workspace.id,
type: "partnerPayout",
status: "completed",
paidAt: {
gte: billingStart,
lte: billingEnd,
},
},
_sum: {
amount: true,
},
}),
]);
return {
usage: sum(clicksData.map((d) => d.value)),
linksUsage: sum(linksData.map((d) => d.value)),
payoutsUsage: payoutsUsage._sum.amount ?? 0,
};
}Short links store routing configurations, Open Graph metadata, A/B testing variants, expiration rules, and native UTM tracking parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content). Domains regulate verification status and link retention policies, while UTM templates store reusable parameter sets for campaigns. Sources: apps/web/prisma/schema/link.prisma:1-95, apps/web/prisma/schema/domain.prisma:1-28, apps/web/prisma/schema/utm.prisma:1-24
Warning
Short links do not index full target URLs directly; instead, they maintain a length-constrained index on [projectId, url(length: 500)] to support high-performance URL upsert and deduplication routines without violating database key length limitations. Sources: apps/web/prisma/schema/link.prisma:99|99
Edge link resolution in Dub relies on high-performance caching layers and direct database queries executed via PlanetScale. When an incoming request reaches the edge middleware, the system parses the hostname and URL path to isolate the target domain and link key. Depending on whether the domain is configured for case sensitivity, the lookup key undergoes ASCII punyencoding and potential lowercasing before querying the underlying datastores. Sources: apps/web/lib/middleware/link.ts:43-63, apps/web/lib/planetscale/get-link-with-partner.ts:24-61
The runtime resolution of a short link and its associated partner data follows a precise execution sequence across the middleware and PlanetScale query layer:
LinkMiddleware() extracts the incoming request domain, full key, and search parameters using the parse() helper function. Sources: apps/web/lib/middleware/link.ts:43-44punyEncode(), and if the domain is not case-sensitive, it is normalized to lowercase. Sources: apps/web/lib/middleware/link.ts:50-56+ character, the suffix is stripped from the key before further processing. Sources: apps/web/lib/middleware/link.ts:58-62getLinkWithPartner() checks domain case sensitivity to apply either encodeKey() or a combination of safeDecodeURIComponent() and punyEncode() to construct keyToQuery. Sources: apps/web/lib/planetscale/get-link-with-partner.ts:24-34conn.execute() runs a relational query joining Link with ProgramEnrollment, Partner, LinkReward, Discount, and Program tables using domain and keyToQuery. Sources: apps/web/lib/planetscale/get-link-with-partner.ts:37-60export const getLinkWithPartner = async ({
domain,
key,
}: {
domain: string;
key: string;
}): Promise<QueryResult | null> => {
const keyToQuery = isCaseSensitiveDomain(domain)
? encodeKey(key)
: punyEncode(safeDecodeURIComponent(key));
console.time("getLinkWithPartner");
const { rows } =
(await conn.execute(
`SELECT
Link.*,
Partner.id as partnerId,
Partner.name as partnerName,
Partner.image as partnerImage,
ProgramEnrollment.groupId as groupId,
ProgramEnrollment.tenantId as tenantId,
PartnerDiscount.id as discountId,
PartnerDiscount.amount as discountAmount,
PartnerDiscount.type as discountType,
PartnerDiscount.maxDuration as discountMaxDuration,
PartnerDiscount.couponId as discountCouponId,
PartnerDiscount.couponTestId as discountCouponTestId
FROM Link
LEFT JOIN ProgramEnrollment ON ProgramEnrollment.programId = Link.programId AND ProgramEnrollment.partnerId = Link.partnerId
LEFT JOIN Partner ON Partner.id = ProgramEnrollment.partnerId
LEFT JOIN LinkReward ON LinkReward.linkId = Link.id
LEFT JOIN Discount PartnerDiscount ON PartnerDiscount.id = COALESCE(LinkReward.discountId, ProgramEnrollment.discountId)
AND PartnerDiscount.programId IS NOT NULL
LEFT JOIN Program ON Program.id = Link.programId
WHERE Link.domain = ? AND Link.key = ?`,
[domain, keyToQuery],
)) || {};
console.timeEnd("getLinkWithPartner");
const link =
rows && Array.isArray(rows) && rows.length > 0 ? (rows[0] as any) : null;
if (!link) {
return null;
}
// ... maps results and returns EdgeLinkProps
};The Link model defines dedicated unique constraints and indexes to guarantee sub-millisecond retrieval performance across various lookup vectors at the edge.
Note
Link expiration timestamps (expiresAt) are synchronized with Redis Time-To-Live (TTL) values, whereas target URLs (url), proxy configurations (proxy), domains (domain), and link keys (key) are mirrored directly on Redis nodes for edge acceleration alongside primary MySQL storage. Sources: apps/web/prisma/schema/link.prisma:3-8|13|13
The partner program architecture in Dub relies on a multi-model relational schema connecting workspaces, programs, partner enrollments, partner groups, and partner-specific link creation endpoints. A partner program (Program) acts as the top-level container defined within a workspace (Project), establishing primary reward settings, domains, and payout modes. Partners (Partner) enroll in programs through ProgramEnrollment records, which track performance metrics, reward overrides, and status lifecycle values. Partners can also be organized into PartnerGroup entities, defining group-level link structures, default reward assignments, and UTM template configurations.
Program enrollments govern the state of a partner inside a specific program using the ProgramEnrollmentStatus enum. Each status handles a distinct phase of partner onboarding, review, or deactivation.
Important
The ProgramEnrollment model enforces unique constraints on both [partnerId, programId] and [tenantId, programId], ensuring that a single partner or tenant cannot duplicate active enrollments within the same program boundary. Sources: apps/web/prisma/schema/program.prisma:174-177
Partner-specific links are created via the POST endpoint at apps/web/app/ee/api/partners/links/route.ts. The request execution follows an explicit validation and processing sequence before persisting the link:
createPartnerLinkSchemaInternal.parse() validates the incoming JSON request body for partner identifiers, custom URLs, keys, and reward IDs. Sources: apps/web/app/ee/api/partners/links/route.ts:98-108getProgramOrThrow() fetches the target program and verifies that its domain and base URL are configured. Sources: apps/web/app/ee/api/partners/links/route.ts:110-121prisma.programEnrollment.findUnique() queries the database using either partnerId or tenantId combined with programId, loading partner relation fields and associated partnerGroup defaults and UTM templates. Sources: apps/web/app/ee/api/partners/links/route.ts:125-144processLink() executes core link validation and generation logic, registering the target URL, domain, program ID, tenant ID, and partner ID. Sources: apps/web/app/ee/api/partners/links/route.ts:165-180applyGroupUtmToLink() applies group UTM template parameters and partner naming conventions to the generated link object. Sources: apps/web/app/ee/api/partners/links/route.ts:189-193throwIfInvalidRewards() validates link-level reward assignments against the program and partner group constraints. Sources: apps/web/app/ee/api/partners/links/route.ts:216-220createLink() persists the final constructed link record with optional reward overrides. Sources: apps/web/app/ee/api/partners/links/route.ts:222-225Warning
If a partner attempts to create a link with custom link-level rewards (clickRewardId, leadRewardId, saleRewardId, or discountId), the workspace plan capability check (canUseAdvancedRewardLogic) is evaluated. If the workspace plan does not permit advanced reward logic, the operation throws a forbidden DubApiError with the PARTNER_LEVEL_REWARDS_PLAN_ERROR constant. Sources: apps/web/app/ee/api/partners/links/route.ts:197-214
The reward and commission data architecture manages partner compensation, event tracking, and periodic click aggregation. Rewards define payout structures and constraints using fixed or percentage models across event types, while commissions record individual financial ledger entries tied to clicks, leads, sales, or referrals.
The Prisma schema defines specific enumeration sets governing event classification, payout structures, commission statuses, and data origins.
The periodic click aggregation pipeline processes link engagement data and batches click commissions. The background execution follows an explicit function call sequence:
processClickAggregation() queries prisma.programEnrollment.findUnique() using partner and program IDs, checking whether the enrollment status is commission-eligible. Sources: apps/web/lib/commissions/process-click-aggregation.ts:39-70calculateEarningsByLink() queries active links with clicks after the start date, fetches geographic click distributions via getTopLinksByCountries(), and calculates link-level earnings against configured click rewards. Sources: apps/web/lib/commissions/process-click-aggregation.ts:90-148createClickCommissions() iterates over link earnings, evaluating reward spend limits via getHistoricalClicksEarnings() and building ledger entries with idempotency keys. Sources: apps/web/lib/commissions/process-click-aggregation.ts:254-349prisma.commission.createMany() persists the batched click commissions with skipDuplicates: true. Sources: apps/web/lib/commissions/process-click-aggregation.ts:359-362syncTotalCommissions() updates aggregate partner program totals following successful insertion. Sources: apps/web/lib/commissions/process-click-aggregation.ts:369-373Note
Click commissions rely on invoiceId formatted as ${linkId}-${aggregationDate} combined with a unique constraint on [invoiceId, programId] to ensure idempotency during batch aggregation runs. Sources: apps/web/prisma/schema/commission.prisma:73|73, apps/web/lib/commissions/process-click-aggregation.ts:348|348
The getCommissions() function queries commission records for admin dashboards and partner portals, applying dynamic status filters and metadata predicates.
export async function getCommissions(filters: CommissionsFilters) {
const { invoiceId, programId, partnerId, status, type, ... } = filters;
const parsedMetadataQuery = parseCommissionMetadataQuery(query);
const metadataWhere = buildCommissionMetadataWhere(parsedMetadataQuery);
if (invoiceId) {
return await prisma.commission.findMany({
where: { invoiceId, programId, ...metadataWhere },
include: { ...commissionIncludes },
});
}
const paginationQuery = buildPaginationQuery(filters);
const statusFilter = status
? status
: type || customerId || payoutId || bountySubmissionId || partnerId
? undefined
: {
notIn: [
CommissionStatus.duplicate,
CommissionStatus.fraud,
CommissionStatus.canceled,
],
};
return await prisma.commission.findMany({
where: {
earnings: { not: 0 },
programId,
status: statusFilter,
createdAt: { gte: startDate, lte: endDate },
...metadataWhere,
},
include: { ...commissionIncludes },
...paginationQuery,
});
}Warning
When querying commissions without an explicit status filter, duplicate, fraud, and canceled commissions are automatically excluded from the result set unless specifically requested via filters. Sources: apps/web/lib/api/commissions/get-commissions.ts:141-151
Bulk data migrations and performance tuning require careful handling of batch sizes, transaction boundaries, and pagination constants when interacting with PlanetScale and Prisma. Large-scale data transformations, such as backfilling metadata or correcting partner link alignments, use bounded iteration loops and explicit limits to prevent memory exhaustion and database lock contention. Sources: apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts:101-125, apps/web/scripts/migrations/backfill-commissions-metadata.ts:25-30
The commission metadata backfilling script processes records in batched loops, pulling source event metadata from Tinybird and performing bulk updates via raw SQL execution. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:115-246
main() initiates an infinite iteration loop governed by a fixed BATCH_SIZE of 1000 records and a throttle delay of 1000 ms. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:26-27, apps/web/scripts/migrations/backfill-commissions-metadata.ts:115-116prisma.commission.findMany() queries target commissions where type is lead or sale, eventId is not null, metadata is null, and id is greater than the current cursor (startingAfter), ordered ascending by id. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:116-142getEventsMetadata() executes a Tinybird pipe query using extracted event IDs to retrieve corresponding event metadata payloads. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:150-159parseUserProvidedMetadata() validates raw string payloads against length bounds (USER_METADATA_MAX_CHARS = 10,000), JSON syntax, and internal event fingerprint checks (looksLikeInternalEventPayload()) to filter out webhook dumps. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:28-29, apps/web/scripts/migrations/backfill-commissions-metadata.ts:50-98prisma.$executeRaw applies conditional case statements (CASE id WHEN ... THEN ... END) to update commission records in bulk when DRY_RUN is disabled. Sources: apps/web/scripts/migrations/backfill-commissions-metadata.ts:25, apps/web/scripts/migrations/backfill-commissions-metadata.ts:212-226Warning
When executing bulk updateMany operations on tables with large volumes of pending or un-processed records, always paginate or apply a limit parameter (such as PRISMA_UPDATEMANY_LIMIT) inside a while (true) loop to prevent exceeding lock timeouts or memory thresholds on PlanetScale. Sources: apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts:101-125
Sources: apps/web/scripts/dub-partner-rewind.ts:6-45, apps/web/scripts/migrations/backfill-commissions-metadata.ts:25-29
Sources: apps/web/scripts/dub-partner-rewind.ts:45-61, apps/web/scripts/migrations/backfill-commissions-metadata.ts:127-226