+ {/* Header with Back Button */}
+
+
+
+
Booking History
+
View all changes and events for this booking
+
+
+
+ {/* Filters */}
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {/* Audit Log List */}
+
+ {filteredLogs.length === 0 ? (
+
+ ) : (
+ filteredLogs.map((log) => {
+ const isExpanded = expandedLogId === log.id;
+ const actionDisplay = actionDisplayMap[log.action] || log.action;
+
+ return (
+
+ {/* Log Header */}
+
+ {/* Icon */}
+
{getActionIcon(log.action)}
+
+ {/* Content */}
+
+
+
+
{actionDisplay}
+
+
+
+ {log.actor.displayName}
+
+ •
+
+
+ {dayjs(log.timestamp).fromNow()}
+
+
+
+
+
+
+
+
+
+ {/* Expanded Details */}
+ {isExpanded && (
+
+
+
+ Type:
+ {log.type}
+
+
+ Actor:
+ {log.actor.type}
+
+
+ Timestamp:
+
+ {dayjs(log.timestamp).format("YYYY-MM-DD HH:mm:ss")}
+
+
+ {log.actor.displayEmail && (
+
+ Actor Email:
+ {log.actor.displayEmail}
+
+ )}
+
+
+ {/* Data JSON */}
+ {log.data && (
+
+
Details:
+
+ {JSON.stringify(log.data, null, 2)}
+
+
+ )}
+
+ )}
+
+ );
+ })
+ )}
+
+
+ {/* Summary Stats */}
+
+
+
+ Showing {filteredLogs.length} of {auditLogs.length} log entries
+
+
+
+
+ );
+}
+
diff --git a/packages/features/booking-audit/ARCHITECTURE.md b/packages/features/booking-audit/ARCHITECTURE.md
index 802d7f07f22e41..4fff9d74a22dea 100644
--- a/packages/features/booking-audit/ARCHITECTURE.md
+++ b/packages/features/booking-audit/ARCHITECTURE.md
@@ -1,6 +1,8 @@
# Booking Audit System - Database Architecture
Based on https://github.com/calcom/cal.com/pull/22817
+Note: This architecture is not in production yet, so we can make any changes we want to it without worrying about backwards compatibility.
+
## Overview
The Booking Audit System tracks all actions and changes related to bookings in Cal.com. The architecture is built around two core tables (`Actor` and `BookingAudit`) that work together to maintain a complete, immutable audit trail.
@@ -25,6 +27,10 @@ model Actor {
phone String?
name String?
+ // GDPR/HIPAA Compliance fields
+ pseudonymizedAt DateTime? // When actor data was pseudonymized for privacy
+ scheduledDeletionDate DateTime? // When actor record should be fully anonymized after retention period
+
createdAt DateTime @default(now())
bookingAudits BookingAudit[]
@@ -35,6 +41,7 @@ model Actor {
@@index([email])
@@index([userId])
@@index([attendeeId])
+ @@index([pseudonymizedAt]) // For compliance cleanup jobs
}
```
@@ -44,6 +51,7 @@ model Actor {
- **Unique Constraints**: Prevents duplicate actors for the same user/email/phone
- **Multiple Identity Fields**: Supports different actor types (users, guests, attendees, system)
- **Extensible System Actors**: Architecture supports multiple system actors (e.g., Cron, Webhooks, API integrations, Background Workers) for granular tracking of automated operations
+- **Pseudonymization on Deletion**: Actor records are pseudonymized (PII nullified) rather than deleted to maintain HIPAA-compliant immutable audit trails. Supports compliance cleanup jobs via `pseudonymizedAt` and `scheduledDeletionDate` indices
---
@@ -61,10 +69,18 @@ model BookingAudit {
actorId String
actor Actor @relation(fields: [actorId], references: [id], onDelete: Restrict)
- type BookingAuditType // Database-level change: created/updated/deleted
- action BookingAuditAction // Business operation: what happened
- timestamp DateTime // When the action occurred (explicitly provided, no default)
- data Json?
+ type BookingAuditType
+ action BookingAuditAction
+
+ // Timestamp of the actual booking change (business event time)
+ // Important: May differ from createdAt if audit is processed asynchronously
+ timestamp DateTime
+
+ // Database record timestamps
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ data Json?
@@index([actorId])
@@index([bookingId])
@@ -75,7 +91,8 @@ model BookingAudit {
- **UUID Primary Key**: Enables time-sortable IDs (will migrate to uuid7 when Prisma 7 is available)
- **Restrict on Delete**: `onDelete: Restrict` prevents actor deletion if audit records exist
- **Required Action**: Every audit record must specify a business action, ensuring explicit tracking of what happened
-- **Explicit Timestamp**: The `timestamp` field has no default and must be explicitly provided, ensuring accurate capture of when the business event occurred
+- **Explicit Timestamp**: The `timestamp` field has no default and must be explicitly provided, representing when the business event actually occurred
+- **Separate Database Timestamps**: `createdAt` and `updatedAt` track when the audit record itself was created/modified, distinct from the business event time
- **JSON Data Field**: Flexible schema for storing action-specific contextual data
- **Indexed Fields**: Efficient queries by `bookingId` and `actorId`
@@ -95,6 +112,7 @@ Defines the type of entity performing an action:
```prisma
enum ActorType {
USER @map("user") // Registered Cal.com user (stored here for audit retention even after user deletion)
+ // Considering renaming it to ANONYMOUS to avoid confusion with Guest of a booking
GUEST @map("guest") // Non-registered user
ATTENDEE @map("attendee") // Guest who booked (has Attendee record)
SYSTEM @map("system") // Automated actions
@@ -141,23 +159,17 @@ enum BookingAuditAction {
CANCELLED @map("cancelled")
ACCEPTED @map("accepted")
REJECTED @map("rejected")
- PENDING @map("pending")
- AWAITING_HOST @map("awaiting_host")
RESCHEDULED @map("rescheduled")
// Attendee management
ATTENDEE_ADDED @map("attendee_added")
ATTENDEE_REMOVED @map("attendee_removed")
- // Cancellation/Rejection/Assignment reasons
- CANCELLATION_REASON_UPDATED @map("cancellation_reason_updated")
- REJECTION_REASON_UPDATED @map("rejection_reason_updated")
- ASSIGNMENT_REASON_UPDATED @map("assignment_reason_updated")
- REASSIGNMENT_REASON_UPDATED @map("reassignment_reason_updated")
+ // Assignment/Reassignment
+ REASSIGNMENT @map("reassignment")
// Meeting details
LOCATION_CHANGED @map("location_changed")
- MEETING_URL_UPDATED @map("meeting_url_updated")
// No-show tracking
HOST_NO_SHOW_UPDATED @map("host_no_show_updated")
@@ -174,17 +186,16 @@ enum BookingAuditAction {
- `CREATED`
2. **Status Changes**: Track booking lifecycle transitions
- - `CANCELLED`, `ACCEPTED`, `REJECTED`, `PENDING`, `AWAITING_HOST`, `RESCHEDULED`
+ - `CANCELLED`, `ACCEPTED`, `REJECTED`, `RESCHEDULED`
3. **Attendee Management**: Track changes to booking participants
- `ATTENDEE_ADDED`, `ATTENDEE_REMOVED`
-4. **Reason Updates**: Track updates to explanatory text fields
- - `CANCELLATION_REASON_UPDATED`, `REJECTION_REASON_UPDATED`
- - `ASSIGNMENT_REASON_UPDATED`, `REASSIGNMENT_REASON_UPDATED`
+4. **Assignment/Reassignment**: Track booking host assignment changes
+ - `REASSIGNMENT`
5. **Meeting Details**: Track changes to meeting logistics
- - `LOCATION_CHANGED`, `MEETING_URL_UPDATED`
+ - `LOCATION_CHANGED`
6. **No-Show Tracking**: Track attendance issues
- `HOST_NO_SHOW_UPDATED`, `ATTENDEE_NO_SHOW_UPDATED`
@@ -194,6 +205,79 @@ enum BookingAuditAction {
---
+## Schema Structure - Change Tracking
+
+All audit actions follow a consistent structure for tracking changes. This structure ensures complete audit trail coverage by capturing both the old and new values for every tracked field.
+
+### Core Pattern
+
+Each action stores a flat object with all relevant fields. Each field tracks both old and new values:
+
+```typescript
+{
+ field1: { old: T | null, new: T },
+ field2: { old: T | null, new: T },
+ field3: { old: T | null, new: T } // Optional fields as needed
+}
+```
+
+### Change Tracking
+
+**All fields use the same pattern** - `{ old: T | null, new: T }`:
+- `old`: The previous value (null if the field didn't exist before)
+- `new`: The new value after the change
+
+**Examples:**
+```typescript
+// Simple field changes
+status: { old: "ACCEPTED", new: "CANCELLED" }
+location: { old: "Zoom", new: "Google Meet" }
+
+// New fields (old is null)
+cancellationReason: { old: null, new: "Client requested" }
+```
+
+### Semantic Clarity at Application Layer
+
+**Action Services decide what to display prominently.** Each Action Service has methods like `getDisplayDetails()` that determine:
+- Which fields are most important to show by default
+- Which fields should be available but not emphasized
+- How to format the data for display
+
+This keeps the data structure simple while maintaining semantic clarity where it matters - in the UI.
+
+### Benefits
+
+1. **Complete Audit Trail**: Full before/after state captured for every change
+2. **Self-Contained Records**: Each record has complete context without querying previous records
+3. **Simple Structure**: Flat object, easy to work with and extend
+4. **Better UI**: Action Services decide what to emphasize based on user needs
+5. **State Reconstruction**: Can rebuild booking state at any point in the audit timeline
+6. **Easier Debugging**: See exact state transitions in each record
+7. **Type Safety**: Zod schemas validate the structure while keeping it flexible
+
+### Examples by Action
+
+#### Simple Action
+```typescript
+// LOCATION_CHANGED
+{
+ location: { old: "Zoom", new: "Google Meet" }
+}
+```
+
+#### Action with Multiple Fields
+```typescript
+// CANCELLED
+{
+ cancellationReason: { old: null, new: "Client requested" },
+ cancelledBy: { old: null, new: "user@example.com" },
+ status: { old: "ACCEPTED", new: "CANCELLED" }
+}
+```
+
+---
+
## JSON Data Schemas by Action
The `BookingAudit.data` field stores action-specific context. Each action has its own schema defined in a dedicated Action Helper Service using Zod validation.
@@ -213,52 +297,52 @@ Used when a booking is initially created. Records the complete state at creation
**Design Decision:** The `status` field accepts any `BookingStatus` value, not just the expected creation statuses (ACCEPTED, PENDING, AWAITING_HOST). This follows the principle of capturing reality rather than enforcing business rules in the audit layer. If a booking is ever created with an unexpected status due to a bug, we want to record that fact for debugging purposes rather than silently skip the audit record.
+**Note:** The CREATED action is unique - it captures the initial booking state at creation, so it doesn't use the `{ old, new }` tracking pattern. It's a flat object with just the initial values: `{ startTime, endTime, status }`.
+
---
### Status Change Actions
#### ACCEPTED
-Used when a booking status changes to accepted (e.g., PENDING → ACCEPTED). Often includes other field changes like calendar references and meeting URLs.
+Used when a booking status changes to accepted.
```typescript
{
- changes?: Array