---
title: "Audit Logging"
description: "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..."
last_updated: "2026-10-05T05:07:35.164326+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/audit-logging"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/api/audit-logs/get-audit-logs.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts)
- [apps/web/app/ee/api/audit-logs/export/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/audit-logs.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/audit-logs.tsx)
- [apps/web/app/ee/api/cron/export/events/partner/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/export/events/partner/route.ts)
- [apps/web/app/ee/api/cron/export/events/workspace/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/export/events/workspace/route.ts)
- [apps/web/lib/api/activity-log/track-reward-overrides.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-overrides.ts)
- [apps/web/lib/api/audit-logs/record-audit-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts)
- [apps/web/lib/api/audit-logs/schemas.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts)
- [apps/web/app/ee/api/partner-profile/programs/programId/activity-logs/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/programs/%5BprogramId%5D/activity-logs/route.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/page-client.tsx)
- [apps/web/app/api/activity-logs/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/activity-logs/route.ts)
- [apps/web/lib/api-logs/capture-webhook-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/capture-webhook-log.ts)
- [apps/web/app/api/logs/logId/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/%5BlogId%5D/route.ts)
- [apps/web/lib/swr/use-activity-logs.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-activity-logs.ts)
- [apps/web/app/api/logs/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/route.ts)
- [apps/web/prisma/schema/activity.prisma](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma)
- [apps/web/app/ee/api/shopify/integration/webhook/shop-redact.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/shopify/integration/webhook/shop-redact.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/webhooks/webhookId/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/webhooks/%5BwebhookId%5D/page-client.tsx)
- [apps/web/ui/activity-logs/action-renderers/partner-group-changed-renderer.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/action-renderers/partner-group-changed-renderer.tsx)
- [apps/web/lib/api/activity-log/track-activity-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/logs/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/logs/page.tsx)
- [apps/web/app/ee/api/intercom/webhook/uninstall/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/intercom/webhook/uninstall/route.ts)
- [apps/web/lib/api/commissions/track-commission-update-activity-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts)
- [apps/web/lib/api/activity-log/track-reward-activity-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts)
- [apps/web/ui/activity-logs/partner-enrollment-activity-section.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-activity-section.tsx)
- [apps/web/lib/swr/use-partner-activity-logs.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-partner-activity-logs.ts)
- [apps/web/lib/zod/schemas/activity-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/activity-log.ts)
- [apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx)
- [apps/web/lib/zod/schemas/workspaces.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/workspaces.ts)
- [apps/web/lib/api-logs/record-api-log.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/record-api-log.ts)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L1-L53), [apps/web/lib/api/audit-logs/record-audit-log.ts:1-80](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L1-L80), [apps/web/prisma/schema/activity.prisma:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20)

## Tinybird Audit Log Ingestion

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L1-L52), [apps/web/lib/api/audit-logs/record-audit-log.ts:1-80](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L1-L80)

### Event Ingestion and Transformation Call Chain

The recording and dispatch of audit logs follow a strict execution path from input validation to upstream delivery:

1. `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-53](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L47-L53)
2. `transformAuditLogTB(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-45](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L13-L45)
3. `recordAuditLogTB(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-79](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L58-L79)

Sources: [apps/web/lib/api/audit-logs/record-audit-log.ts:13-79](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L13-L79)

> [!WARNING]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/record-audit-log.ts#L58-L72)

### Audit Log Schema and Target Types

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L15-L30)

| Field Name | Zod Type | Nullable | Description |
| :--- | :--- | :--- | :--- |
| `id` | `z.string()` | No | Unique audit log identifier prefixed with `audit_`. Sources: [apps/web/lib/api/audit-logs/schemas.ts:17-17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L17) |
| `timestamp` | `z.string()` | No | ISO 8601 timestamp generated at record creation. Sources: [apps/web/lib/api/audit-logs/schemas.ts:18-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L18) |
| `workspace_id` | `z.string()` | No | Prefixed workspace identifier associated with the event. Sources: [apps/web/lib/api/audit-logs/schemas.ts:19-19](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L19) |
| `program_id` | `z.string()` | No | Program identifier associated with the workspace action. Sources: [apps/web/lib/api/audit-logs/schemas.ts:20-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L20) |
| `action` | `z.string()` | No | Categorized action string validated against supported actions. Sources: [apps/web/lib/api/audit-logs/schemas.ts:21-21](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L21) |
| `actor_id` | `z.string()` | No | Identifier of the user or system entity performing the action. Sources: [apps/web/lib/api/audit-logs/schemas.ts:22-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L22) |
| `actor_type` | `z.string()` | No | Classification of the actor, defaulting to `user`. Sources: [apps/web/lib/api/audit-logs/schemas.ts:23-23](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L23) |
| `actor_name` | `z.string()` | No | Display name of the actor. Sources: [apps/web/lib/api/audit-logs/schemas.ts:24-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L24) |
| `description` | `z.string()` | No | Human-readable narrative description of the event. Sources: [apps/web/lib/api/audit-logs/schemas.ts:25-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L25) |
| `targets` | `z.string()` | Yes | JSON-serialized array of targeted resources and entity metadata. Sources: [apps/web/lib/api/audit-logs/schemas.ts:26-26](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L26) |
| `ip_address` | `z.string()` | Yes | Client IP address captured from request headers or Vercel runtime. Sources: [apps/web/lib/api/audit-logs/schemas.ts:27-27](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L27) |
| `user_agent` | `z.string()` | Yes | Client user-agent string extracted from request headers. Sources: [apps/web/lib/api/audit-logs/schemas.ts:28-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L28) |
| `metadata` | `z.string()` | Yes | JSON-serialized custom metadata dictionary. Sources: [apps/web/lib/api/audit-logs/schemas.ts:29-29](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L29) |

Sources: [apps/web/lib/api/audit-logs/schemas.ts:16-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/schemas.ts#L16-L30)

## Audit Log Export and Security

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L6-L52)

### Audit Log Query and Export Pipeline

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L10-L60)

The execution proceeds through a strict call chain:
1. `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-20](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L18-L20)
2. `getPlanCapabilities(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-36](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L29-L36)
3. `getDefaultProgramIdOrThrow(workspace)` retrieves the active program identifier required for log retrieval. Sources: [apps/web/app/ee/api/audit-logs/export/route.ts:38-38](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L38)
4. `getAuditLogs(...)` invokes Tinybird pipe `get_audit_logs` with UTC-formatted date strings. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:38-51](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L38-L51), [apps/web/app/ee/api/audit-logs/export/route.ts:40-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L40-L45)
5. `convertToCSV(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-54](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L47-L54)

Sources: [apps/web/app/ee/api/audit-logs/export/route.ts:16-55](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L16-L55)

> [!WARNING]
> 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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/audit-logs/export/route.ts#L29-L36), [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/audit-logs.tsx:81-129](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/audit-logs.tsx#L81-L129)

### Tinybird Response and Filter Schemas

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L3-L25)

| Schema Name | Target Field / Property | Zod Definition / Type | Purpose |
| :--- | :--- | :--- | :--- |
| `auditLogFilterSchemaTB` | `workspaceId` | `z.string().transform(prefixWorkspaceId)` | Validates and prefixes workspace ID. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:7-7](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L7) |
| `auditLogFilterSchemaTB` | `programId` | `z.string()` | Identifies the program context. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:8-8](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L8) |
| `auditLogFilterSchemaTB` | `start` / `end` | `z.string()` | Query time boundaries. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:9-10](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L9-L10) |
| `auditLogResponseSchemaTB` | `id` / `timestamp` / `action` | `z.string()` | Core event metadata identifiers. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:14-16](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L14-L16) |
| `auditLogResponseSchemaTB` | `actor_id` / `actor_type` / `actor_name` | `z.string()` | Actor identity attributes. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:17-19](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L17-L19) |
| `auditLogResponseSchemaTB` | `description` / `ip_address` / `user_agent` | `z.string()` | Narrative and client connection details. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:20-22](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L20-L22) |
| `auditLogResponseSchemaTB` | `targets` / `metadata` | `z.string()` | Serialized JSON target resources and custom metadata. Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:23-24](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L23-L24) |

Sources: [apps/web/lib/api/audit-logs/get-audit-logs.ts:6-25](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/audit-logs/get-audit-logs.ts#L6-L25)

### Client UI Settings and Export Execution

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/audit-logs.tsx#L12-L66), [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/page-client.tsx:7-15](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/page-client.tsx#L7-L15)

```typescript
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();
}
```

Sources: [apps/web/app/app.dub.co/dashboard/slug/ee/settings/security/audit-logs.tsx:22-66](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/security/audit-logs.tsx#L22-L66)

## Prisma Activity Log Persistence

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20), [apps/web/lib/api/activity-log/track-activity-log.ts:1-93](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L1-L93)

### Relational Schema and Indexes

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](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20)

| Column Name | Prisma Type | Modifiers / Attributes | Description |
| :--- | :--- | :--- | :--- |
| `id` | `String` | `@id @default(cuid())` | Unique identifier for the activity log entry. Sources: [apps/web/prisma/schema/activity.prisma:2-2](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L2) |
| `workspaceId` | `String` | None | Workspace context identifier. Sources: [apps/web/prisma/schema/activity.prisma:3-3](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L3) |
| `programId` | `String` | None | Program context identifier. Sources: [apps/web/prisma/schema/activity.prisma:4-4](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L4) |
| `parentResourceType` | `String` | `@nullable` | Optional parent resource classification. Sources: [apps/web/prisma/schema/activity.prisma:5-5](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L5) |
| `parentResourceId` | `String` | `@nullable` | Optional parent resource identifier. Sources: [apps/web/prisma/schema/activity.prisma:6-6](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L6) |
| `resourceType` | `String` | None | Target resource classification. Sources: [apps/web/prisma/schema/activity.prisma:7-7](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L7) |
| `resourceId` | `String` | None | Target resource identifier. Sources: [apps/web/prisma/schema/activity.prisma:8-8](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L8) |
| `userId` | `String` | `@nullable` | Optional actor user identifier. Sources: [apps/web/prisma/schema/activity.prisma:9-9](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L9) |
| `action` | `String` | None | Performed action identifier string. Sources: [apps/web/prisma/schema/activity.prisma:10-10](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L10) |
| `description` | `String` | `@nullable @db.Text` | Optional human-readable narrative text. Sources: [apps/web/prisma/schema/activity.prisma:11-11](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L11) |
| `batchId` | `String` | `@nullable` | Optional batch grouping identifier. Sources: [apps/web/prisma/schema/activity.prisma:12-12](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L12) |
| `changeSet` | `Json` | `@nullable` | Structured diff payload JSON. Sources: [apps/web/prisma/schema/activity.prisma:13-13](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L13) |
| `createdAt` | `DateTime` | `@default(now())` | Timestamp when the entry was recorded. Sources: [apps/web/prisma/schema/activity.prisma:14-14](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L14) |

Sources: [apps/web/prisma/schema/activity.prisma:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L18-L19)

### Tracking Helper Execution Flow

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L34-L65)

```typescript
const ACTIONS_WITHOUT_CHANGE_SET: ActivityLogAction[] = [
  "submittedLead.created",
  "reward.created",
  "reward.deleted",
];
```

Sources: [apps/web/lib/api/activity-log/track-activity-log.ts:11-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L11-L15)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L34-L65)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L39-L43)

### Transactional Logging and Zod Resource Validation

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L68-L93)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/activity-log.ts#L4-L13)

| Design Choice | Benefit | Cost |
| :--- | :--- | :--- |
| **Whitelisted Actions Without ChangeSet** | Allows creation events (like rewards and leads) to log without prior diff states. Sources: [apps/web/lib/api/activity-log/track-activity-log.ts:11-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L11-L15) | Requires maintaining explicit exemption arrays when new lifecycle creation actions are introduced. Sources: [apps/web/lib/api/activity-log/track-activity-log.ts:11-15](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L11-L15) |
| **Separate Transactional Helper (`trackActivityLogsTx`)** | Prevents dangling activity records when parent business logic transactions roll back. Sources: [apps/web/lib/api/activity-log/track-activity-log.ts:68-93](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L68-L93) | Callers must explicitly thread the transaction client `tx` through repository service layers. Sources: [apps/web/lib/api/activity-log/track-activity-log.ts:68-74](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L68-L74) |
| **JSON ChangeSet Column with FieldDiff Schema** | Stores arbitrary structural updates (`old` and `new` unknown values) flexibly. Sources: [apps/web/prisma/schema/activity.prisma:13-13](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L13), [apps/web/lib/zod/schemas/activity-log.ts:57-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/activity-log.ts#L57-L60) | Loses relational column-level indexing on inner diff properties within queries. Sources: [apps/web/prisma/schema/activity.prisma:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20) |

Sources: [apps/web/prisma/schema/activity.prisma:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/prisma/schema/activity.prisma#L1-L20), [apps/web/lib/api/activity-log/track-activity-log.ts:11-93](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-activity-log.ts#L11-L93), [apps/web/lib/zod/schemas/activity-log.ts:57-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/activity-log.ts#L57-L60)

## Partner and Commission Change Tracking

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-overrides.ts#L1-L57), [apps/web/lib/api/commissions/track-commission-update-activity-log.ts:1-30](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L1-L30), [apps/web/lib/api/activity-log/track-reward-activity-log.ts:1-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts#L1-L32)

### Commission Update Tracking and Snapshot Generation

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L31-L75)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L19-L75)

```typescript
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);
}
```
Sources: [apps/web/lib/api/commissions/track-commission-update-activity-log.ts:31-75](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L31-L75)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L77-L142)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/track-commission-update-activity-log.ts#L127-L134)

### Reward Lifecycle and Modifier Change Tracking

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/activity-log.ts#L71-L77), [apps/web/lib/api/activity-log/track-reward-activity-log.ts:9-143](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts#L9-L143)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts#L34-L125)

| Reward Action | Trigger Condition | ChangeSet Structure |
| :--- | :--- | :--- |
| `reward.created` | `old` is `null` and `new` is present | `{ reward: { old: null, new: newSnapshot } }` |
| `reward.updated` | Both `old` and `new` are present, and `getResourceDiff` finds changes | `{ reward: { old: oldSnapshot, new: newSnapshot } }` |
| `reward.deleted` | `old` is present and `new` is `null` | `{ reward: { old: oldSnapshot, new: null } }` |
| `reward.conditionAdded` | Modifier ID exists in new modifiers but not old | `{ reward: { old: null, new: { ...newReward, modifiers: [newMod] } } }` |
| `reward.conditionUpdated` | Modifier ID exists in both but `modifierEquals` returns false | `{ reward: { old: { ...oldReward, modifiers: [oldMod] }, new: { ...newReward, modifiers: [newMod] } } }` |
| `reward.conditionRemoved` | Modifier ID exists in old modifiers but is missing in new | `{ reward: { old: { ...oldReward, modifiers: [oldMod] }, new: null } }` |

Sources: [apps/web/lib/api/activity-log/track-reward-activity-log.ts:145-225](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts#L145-L225)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-activity-log.ts#L227-L239)

### Partner and Link Reward Overrides

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-overrides.ts#L30-L193)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/activity-log/track-reward-overrides.ts#L44-L178)

## Activity Feeds and Audit Presentation

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/activity-logs/route.ts#L12-L34), [apps/web/lib/swr/use-activity-logs.ts:6-34](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-activity-logs.ts#L6-L34), [apps/web/lib/swr/use-partner-activity-logs.ts:6-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-partner-activity-logs.ts#L6-L31)

### SWR Data Fetching Hooks

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-activity-logs.ts#L6-L34)

| Hook Name | Target Endpoint | Enabling Conditions | Returned Properties |
| :--- | :--- | :--- | :--- |
| `useActivityLogs` | `/api/activity-logs?${searchParams}` | `enabled && workspaceId && query?.resourceType && (query?.parentResourceId || query?.resourceId)` | `activityLogs`, `error`, `loading`, `mutate` |
| `usePartnerActivityLogs` | `/api/partner-profile/programs/${programSlug}/activity-logs?${searchParams}` | `enabled && programSlug && query?.resourceType && query?.resourceId` | `activityLogs`, `error`, `loading`, `mutate` |
| `usePartnerEnrollmentHistorySheet` | Route query params via `useRouterStuff` | `partner?.id` exists | `hasActivityLogs`, `partnerEnrollmentHistorySheet`, `setIsOpen` |

Sources: [apps/web/lib/swr/use-activity-logs.ts:6-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-activity-logs.ts#L6-L42), [apps/web/lib/swr/use-partner-activity-logs.ts:6-39](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/swr/use-partner-activity-logs.ts#L6-L39), [apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:52-93](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx#L52-L93)

### Sheet Drawers and Activity Sections

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx#L41-L93)

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-activity-section.tsx#L9-L45), [apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:41-93](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx#L41-L93)

Sources: [apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx:41-93](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/partner-enrollment-history-sheet.tsx#L41-L93)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/partner-profile/programs/%5BprogramId%5D/activity-logs/route.ts#L63-L68)

### Action Renderers

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](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/activity-logs/action-renderers/partner-group-changed-renderer.tsx#L18-L48)

## API and Webhook Request Logging

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/capture-webhook-log.ts#L16-L51), [apps/web/lib/api-logs/record-api-log.ts:30-93](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/record-api-log.ts#L30-L93)

### API Log Recording and Ingestion Pipeline

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/capture-webhook-log.ts#L4-L50), [apps/web/lib/api-logs/record-api-log.ts:8-92](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/record-api-log.ts#L8-L92)

```typescript
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);
};
```
Sources: [apps/web/lib/api-logs/record-api-log.ts:36-67](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/record-api-log.ts#L36-L67)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api-logs/record-api-log.ts#L69-L92)

### API Log Retrieval and Plan Retention Enforcement

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/route.ts#L8-L40), [apps/web/app/api/logs/logId/route.ts:9-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/%5BlogId%5D/route.ts#L9-L45)

| Endpoint Route | Required Permission | Query Validation Schema | Retention & Enrichment Behavior |
| :--- | :--- | :--- | :--- |
| `GET /api/logs` | `workspaces.read` | `getApiLogsQuerySchema` via `searchParams` | Applies `getApiLogsDateRange` based on workspace plan, fetches filtered logs, and enriches them. Sources: [apps/web/app/api/logs/route.ts:8-40](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/route.ts#L8-L40) |
| `GET /api/logs/:logId` | `workspaces.read` | Route parameter `logId` | Fetches single log by ID, throws `not_found` if missing or older than plan retention date, and enriches log data. Sources: [apps/web/app/api/logs/logId/route.ts:9-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/%5BlogId%5D/route.ts#L9-L45) |

Sources: [apps/web/app/api/logs/route.ts:8-40](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/route.ts#L8-L40), [apps/web/app/api/logs/logId/route.ts:9-45](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/logs/%5BlogId%5D/route.ts#L9-L45)

## Related

- [Authentication and Sessions](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/authentication-and-sessions)
- [Enterprise SSO and SCIM](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/authentication-and-security/enterprise-sso-and-scim)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
