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:
Conversion and event tracking form the core infrastructure for capturing visitor interactions, link performance, and downstream attribution across the system. This subsystem solves the complexity of bridging client-side click handling with server-side business intelligence by offering secure client ingestion endpoints, edge caching routers, robust bot filtering pipelines, and server-side attribution engines for both leads and sales. Key design decisions include utilizing Redis for low-latency deduplication and caching, Tinybird for high-throughput analytical event logging, and integrated webhooks or direct API handlers to support external processors like Stripe, Shopify, AppsFlyer, Singular, and Google Ads. By orchestrating automated workflows, partner commission triggers, and historical event replay pipelines, the tracking layer ensures reliable attribution and unified analytics across the entire ecosystem.
Sources: apps/web/app/ee/api/track/lead/client/route.ts:1-60, apps/web/app/ee/api/track/application/route.ts:41-117, apps/web/app/ee/api/track/visit/route.ts:17-117, apps/web/app/ee/api/track/click/route.ts:57-183, apps/web/app/ee/api/appsflyer/webhook/route.ts:27-177, apps/web/lib/integrations/google-ads/api.ts:443-503, apps/web/app/ee/api/track/sale/client/route.ts:13-73, apps/web/lib/api/conversions/track-sale.ts:363-531, apps/web/app/ee/api/track/open/route.ts:23-202, apps/web/lib/api/conversions/track-lead.ts:112-231, apps/web/app/ee/api/stripe/integration/webhook/checkout-session-completed.ts:153-324, apps/web/lib/tinybird/record-click.ts:22-236
Client-side ingestion relies on public Next.js API routes under apps/web/app/(ee)/api/track/ to accept tracking payloads for visits, clicks, deep link opens, and client-side conversions (leads and sales). Each route handles cross-origin requests by returning COMMON_CORS_HEADERS and responding to preflight OPTIONS requests with status 204.
Sources: apps/web/app/ee/api/track/lead/client/route.ts:6-67, apps/web/app/ee/api/track/visit/route.ts:2-124, apps/web/app/ee/api/track/click/route.ts:6-190, apps/web/app/ee/api/track/sale/client/route.ts:6-80, apps/web/app/ee/api/track/open/route.ts:2-209
Incoming requests undergo parsing and validation using Zod schemas before hitting downstream attribution wrappers or database resolvers. Click tracking validates domains via getDomainWithoutWWW and requires a link key, optionally accepting custom URLs and referrers. Visit and open endpoints resolve paths by extracting pathname segments, defaulting root paths to _root.
Sources: apps/web/app/ee/api/track/click/route.ts:25-62, apps/web/app/ee/api/track/visit/route.ts:20-34, apps/web/app/ee/api/track/open/route.ts:31-79
Sources: apps/web/app/ee/api/track/lead/client/route.ts:9-60, apps/web/app/ee/api/track/visit/route.ts:18-27, apps/web/app/ee/api/track/click/route.ts:25-61, apps/web/app/ee/api/track/sale/client/route.ts:9-73, apps/web/app/ee/api/track/open/route.ts:15-33
To minimize database load, client endpoints parallelize lookups across Redis global caches using redisGlobalWithTimeout. For click tracking, the pipeline queries recordClickCache and linkCache simultaneously to check for existing click IDs and pre-fetched link properties.
Sources: apps/web/app/ee/api/track/visit/route.ts:38-71, apps/web/app/ee/api/track/click/route.ts:66-98, apps/web/app/ee/api/track/open/route.ts:80-109, apps/web/lib/tinybird/record-click.ts:169-233
Note
When a click ID is newly generated or recorded via recordClick, shouldCacheClickId instructs Redis to cache the full clickData object under clickIdCache:${clickId} with a 5-minute expiration (ex: 60 * 5) to bridge ingestion lag before events appear in Tinybird.
Client-side lead and sale tracking endpoints use withPublishableKey middleware alongside verifyAnalyticsAllowedHostnames to ensure requests originate from permitted domains configured on the workspace. If an unauthorized origin calls the endpoint, a DubApiError with code forbidden is thrown, referencing the settings dashboard URL.
Warning
Requests containing the dub-no-track HTTP header or dub-no-track query parameter are immediately dropped, returning null before bot detection or analytics recording executes.
Sources: apps/web/app/ee/api/track/lead/client/route.ts:14-28, apps/web/app/ee/api/track/sale/client/route.ts:14-28, apps/web/lib/tinybird/record-click.ts:57-62
The application event ingestion pipeline processes marketplace and program lifecycle tracking events such as visits and starts. The entry point handles incoming requests via POST /api/track/application, wrapping execution with Axiom logging and CORS headers.
Sources: apps/web/app/ee/api/track/application/route.ts:41-43, apps/web/app/ee/api/track/application/route.ts:119-124
The pipeline executes initial validation and security checks in a strict call order before parsing request payloads.
detectBot(req) — Evaluates user agent and request characteristics against bot signatures; if a bot is detected, an immediate 202 response with { ok: true } is returned.getIP() — Resolves the client IP address for rate-limiting identification.assertRateLimit() — Validates the IP against the RATELIMIT_POLICIES.trackApplication policy via Upstash.trackApplicationEventSchema.parse() — Parses and validates the request body for eventName, url, and referrer.Warning
Bot requests bypass database queries and rate-limit checks entirely, returning an HTTP 202 status code to prevent bot traffic from polluting marketplace application analytics.
After payload validation, the pipeline identifies the target program and dispatches lifecycle events based on the requested event name.
The server-side lead attribution engine processes direct API requests, client SDK telemetry, and internal authentication flows to attribute lead conversion events to click IDs, resolve customer profiles, and trigger partner commissions and webhooks. The core HTTP endpoint is mounted at POST /api/track/lead, requiring authenticated workspace membership with business, advanced, or enterprise plans.
Sources: apps/web/app/ee/api/track/lead/route.ts:9-11, apps/web/app/ee/api/track/lead/route.ts:61-65
Incoming payloads are parsed against a Zod validation schema that normalizes fields and handles legacy parameter naming for backwards compatibility.
Warning
The resolver checks customerExternalId, externalId, and customerId in sequential fallback order (newExternalId || oldExternalId || oldCustomerId). If all three resolve to a nullish value, a bad_request DubApiError is thrown immediately.
The lead attribution engine executes through a sequential pipeline from request handling to asynchronous post-processing.
Sources: apps/web/app/ee/api/track/lead/route.ts:12-59, apps/web/lib/api/conversions/track-lead.ts:112-321
Dub tracks authentication-triggered sign-up leads internally via trackDubLead(). This utility retrieves the visitor tracking cookie (dub_id), invokes the underlying SDK tracking method with a "Sign Up" event name, and subsequently purges tracking cookies.
export const trackDubLead = async (user: User) => {
const cookieStore = await cookies();
const clickId = cookieStore.get("dub_id")?.value;
if (!clickId) {
console.log("No dub_id cookie found, skipping lead tracking...");
return;
}
// send the lead event to Dub
await dub.track.lead({
clickId,
eventName: "Sign Up",
customerExternalId: user.id,
customerName: user.name,
customerEmail: user.email,
customerAvatar: user.image,
});
// delete the cookies
cookieStore.delete("dub_id");
cookieStore.delete("dub_partner_data");
};Note
OpenAPI path definitions register /track/lead under the trackPaths dictionary mapping directly to the POST handler implementation for automated API documentation generation.
The sale attribution engine handles conversion processing for revenue events, currency normalization, first-conversion detection, and partner commission triggers. Incoming sale requests are processed via the protected workspace endpoint at POST /api/track/sale, which requires workspace authentication and specific plan tiers (business, advanced, or enterprise) with owner or member roles.
When a sale request hits the API route, parameters are validated using Zod schemas supporting backward compatibility aliases (customerExternalId, externalId, and customerId). The execution pipeline flows through core validation, currency conversion, and event recording.
Sources: apps/web/app/ee/api/track/sale/route.ts:10-63, apps/web/lib/api/conversions/track-sale.ts:469-531
The sale ingestion payload accepts several standard and financial attributes to record and attribute revenue events accurately.
Sources: apps/web/app/ee/api/track/sale/route.ts:14-36, apps/web/lib/api/conversions/track-sale.ts:469-485
Warning
If the transaction amount is less than or equal to 0, the sale tracking function immediately bypasses event recording and returns a null sale object, preventing zero-value or negative revenue pollution in analytics.
Sources: apps/web/app/ee/api/track/sale/route.ts:41-45, apps/web/lib/api/conversions/track-sale.ts:493-500
Non-USD currencies are automatically normalized prior to sale persistence. When currency !== "usd", the engine invokes convertCurrency to compute the converted amount and standardized currency code. Commission generation sources default to CommissionSource.api when manual or automated sales are processed through tracking routes.
if (currency !== "usd") {
const { currency: convertedCurrency, amount: convertedAmount } =
await convertCurrency({
currency,
amount,
});
currency = convertedCurrency;
amount = convertedAmount;
}Note
OpenAPI path definitions map /track/sale within the trackPaths schema dictionary directly to the POST sale tracking endpoint handler.
Conversion attribution is synchronized from external services via dedicated ingestion webhooks and client-side tracking pixels. The platform verifies requests against provider IP ranges or security signatures before processing conversions from AppsFlyer, Singular, Shopify, and Stripe.
Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:26-49, apps/web/app/ee/api/singular/webhook/route.ts:37-52, apps/web/app/ee/api/shopify/pixel/route.ts:20-82
Both AppsFlyer and Singular ingest postback events via GET routes. The AppsFlyer webhook validates client IPs against APPSFLYER_IP_RANGES, parses the appId and partnerEventId, and matches the installation via Prisma. Singular maps incoming event names through singularToDubEvent before dispatching leads or sales.
const singularToDubEvent = {
activated: "lead",
sng_complete_registration: "lead",
sng_subscribe: "sale",
sng_ecommerce_purchase: "sale",
__iap__: "sale", // In-app purchase
"Copy GAID": "lead", // Singular Device Assist
"copy IDFA": "lead", // Singular Device Assist
};Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:37-79, apps/web/app/ee/api/singular/webhook/route.ts:15-23, apps/web/app/ee/api/singular/webhook/route.ts:40-107
The Shopify pixel endpoint receives client-side pixel events (clickId and checkoutToken), enforces rate limiting via Upstash, validates the click event, and caches the association in shopifyCheckoutCache before triggering the order processing job.
Warning
If either checkoutToken or clickId is missing from the incoming Shopify pixel payload, the request is immediately acknowledged with an OK response and skipped without performing attribution.
Stripe webhooks synchronize customer records and handle checkout completions. When processing customer creation or updates via syncCustomer, the engine checks metadata for dubClickId and an external ID. If a customer checks out using a promotion code without an existing attribution link, attributeViaPromotionCodeId resolves the Stripe promotion code ID to a discount code in Dub, records a fake click event, and provisions the customer and lead event.
export async function attributeViaPromotionCodeId({
promotionCodeId,
workspace,
mode,
customerDetails,
}: {
promotionCodeId: string;
workspace: Pick<
Project,
"id" | "defaultProgramId" | "stripeConnectId" | "webhookEnabled"
>;
mode: StripeMode;
customerDetails: PromoCodeCustomerDetails;
}) {
const promotionCode = await getPromotionCode({
promotionCodeId,
stripeAccountId: workspace.stripeConnectId!,
mode,
});
// ... resolves discountCode, records fake click, and creates customer
}Sources: apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:22-86, apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-70
Sources: apps/web/app/ee/api/appsflyer/webhook/route.ts:37-49, apps/web/app/ee/api/singular/webhook/route.ts:40-52, apps/web/app/ee/api/shopify/pixel/route.ts:21-54, apps/web/app/ee/api/stripe/integration/webhook/utils/sync-customer.ts:31-62, apps/web/app/ee/api/stripe/integration/webhook/utils/attribute-via-promotion-code-id.ts:30-80
The Google Ads integration handles server-side conversion uploads and click conversion synchronization by communicating with the Google Ads API and Data Manager service. When a conversion payload is processed, the system extracts advertising identifiers such as gclid, gbraid, or wbraid from click URLs, currency units are normalized, and jobs are queued or executed against Google Ads endpoints.
Sources: apps/web/lib/integrations/google-ads/api.ts:9-25, apps/web/lib/integrations/google-ads/upload-conversion.ts:21-47
Conversion uploads follow a structured execution path from inbound requests down to API data ingestion. The operation proceeds through the following call chain:
queueGoogleAdsConversionUpload() → qstash.publishJSON() → POST /api/google-ads/upload-conversion → uploadGoogleAdsConversion() → GoogleAdsApi.uploadClickConversion() → dataManagerFetch()
queueGoogleAdsConversionUpload() validates that the click URL contains a valid click identifier (gclid, gbraid, or wbraid) and that the workspace has the Google Ads integration installed. It adjusts non-zero-decimal currency values and publishes a payload via QStash with a deduplication ID.POST /api/google-ads/upload-conversion receives the cron-triggered payload and delegates execution to uploadGoogleAdsConversion().uploadGoogleAdsConversion() retrieves installed integration settings from Prisma, matches the event name against lead or sale conversion mappings, and initializes a GoogleAdsApi client instance using OAuth tokens.GoogleAdsApi.uploadClickConversion() constructs operating account destinations, ad identifiers, and event metadata (formatting timestamps via formatGoogleAdsEventTimestamp), and executes the upload through dataManagerFetch() targeting the events:ingest path.Sources: apps/web/lib/integrations/google-ads/upload-conversion.ts:49-210, apps/web/app/ee/api/google-ads/upload-conversion/route.ts:8-13, apps/web/lib/integrations/google-ads/api.ts:443-503
Warning
New integrations must utilize the Data Manager API (events:ingest) rather than the legacy ConversionUploadService.UploadClickConversions method when uploading offline click conversions.
When listing conversion actions or authenticating requests across manager hierarchies, the system evaluates candidate login customer IDs using getLoginCustomerIdCandidates() to avoid permission errors.
const candidates = getLoginCustomerIdCandidates({
customers: currentSettings.customers,
selectedCustomerId: customerId,
loginCustomerId: currentSettings.loginCustomerId,
});Sources: apps/web/app/ee/api/google-ads/conversion-actions/route.ts:57-61, apps/web/lib/integrations/google-ads/api.ts:541-595
Sources: apps/web/lib/integrations/google-ads/api.ts:415-440, apps/web/lib/integrations/google-ads/api.ts:443-503
Tip
During upload execution, the system retries failed requests up to three times with exponential backoff (1000 * Math.pow(2, attempt)) before marking the conversion upload as failed.
Customer reattribution workflows allow moving customer entities and reconciling their event histories between distinct identifiers, while historical analytics mechanisms retrieve and parse customer conversion events from Tinybird and MySQL.
The customer reattribution lifecycle manages stub verification, event planning, and transactional record recreation. isReattributedCustomerStub() validates whether a customer record is a stub by checking prefix patterns against externalId (reattributed_, dummy_, retired_) alongside null checks for partnerId, linkId, and programId. getCustomerReattributeEvents() fetches events for a given customer ID via getCustomerEventsTB() up to CUSTOMER_REATTRIBUTION_EVENTS_LIMIT (500), filtering for entries where the event property is strictly "click", "lead", or "sale". loadReattributeEventPlan() evaluates old and new customer events to build an event plan capturing click existence, lead counts, sale counts, total sale amounts, and timestamps.
export async function loadReattributeEventPlan({
oldCustomerId,
newCustomerId,
}: {
oldCustomerId: string;
newCustomerId: string;
}): Promise<ReattributeEventPlan> {
const [oldEvents, newEvents] = await Promise.all([
getCustomerReattributeEvents(oldCustomerId),
getCustomerReattributeEvents(newCustomerId),
]);
if (oldEvents.length >= CUSTOMER_REATTRIBUTION_EVENTS_LIMIT) {
throw new Error(
`Customer ${oldCustomerId} has too many events to reattribute (limit ${CUSTOMER_REATTRIBUTION_EVENTS_LIMIT}).`,
);
}
const sourceEvents = oldEvents.length > 0 ? oldEvents : newEvents;
const clickEvent = sourceEvents.find((event) => event.event === "click");
const leadEvent = sourceEvents.find((event) => event.event === "lead");
const saleEvents = sourceEvents.filter((event) => event.event === "sale");
return {
hasClick: Boolean(clickEvent),
hasLead: Boolean(leadEvent),
leadCount: leadEvent ? 1 : 0,
saleCount: saleEvents.length,
saleAmount: saleEvents.reduce(
(sum, event) => sum + (event.saleAmount ?? 0),
0,
),
leadTimestamp: leadEvent?.timestamp ?? null,
saleTimestamp: saleEvents[saleEvents.length - 1]?.timestamp ?? null,
};
}Warning
If a customer exceeds CUSTOMER_REATTRIBUTION_EVENTS_LIMIT (500 events) during reattribution planning, an error is thrown to prevent unbounded payload processing and memory exhaustion.
The Framer batch backfill cron endpoint (POST /api/cron/framer/backfill-leads-batch) validates workspace authorization against FRAMER_WORKSPACE_ID (clsvopiw0000ejy0grp821me0), parses inbound request payloads via Zod, and queries Tinybird pipes alongside Prisma databases to process historical lead and sale events.
export const POST = withWorkspace(
async ({ req, workspace }) => {
try {
if (workspace.id !== FRAMER_WORKSPACE_ID) {
throw new DubApiError({
code: "unauthorized",
message: "Unauthorized",
});
}
const originalPayload = schema.parse(
await parseRequestBody(req),
) as PayloadItem[];Tip
Background updates to link statistics during batch backfills are delegated via Vercel's waitUntil() helper function to ensure immediate HTTP response return without blocking client connections.
getCustomerEvents() aggregates customer telemetry by querying Tinybird event data and hydrating link records from MySQL via getLinksMap(). It normalizes timestamps to UTC, maps processed regional and referer fields, parses click schemas, decodes case-sensitive links, and parses lead or sale metadata depending on the specific event type.
Sources: apps/web/lib/analytics/get-customer-events.ts:13-84, apps/web/lib/api/customers/reattribute-customer.ts:98-171, apps/web/lib/api/customers/reattribute-customer.ts:204-238