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 provides a comprehensive audit logging and activity tracking subsystem designed to record, ingest, persist, and export operational changes across workspaces, partner programs, and resources. By combining Tinybird analytics pipes for high-throughput event streaming with relational Prisma models for structured resource state transitions, the system captures administrative actions, partner enrollments, reward overrides, commission updates, and inbound webhook or API requests. This architecture ensures strict compliance, transparent audit trails, and robust diagnostic visibility for enterprise workspaces. Sources: apps/web/lib/api/audit-logs/get-audit-logs.ts:1-53, apps/web/lib/api/audit-logs/record-audit-log.ts:1-80, apps/web/prisma/schema/activity.prisma:1-20
Workspace audit events are ingested, enriched, and dispatched to Tinybird through a dedicated pipeline that captures administrative and programmatic actions across Dub workspaces. When an event is recorded via recordAuditLog(), the function inspects incoming HTTP requests to extract client IP addresses and user agents, transforms the payload into the required schema format with prefixed identifiers, and sends the batch or single event to the Tinybird ingestion endpoint (dub_audit_logs). Conversely, retrieval operations query Tinybird pipes using getAuditLogs() with date-range filters and prefixed workspace identifiers. Sources: apps/web/lib/api/audit-logs/get-audit-logs.ts:1-52, apps/web/lib/api/audit-logs/record-audit-log.ts:1-80
The recording and dispatch of audit logs follow a strict execution path from input validation to upstream delivery:
recordAuditLog(data) — Accepts single AuditLogInput objects or arrays, retrieves HTTP headers via headers(), and resolves client IP addresses using either Vercel's getIPAddress(dataReq) or fallback getIP(). Sources: apps/web/lib/api/audit-logs/record-audit-log.ts:47-53transformAuditLogTB(data, context) — Parses input against recordAuditLogInputSchema, extracts user-agent headers, generates unique identifiers via createId({ prefix: "audit_" }), formats ISO timestamps, prefixes workspace identifiers with prefixWorkspaceId(), and serializes targets and metadata objects into JSON strings. Sources: apps/web/lib/api/audit-logs/record-audit-log.ts:13-45recordAuditLogTB(auditLogs) — Built using tb.buildIngestEndpoint(), this function dispatches the transformed payloads to the dub_audit_logs Tinybird datasource with wait: true. If transmission fails, errors are logged to console and reported via the internal error handler log(). Sources: apps/web/lib/api/audit-logs/record-audit-log.ts:58-79Warning
If recordAuditLogTB throws an exception during Tinybird ingestion, the failure is caught locally, logged to the console alongside the serialized audit payload, and dispatched to the internal notification system via log(). This prevents audit logging failures from crashing parent API mutations while ensuring administrative visibility into ingestion outages. Sources: apps/web/lib/api/audit-logs/record-audit-log.ts:58-72
The ingestion schema enforces strict typing for stored audit attributes. Every record contains standard tracking properties mapped to Tinybird data columns. Sources: apps/web/lib/api/audit-logs/schemas.ts:15-30
Enterprise audit log querying and export relies on Tinybird pipes, Zod schema validation, and specialized conversion utilities to package workspace activity for compliance. The querying layer validates incoming filters through auditLogFilterSchemaTB, which prefixes workspace identifiers and extracts date bounds before executing queries against Tinybird pipes via getAuditLogs. Sources: apps/web/lib/api/audit-logs/get-audit-logs.ts:6-52
The export route POST /api/audit-logs/export orchestrates compliance downloads by parsing request bodies for start and end parameters, enforcing enterprise plan capabilities, and streaming formatted CSV output. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:10-60
The execution proceeds through a strict call chain:
parseRequestBody(req) extracts JSON payloads containing start and end dates validated by auditLogExportQuerySchema. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:18-20getPlanCapabilities(workspace.plan) evaluates whether canExportAuditLogs is enabled for the workspace plan, throwing a DubApiError with code forbidden if unauthorized. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:29-36getDefaultProgramIdOrThrow(workspace) retrieves the active program identifier required for log retrieval. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:38-38getAuditLogs(...) invokes Tinybird pipe get_audit_logs with UTC-formatted date strings. Sources: apps/web/lib/api/audit-logs/get-audit-logs.ts:38-51, apps/web/app/ee/api/audit-logs/export/route.ts:40-45convertToCSV(auditLogs) serializes the returned event records into CSV format, returned with headers Content-Type: application/csv and Content-Disposition: attachment;. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:47-54Warning
Requests to /api/audit-logs/export without enterprise plan capabilities trigger a forbidden API error. The client-side UI component AuditLogs disables date pickers and export triggers when canExportAuditLogs evaluates to false, rendering an upgrade CTA banner instead. Sources: apps/web/app/ee/api/audit-logs/export/route.ts:29-36, apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/audit-logs.tsx:81-129
The audit log retrieval interface validates both filter inputs and response row shapes using Zod v4. Sources: apps/web/lib/api/audit-logs/get-audit-logs.ts:3-25
The client component AuditLogs manages state for date ranges and export loading spinners inside the workspace security settings view (WorkspaceSecurityClient). Sources: apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/audit-logs.tsx:12-66, apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/page-client.tsx:7-15
export async function exportAuditLogs() {
const response = await fetch(
`/api/audit-logs/export?workspaceId=${workspaceId}`,
{
method: "POST",
body: JSON.stringify({ start, end }),
headers: { "Content-Type": "application/json" },
},
);
if (!response.ok) {
const { error } = await response.json();
throw new Error(error.message);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `Dub Audit Logs Export - ${new Date().toISOString()}.csv`;
a.click();
}The relational activity log module persists workspace state transitions, partner modifications, and resource actions using a Prisma schema backed by tracking helper utilities. It filters out unchangeable records and supports batch creation both standalone and within transactional client boundaries. Sources: apps/web/prisma/schema/activity.prisma:1-20, apps/web/lib/api/activity-log/track-activity-log.ts:1-93
The ActivityLog Prisma model defines fields for tracking identifiers, resource scopes, change sets, and user relations. Sources: apps/web/prisma/schema/activity.prisma:1-20
Note
The table configuration includes composite indexing on [resourceType, resourceId] alongside a standalone index on userId to optimize relational lookup performance. Sources: apps/web/prisma/schema/activity.prisma:18-19
The activity tracking subsystem processes single objects or arrays, filters out logs that lack required change sets unless whitelisted, and executes batch persistence. Sources: apps/web/lib/api/activity-log/track-activity-log.ts:34-65
const ACTIONS_WITHOUT_CHANGE_SET: ActivityLogAction[] = [
"submittedLead.created",
"reward.created",
"reward.deleted",
];Call-chain execution for standalone activity logging: trackActivityLog() → array coercion (Array.isArray) → inputs.filter() checking ACTIONS_WITHOUT_CHANGE_SET or populated changeSet → length check abort (if (inputs.length === 0) return) → prisma.activityLog.createMany() mapping json payloads → console logging or logger.error catch block with logger.flush(). Sources: apps/web/lib/api/activity-log/track-activity-log.ts:34-65
Warning
If an input action is not present in ACTIONS_WITHOUT_CHANGE_SET, an empty or missing changeSet object will cause the helper to filter out and discard the log entry entirely before reaching the database. Sources: apps/web/lib/api/activity-log/track-activity-log.ts:39-43
When operations must run atomically within caller transactions, trackActivityLogsTx() accepts an active Prisma.TransactionClient instance (tx) and performs identical validation before executing tx.activityLog.createMany(). Sources: apps/web/lib/api/activity-log/track-activity-log.ts:68-93
Resource types and actions are strictly constrained by Zod v4 schemas. Resource types include partner, commission, clickReward, saleReward, leadReward, referralReward, customReward, and submittedLead. Sources: apps/web/lib/zod/schemas/activity-log.ts:4-13
Sources: apps/web/prisma/schema/activity.prisma:1-20, apps/web/lib/api/activity-log/track-activity-log.ts:11-93, apps/web/lib/zod/schemas/activity-log.ts:57-60
Partner and commission change tracking manages state transitions across rewards, partner discount overrides, link-level customizations, and commission adjustments. The logging pipeline generates structured snapshots and computes field-level differences using dedicated utility functions before recording entries to the database. Sources: apps/web/lib/api/activity-log/track-reward-overrides.ts:1-57, apps/web/lib/api/commissions/track-commission-update-activity-log.ts:1-30, apps/web/lib/api/activity-log/track-reward-activity-log.ts:1-32
Commission tracking operates on arrays of old and new commission records containing identifier, amount, earnings, and status fields. The trackCommissionActivityLog function coordinates this process through explicit step sequencing. Sources: apps/web/lib/api/commissions/track-commission-update-activity-log.ts:31-75
Call-chain execution for commission updates: trackCommissionActivityLog() → id extraction & sorting (oldById, newById, commissionIds) → toCommissionActivitySnapshot() → getResourceDiff() evaluating COMMISSION_ACTIVITY_FIELDS (amount, earnings, status) → conditional activity log push with action commission.updated → trackActivityLog(). Sources: apps/web/lib/api/commissions/track-commission-update-activity-log.ts:19-75
export async function trackCommissionActivityLog({
old: oldCommissions,
new: newCommissions,
...baseInput
}: TrackActivityLogParams) {
const activityLogs: TrackActivityLogInput[] = [];
const oldById = new Map((oldCommissions ?? []).map((c) => [c.id, c]));
const newById = new Map((newCommissions ?? []).map((c) => [c.id, c]));
const commissionIds = [
...new Set([...oldById.keys(), ...newById.keys()]),
].sort();
for (const id of commissionIds) {
const oldCommission = oldById.get(id);
const newCommission = newById.get(id);
if (oldCommission && newCommission) {
const oldSnapshot = toCommissionActivitySnapshot(oldCommission);
const newSnapshot = toCommissionActivitySnapshot(newCommission);
const diff = getResourceDiff(oldSnapshot, newSnapshot, {
fields: COMMISSION_ACTIVITY_FIELDS,
});
if (diff) {
activityLogs.push({
...baseInput,
resourceId: newCommission.id,
resourceType: "commission",
action: "commission.updated",
changeSet: {
commission: {
old: oldSnapshot,
new: newSnapshot,
},
},
});
}
}
}
return await trackActivityLog(activityLogs);
}Bulk status changes leverage wrapper utilities. trackCommissionStatusUpdate applies a uniform CommissionStatus across a filtered commission set, whereas trackCommissionStatusUpdatesByProgram groups commissions by program ID and resolves the corresponding workspace from associated payout configurations before delegation. Sources: apps/web/lib/api/commissions/track-commission-update-activity-log.ts:77-142
Note
trackCommissionStatusUpdatesByProgram will log an error to the console and skip processing for any program whose workspace ID cannot be resolved from the provided payout records. Sources: apps/web/lib/api/commissions/track-commission-update-activity-log.ts:127-134
Reward activity tracking handles creation, updates, deletions, and conditional modifier changes. The helper trackRewardActivityLog determines resource types using REWARD_EVENT_TO_RESOURCE_TYPE mapped from the reward event property. Sources: apps/web/lib/zod/schemas/activity-log.ts:71-77, apps/web/lib/api/activity-log/track-reward-activity-log.ts:9-143
Modifier changes within rewards are evaluated by buildModifierChangeSetEntries, which maps old and new modifier arrays by identifier and inspects equality via modifierEquals (performing stringified comparison). Changes produce distinct actions: reward.conditionAdded, reward.conditionUpdated, or reward.conditionRemoved. Sources: apps/web/lib/api/activity-log/track-reward-activity-log.ts:34-125
Warning
If a description is provided alongside multiple reward activity logs (such as an update accompanied by modifier adjustments), the description is assigned exclusively to the primary lifecycle log index (reward.created, reward.updated, or reward.deleted), defaulting to index 0 if no primary action is found. Sources: apps/web/lib/api/activity-log/track-reward-activity-log.ts:227-239
Partner reward and discount overrides are tracked via trackPartnerRewardOverrideLog (executing within an active Prisma transaction) and trackLinkRewardOverrideLog (executing independently via prisma). Both functions inspect reward fields (clickReward, leadReward, saleReward) and discount changes. Sources: apps/web/lib/api/activity-log/track-reward-overrides.ts:30-193
If reward identifiers or discount identifiers change, the respective records are queried in batch using Promise.all, serialized into activity snapshots, and committed as partner.rewardChanged or partner.discountChanged activity log entries. Sources: apps/web/lib/api/activity-log/track-reward-overrides.ts:44-178
Frontend integration of audit trails and partner activity feeds relies on specialized SWR hooks, slide-over sheet drawers, and dedicated action renderers. Activity log data is retrieved from workspace and partner-profile endpoints using query parameter filters for resource type, resource ID, parent resource ID, and specific actions. Sources: apps/web/app/api/activity-logs/route.ts:12-34, apps/web/lib/swr/use-activity-logs.ts:6-34, apps/web/lib/swr/use-partner-activity-logs.ts:6-31
Data retrieval for activity logs is implemented via modular hooks that handle conditional execution and parameter serialization. The workspace-level useActivityLogs hook verifies workspace resolution and query requirements before invoking the fetcher with serialized URL search parameters, preserving previous data across updates. Sources: apps/web/lib/swr/use-activity-logs.ts:6-34
Sources: apps/web/lib/swr/use-activity-logs.ts:6-42, apps/web/lib/swr/use-partner-activity-logs.ts:6-39, apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:52-93
Partner history and audit events can be inspected via slide-over sheets powered by PartnerEnrollmentHistorySheet. The sheet component manages visibility state synchronized with URL search parameters via useRouterStuff, adding or deleting the history parameter. Sources: apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:41-93
The UI call sequence proceeds as follows:
usePartnerEnrollmentHistorySheet() evaluates the history search parameter → setIsOpen() updates URL parameters via queryParams({ set: { history: "true" } }) → PartnerEnrollmentHistorySheet renders the sheet container → PartnerEnrollmentActivitySection invokes useActivityLogs with resource type partner → ActivityFeed displays the returned log entries. Sources: apps/web/ui/activity-logs/partner-enrollment-activity-section.tsx:9-45, apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:41-93
Note
The partner profile activity log endpoint at apps/web/app/(ee)/api/partner-profile/programs/[programId]/activity-logs/route.ts explicitly overrides workspace user data by mapping retrieved activity logs to set user: null, preventing workspace operators' user profiles from being exposed to partners. Sources: apps/web/app/ee/api/partner-profile/programs/programId/activity-logs/route.ts:63-68
Specific activity actions are interpreted by UI action renderers. For instance, PartnerGroupChangedRenderer handles partner.groupChanged actions by retrieving available workspace groups via useGroups, extracting the target group change set, and rendering dynamic labels and pills depending on whether an old group exists and whether a user or an automated group move triggered the event. Sources: apps/web/ui/activity-logs/action-renderers/partner-group-changed-renderer.tsx:18-48
Incoming webhook deliveries and API requests are captured and persisted for diagnostic auditability using Tinybird pipes and ingestion endpoints. Request metadata, duration, response bodies, and routing patterns are normalized and recorded to the dub_api_logs datasource with robust retry logic. Sources: apps/web/lib/api-logs/capture-webhook-log.ts:16-51, apps/web/lib/api-logs/record-api-log.ts:30-93
The recording execution flow proceeds as follows: captureWebhookLog() or external API handlers call parseResponseBody() to clone and extract JSON response payloads → recordApiLog() constructs an ApiLogInput object with a generated req_ ID, ISO timestamp, workspace ID, path sanitization (/api/ prefix replacement), and JSON-serialized request/response bodies → recordApiLogTB() invokes the Tinybird ingest endpoint via tb.buildIngestEndpoint with wait: true and dub_api_logs datasource → on network or ingestion failure, the loop sleeps with exponential backoff (100 * Math.pow(2, attempt) for up to 3 retries) before logging errors to Axiom or console. Sources: apps/web/lib/api-logs/capture-webhook-log.ts:4-50, apps/web/lib/api-logs/record-api-log.ts:8-92
export const recordApiLog = async ({
workspaceId,
method,
path,
routePattern,
statusCode,
duration,
userAgent,
requestBody,
queryParams,
responseBody,
tokenId,
userId,
requestType,
}: RecordApiLogParams) => {
const apiLog: ApiLogInput = {
id: createId({ prefix: "req_" }),
timestamp: new Date().toISOString(),
workspace_id: workspaceId,
method,
path: path.replace("/api/", "/"),
route_pattern: routePattern,
status_code: statusCode,
duration,
user_agent: userAgent ?? "",
request_body: JSON.stringify(requestBody),
query_params: queryParams ? JSON.stringify(queryParams) : "",
response_body: JSON.stringify(responseBody),
token_id: tokenId ?? "",
user_id: userId ?? "",
request_type: requestType,
};
return await recordApiLogTB(apiLog);
};Warning
If all retry attempts fail when recording an API log, the failure is caught, checked against process.env.CI, and logged to the internal error monitoring service via log(), ensuring that logging failures do not crash the primary request lifecycle. Sources: apps/web/lib/api-logs/record-api-log.ts:69-92
Workspace operators query recorded logs via /api/logs and /api/logs/[logId] endpoints protected by withWorkspace middleware requiring workspaces.read permissions. Retrieved logs are validated against workspace plan retention rules and enriched before returning JSON responses. Sources: apps/web/app/api/logs/route.ts:8-40, apps/web/app/api/logs/logId/route.ts:9-45