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:
The Tinybird Analytics Engine powers high-performance event ingestion, real-time conversion tracking, and multi-dimensional timeseries aggregation across clicks, leads, and sales. Built around Tinybird data sources and pipes, it ingests high-volume click streams, attribute conversions, and synchronizes metadata to drive analytics and reporting dashboards.
Sources: apps/web/lib/tinybird/record-lead.ts:5-8, apps/web/lib/tinybird/record-sale.ts:5-8, apps/web/lib/analytics/get-analytics.ts:99-123
The analytics subsystem relies on Tinybird builder utilities to instantiate ingestion endpoints and query pipes, routing streaming click data, webhook notifications, postback events, and customer timeline queries through strongly-typed Zod schemas.
Sources: apps/web/lib/tinybird/record-click-zod.ts:1-44, apps/web/lib/tinybird/record-webhook-event.ts:1-7, apps/web/lib/postback/record-postback-event.ts:1-7, apps/web/lib/tinybird/get-customer-events-tb.ts:1-25
Tinybird ingestion and query pipelines are constructed via tb.buildIngestEndpoint and tb.buildPipe methods, mapping structured domain models to specific data sources and pipes.
Sources: apps/web/lib/tinybird/record-click-zod.ts:39-43, apps/web/lib/tinybird/record-webhook-event.ts:4-7, apps/web/lib/postback/record-postback-event.ts:4-7, apps/web/lib/tinybird/get-customer-events-tb.ts:4-8
The customer timeline query flow wraps the underlying Tinybird pipe call inside an asynchronous helper function (getCustomerEventsTB) that accepts customer identifiers, optional link filters, and result constraints.
export const getCustomerEventsTB = async ({
customerId,
linkIds,
limit,
}: {
customerId: string;
linkIds?: string[];
limit?: number;
}) => {
return await pipe({
customerId,
...(linkIds ? { linkIds } : {}),
...(limit ? { limit } : {}),
});
};Note
Ingestion endpoints such as recordClickZod explicitly configure synchronous waiting (wait: true), ensuring that event payloads validated by recordClickZodSchema are confirmed upon insertion into the dub_click_events data source.
Sources: apps/web/lib/tinybird/record-click-zod.ts:39-43
The click recording subsystem manages incoming link requests through validation checks, bot filtration, QR code detection, metadata enrichment, and asynchronous background buffering. Handled primarily by recordClick in apps/web/lib/tinybird/record-click.ts and validated via recordClickZod using recordClickZodSchema in apps/web/lib/tinybird/record-click-zod.ts, the pipeline processes incoming click requests while applying rate-limiting deduplication via Redis.
Sources: apps/web/lib/tinybird/record-click.ts:1-236, apps/web/lib/tinybird/record-click-zod.ts:1-44
When a click request arrives, the recordClick function executes an explicit sequence of validation and enrichment steps before scheduling asynchronous persistence:
!clickId check: Validates that a clickId is present; returns null if absent.dub-no-track header or query check: Inspects request headers and search parameters for the tracking opt-out flag.detectBot(req): Evaluates bot signatures against incoming user-agent data, skipping bot verification if trigger === "deeplink".getIdentityHash(req): Computes an identity hash representing the client fingerprint for rate-limiting.recordClickCache.get(...): Checks Redis to deduplicate clicks for the domain and key pair from the same IP address within a one-hour window.detectQr(req): Checks if the request originated from a QR code scan, updating trigger to "qr".continent, country, region, city, latitude, longitude, vercel_region), IP address, user-agent parsing (device, browser, OS, CPU architecture), and referrer information.waitUntil(...): Dispatches asynchronous background tasks including Tinybird event ingestion, Redis cache updates, and Redis stream event publishing.Warning
If skipRatelimit is false, a cache hit in recordClickCache causes the function to return null immediately without recording a click. If Redis throws an error during this check, the function catches the exception and returns null to prevent overwhelming Tinybird or MySQL.
Sources: apps/web/lib/tinybird/record-click.ts:84-100
The ingested click payload conforms to recordClickZodSchema, which defines default fallback values for geographic, device, and request properties.
Once click metadata is compiled, recordClick buffers and persists events concurrently in the background using waitUntil and Promise.allSettled. The asynchronous block executes four parallel operations:
${process.env.TINYBIRD_API_URL}/v0/events?name=dub_click_events&wait=true authenticated via Bearer token.recordClickCache.set(...) to store the click identifier in Redis for 1 hour, preventing duplicate clicks from the same identity hash.publishLinkClickEvent(...).clickData payload via publishWorkspaceClickEvent(clickData).Tip
If shouldCacheClickId is enabled, the raw clickData object is cached directly in Redis under clickIdCache:${clickId} with a 5-minute expiration (ex: 60 * 5). This bridges the ingestion latency gap before newly recorded clicks become queryable directly inside Tinybird.
Sources: apps/web/lib/tinybird/record-click.ts:163-167
Conversion tracking handles lead and sale events by routing incoming requests through server-side and client-side API endpoints, enforcing workspace authentication, validating payloads with Zod schemas, attributing events to existing or new customers, and recording metrics into Tinybird data sources. The core pipeline resolves customer identifiers, handles click lookups, enforces deduplication via Redis, and fans out side effects including partner commissions, workflows, webhooks, and conversion uploads.
Sources: apps/web/app/ee/api/track/lead/route.ts:1-65, apps/web/lib/api/conversions/track-lead.ts:34-407
Lead and sale events are pushed to Tinybird datasources using endpoints built with tb.buildIngestEndpoint. Each ingestion handler supports standard event records as well as variant payloads featuring explicit timestamp strings.
When a lead conversion request arrives at either the server-side API (POST /api/track/lead) or client-side API (POST /api/track/lead/client), control passes through authentication wrappers and validation layers before executing trackLead().
The trackLead operation executes the following call chain:
prisma.customer.findUnique(...) — Queries the database to locate any existing customer record matching the workspace and customerExternalId.redis.set(...) — If mode is not deferred, attempts to set a 1-week Redis key trackLead:${workspace.id}:${customerExternalId}:${stringifiedEventName} with nx: true for event deduplication. If res === null, isDuplicateEvent is marked true.getClickEvent(...) — Retrieves click metadata associated with the resolved clickId.prisma.link.findUnique(...) — Fetches the referral link to verify ownership, active status (disabledAt), and workspace alignment.getOrCreateCustomer(...) — If no customer record exists in PostgreSQL, provisions a new customer linked to the click, partner program, and country data.redis.set(...) (Wait Mode) — If mode === "wait", caches the lead event payload in Redis for 5 minutes under leadCache:${customer.id} and leadCache:${customer.id}:${stringifiedEventName} to bridge Tinybird ingestion latency.recordLead(...) — Invokes the Tinybird ingestion endpoint in a waitUntil background block (unless mode === "deferred").prisma.link.update(...), prisma.project.update(...), queuePartnerCommissionCreation(...), executeWorkflows(...), syncPartnerLinksStats(...), sendWorkspaceWebhook(...), queueGoogleAdsConversionUpload(...), and sendPartnerPostback(...).Warning
If a request omits clickId, trackLead requires an existing customer record linked to the provided customerExternalId in order to inherit its stored clickId. If neither is found, the operation immediately throws a bad_request DubApiError.
Sources: apps/web/lib/api/conversions/track-lead.ts:60-72
Client-side lead tracking (POST /api/track/lead/client) is protected by publishable keys and verifies that request origins comply with workspace configurations.
export const POST = withPublishableKey(
async ({ req, workspace }) => {
const body = await parseRequestBody(req);
const allowRequest = verifyAnalyticsAllowedHostnames({
allowedHostnames: (workspace?.allowedHostnames ?? []) as string[],
req,
});
if (!allowRequest) {
throw new DubApiError({
code: "forbidden",
message: `Request origin '${getHostnameFromRequest(req)}' is not included in the allowed hostnames for this workspace.`,
});
}
const parsed = trackLeadRequestSchema.parse(body);
const response = await trackLead({ ...parsed, workspace });
return NextResponse.json(response, { headers: COMMON_CORS_HEADERS });
},
{
requiredPlan: ["business", "advanced", "enterprise"],
},
);Note
Client-side tracking routes automatically respond to OPTIONS preflight requests with COMMON_CORS_HEADERS and a 204 status code to support cross-origin browser requests.
Sources: apps/web/app/ee/api/track/lead/client/route.ts:62-67
The analytics subsystem relies on rigorous Zod schemas, transformation pipelines, and data models to validate and ingest events into Tinybird. Data structures govern lead conversions, link metadata recordings, error logging, and external integration mappings.
Sources: apps/web/lib/zod/schemas/leads.ts:1-147, apps/web/lib/tinybird/record-link.ts:1-90, apps/web/lib/tinybird/log-import-error.ts:1-8, apps/web/lib/integrations/segment/transform.ts:1-137
Incoming tracking requests and database records undergo validation using strongly typed Zod definitions. The trackLeadRequestSchema enforces constraints on fields such as clickId, eventName, customerExternalId, and tracking mode.
Note
Tinybird ingestion schemas omit client-side timestamps so that Tinybird can generate its own authoritative ingestion timestamps. Sources: apps/web/lib/zod/schemas/leads.ts:94-96
The data transformation layer normalizes internal database objects and webhook payloads before dispatching them to Tinybird endpoints or third-party analytics integrations.
const transformLinkTB = (link: ExpandedLink) => {
const key = decodeKeyIfCaseSensitive({
domain: link.domain,
key: link.key,
});
return {
link_id: link.id,
domain: link.domain,
key,
url: link.url,
tag_ids: link.tags?.map(({ tag }) => tag.id) ?? [],
folder_id: link.folderId ?? "",
tenant_id: link.tenantId ?? "",
program_id: link.programId ?? "",
partner_id: link.partnerId ?? "",
partner_group_id: link.programEnrollment?.groupId ?? "",
partner_tag_ids:
link.programEnrollment?.programPartnerTags?.map(
({ partnerTagId }) => partnerTagId,
) ?? [],
workspace_id: link.projectId,
created_at: link.createdAt,
};
};For Segment integrations, webhook payloads are normalized through formatEventForSegment(), mapping event types to target properties:
Caution
Unsupported Segment event types trigger an immediate runtime error (Event ${event} is not supported for Segment.), halting the transformation pipeline.
Sources: apps/web/lib/integrations/segment/transform.ts:31-33
Analytics metrics are retrieved and aggregated through Tinybird pipe queries and MySQL fallback paths. The querying subsystem parses parameters, handles timezones, formats dates for ClickHouse, and executes parameterized data pipelines for timeseries, events, and lead lookups.
Sources: apps/web/lib/analytics/get-analytics.ts:27-196, apps/web/lib/analytics/get-events.ts:35-168
The analytics retrieval functions execute distinct initialization steps before dispatching calls to Tinybird pipes or relational databases.
export const getLeadEvent = async ({
customerId,
eventName,
}: {
customerId: string;
eventName?: string | null;
}) => {
try {
const cachedLeadEvent = await redis.get<LeadEventTB>(
`leadCache:${customerId}${eventName ? `:${eventName.toLowerCase().replaceAll(" ", "-")}` : ""}`,
);
if (cachedLeadEvent) {
return cachedLeadEvent;
}
} catch (_e) {}
try {
const { data } = await getLeadEventTB({ customerId, eventName });
return data[0];
} catch (error) {
console.error(
`[getLeadEvent] Error getting lead event for customerId: ${customerId}${eventName ? ` and eventName: ${eventName}` : ""}`,
error,
);
return null;
}
};Note
getLeadEvent checks Upstash Redis cache using a namespaced key (leadCache:${customerId}...) before falling back to querying the Tinybird get_lead_event pipe.
Sources: apps/web/lib/tinybird/get-lead-event.ts:24-37
Query parameters determine whether requests target optimized MySQL tables (such as all-time link clicks) or dynamic Tinybird pipes (v4_count, v4_timeseries, v4_group_by, v4_events, get_lead_events, get_lead_event).
Sources: apps/web/lib/analytics/get-analytics.ts:54-78, apps/web/lib/analytics/get-analytics.ts:99-123, apps/web/lib/analytics/get-events.ts:77-86, apps/web/lib/tinybird/get-lead-events.ts:5-11, apps/web/lib/tinybird/get-lead-event.ts:7-14
Sources: apps/web/lib/analytics/get-analytics.ts:54-78, apps/web/lib/analytics/get-analytics.ts:99-123, apps/web/lib/analytics/get-events.ts:77-86, apps/web/lib/tinybird/get-lead-events.ts:5-11, apps/web/lib/tinybird/get-lead-event.ts:7-14
Caution
When groupBy === "count", interval === "all", and no custom date ranges or dimensional filters are present, getAnalytics bypasses Tinybird entirely and queries PlanetScale MySQL directly.
Sources: apps/web/lib/analytics/get-analytics.ts:54-78
Operational maintenance across the analytics and event infrastructure handles batch backfills, deduplication, administrative corrections, and event deletion. Backfill scripts orchestrate large-scale data imports by reading source records (such as CSV files or webhook payloads), validating them via Zod schemas, checking existing database entries in PostgreSQL via Prisma, and batch-ingesting time-partitioned payloads into Tinybird datasources. Administrative adjustments correct event attribution by fetching historical records through Tinybird pipes, mutating link identifiers, recording updated timestamps, and issuing deletion conditions against base datasources and materialized views.
Sources: apps/web/app/ee/api/cron/framer/backfill-leads-batch/route.ts:21-75, apps/web/scripts/tinybird/delete-lead-event.ts:4-17, apps/web/scripts/tinybird/update-lead-event.ts:16-57, apps/web/scripts/tinybird/update-sale-event.ts:7-48, apps/web/scripts/customers/beehiiv/update-sale-events.ts:16-54, apps/web/scripts/programs/3-import-customer-leads.ts:23-110
Large-scale customer and lead import operations process data in structured phases. For instance, customer lead imports execute a multi-step sequence: Papa.parse() reads CSV data streams $\rightarrow$ records are filtered against existing Prisma customer and link records $\rightarrow$ click events are mapped with generated nanoid(16) identifiers $\rightarrow$ clicks are grouped by year to satisfy ClickHouse partition limits $\rightarrow$ newline-delimited JSON batches are posted to Tinybird $\rightarrow$ customer records are bulk-created via prisma.customer.createMany with duplicate skipping $\rightarrow$ lead events are recorded via recordLeadWithTimestamp() $\rightarrow$ link statistics are updated and synced via syncPartnerLinksStats().
Warning
ClickHouse enforces a maximum of 12 partitions (months) for a given event backfill operation. Backfill scripts must reduce and group records by calendar year or month before submitting NDJSON payloads toTinybird. Sources: apps/web/scripts/programs/3-import-customer-leads.ts:157-169
Because Tinybird datasources are immutable append-only logs, updating an event (such as migrating a customer lead or sale to a new link ID) requires a two-step mutation pattern: recording the corrected event with its original timestamp, followed by issuing a programmatic delete condition against both the base datasource and its materialized view.
Sources: apps/web/scripts/tinybird/delete-lead-event.ts:5-17, apps/web/scripts/tinybird/update-lead-event.ts:33-57, apps/web/scripts/tinybird/update-sale-event.ts:23-48, apps/web/scripts/customers/beehiiv/update-sale-events.ts:32-52
Sources: apps/web/scripts/tinybird/delete-lead-event.ts:5-39, apps/web/scripts/tinybird/update-lead-event.ts:7-83, apps/web/scripts/tinybird/update-sale-event.ts:4-73, apps/web/scripts/customers/beehiiv/update-sale-events.ts:7-74
Caution
Administrative deletion requests sent to Tinybird endpoints (/v0/datasources/{dataSource}/delete) require application/x-www-form-urlencoded payloads containing a delete_condition parameter. Both base tables and their corresponding _mv materialized views must be updated independently using Promise.allSettled.
Sources: apps/web/scripts/tinybird/delete-lead-event.ts:8-38, apps/web/scripts/tinybird/update-lead-event.ts:44-57