Files
calendar/packages/prisma/schema.prisma
T
Keith WilliamsGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>cal.comMorgancubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
6923b97cd2 feat: upgrade Prisma to 6.16.0 with no-rust engine (#23816)
* feat: upgrade Prisma to 6.16.0 with no-rust engine

- Update Prisma packages to 6.16.0
- Add PostgreSQL adapter dependency
- Configure engineType: 'client' and provider: 'prisma-client' in schema
- Update Prisma client instantiation with PostgreSQL adapter
- Remove binaryTargets from generators (not needed with library engine)
- Fix schema view issue by removing @id decorator from BookingTimeStatusDenormalized
- Fix ESLint warning by removing non-null assertion

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Web app running but types wrecked

* web app running but build and type issues

* Removed the connection pool

* Fixed zod type issue

* Fixed types in booking reference extension

* Fixed test issues

* Type checks passing it seems

* Using cjs as moduleFormat

* Fixing Prisma undefined

* fix: update prismock initialization for Prisma 6.16 compatibility

- Add @prisma/internals dependency for getDMMF()
- Restructure prismock initialization to use createPrismock() with DMMF
- Create Proxy that's returned from mock factory for proper spy support
- Fixes 89 failing unit tests with 'Cannot read properties of undefined (reading datamodel)' error
- Based on workaround from prismock issue #1482

All unit tests now pass (375 test files, 3323 tests passed)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: cast serviceAccountKey to Prisma.InputJsonValue in bookingScenario.ts

- Apply type cast at lines 2493 and 2535
- Fixes type errors from Prisma 6.16 upgrade
- Follows established pattern from delegationCredential.ts
- Add eslint-disable for pre-existing any types
- Rename unused appStoreLookupKey parameter to satisfy lint
- All 3323 tests passing

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* chore: remove whitespace-only lines from bookingScenario.ts

- Remove blank lines where eslint-disable comments were replaced
- Cleanup from pre-commit hook formatting

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Update is-prisma-available-check.ts

* fix: remove datasources config when using Prisma Driver Adapters

- Update customPrisma to create new adapter when datasources URL is provided
- Remove datasources config from API v2 Prisma services (already in adapter)
- Fixes 'Custom datasource configuration is not compatible with Prisma Driver Adapters' error

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use Pool instances for PrismaPg adapters in index.ts

- Create Pool instance before passing to PrismaPg adapter
- Update customPrisma to create Pool for custom connection strings
- Matches working pattern from API v2 services
- Fixes 'Invalid `prisma.$queryRawUnsafe()` invocation' error

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Not using queryRawUnsafe

* Trying anything at this point

* Make sure the DB is ready first

* Don't auto run migrations in CI mode

* Revert "Make sure the DB is ready first"

This reverts commit 2b20bd45c974f3d7e07d8b904bc7fcdae37cce03.

* Dynamic import of prisma

* Commenting where it seems to break

* Backwards compatability for API v2

* fix: add explicit type annotations for map callbacks in API v2

- Add type annotation for map parameter in memberships.repository.ts
- Add type annotation for map parameter in stripe.service.ts
- Fixes implicit 'any' type errors from stricter Prisma 6.16.0 type inference

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add explicit type annotations for API v2 map callbacks

- users.repository.ts:292: add Profile & { user: User } type
- memberships.service.ts:19-20: add Membership type to filter callbacks

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add explicit type annotation for attributeToUser in organizations-users.repository.ts

- organizations-users.repository.ts:63: add AttributeToUser with nested relations type

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add explicit Membership type annotations in teams.repository.ts

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use API v2 dedicated Prisma client to support adapter in PrismaClientOptions

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Running API v2 build commands together so they all get the space size var

* Fixing Maximum call depth exceeded error

* fixed type issues

* Trying to make the seed more stable

* Revert "Trying to make the seed more stable"

This reverts commit 1fd4495e6af7acd7981cda7dedec3168979b0e9d.

* Fixed path to prisma client

* Fixed type check

* Fix eslint warnings

* fix: externalize @prisma/adapter-pg and pg in platform-libraries Vite config

- Add @prisma/adapter-pg and pg to external dependencies list
- Add corresponding globals for these packages
- Fix Prisma client aliases to point to packages/prisma/client instead of node_modules
- Add Node.js resolve conditions to prefer Node.js exports
- Keep commonjsOptions.include for proper CommonJS transformation
- Add eslint-disable for __dirname in Vite config file
- Remove problematic prettier/prettier eslint comment

This fixes the 'Extensions.defineExtension is unable to run in this browser environment' error when running yarn generate-swagger in apps/api/v2 after upgrading to Prisma v6.16 with the no-rust engine approach.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update Prisma imports in API v2 services to use package path

- Change imports from '../../../generated/prisma/client' to '@calcom/prisma/client'
- Fixes CI error: Cannot find module '../../../../../packages/prisma/generated/prisma/client.ts'
- Aligns with backwards compatibility re-export structure after Prisma v6.16 upgrade

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove .ts extension from Prisma client path mapping in tsconfig

- Remove file extension from @calcom/prisma/client path mapping
- Fixes runtime error: Cannot find module '../../../../../packages/prisma/generated/prisma/client.ts'
- TypeScript path mappings should not include file extensions per best practices
- Allows Node.js to correctly resolve to .js files at runtime

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: resolve Prisma 6.16.0 type incompatibilities in bookingScenario tests

- Changed InputPayment.data type from PaymentData to Prisma.InputJsonValue
- Changed createCredentials key parameter from JsonValue to InputJsonValue
- Removed unused PaymentData type definition
- Resolves type errors at lines 709 and 1088 without using 'as any' casts

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: remove non-existent Watchlist fields from test fixtures

- Remove createdById from isLockedOrBlocked.test.ts (lines 15, 20)
- Remove severity and createdById from _post.test.ts (line 110)
- These fields don't exist in Watchlist model schema after Prisma 6.16.0 upgrade
- Resolves TS2353 errors without using 'as any' casts

Relates to PR #23816

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: api v2 imports generated prisma and platform libraries

* fix: resolve type errors from Prisma 6.16 upgrade

- Add missing markdownToSafeHTML import in AppCard.tsx
- Fix organizationId null handling in fresh-booking.test.ts
- Remove non-existent createdById field from Watchlist test utils

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Put back some external rollups

* Added back the resolve conditions

* Stop using Pool directly

* chore: remove prisma bookingReferenceExtension and update calls

* fix: organizations-admin-not-team-member-event-types.e2e-spec.ts

* chore: bring back POOL in api v2 prisma clients

* chore: remove Pool but await connect

* fixup! chore: remove Pool but await connect

* chore: bring back Pool on all clients

* chore: end pool manually

* chore: test with pool max 1

* chore: e2e test prisma max  pool of 1 connection

* chore: give more control over pool for prisma module with env

* remove pool from base prisma client

* chore: prisma client in libraries use pool

* Fixed types

* chore: log pool events and improve pooling

* Fixing some types and tests

* Changing the parsing of USE_POOL

* fix: ensure Prisma client is connected before seeding to prevent transaction errors

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* chore: adjust pools

* chore: add process.env.USE_POOL to libraries vite config

* fix: v1 _patch reference check bookingRef on the booking find

* fix: v1 get references deleted null for system admin

* test: add integration tests for bookingReference soft-delete behavior

- Add bookingReference.integration-test.ts to test repository methods
- Add handleDeleteCredential.integration-test.ts to test credential deletion cascade
- Add booking-references.integration-test.ts for API v1 integration tests
- All tests verify soft-delete behavior without using mocks
- Tests use real database operations to ensure soft-deleted records persist
- Cover scenarios: replacing references, credential deletion, querying with filters

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: convert booking-references test to actual API endpoint testing

- Modified _get.ts to export handler function for testing
- Refactored integration test to call API handler instead of directly testing Prisma
- Added timestamps to test data to avoid conflicts
- Tests now verify API layer correctly filters soft-deleted references
- All 4 tests passing

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add explicit prisma.$connect() call to seed-insights script

With Prisma 6.16 and the PostgreSQL adapter, scripts need to explicitly call $connect() before running database operations to ensure the connection pool is properly initialized. This prevents 'Transaction already closed' errors.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add $connect() to main() execution in seed-insights

Both main() and createPerformanceData() entry points need explicit prisma.$connect() calls with the Prisma 6.16 PostgreSQL adapter.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: always use connection pool for Prisma PostgreSQL adapter

Enable connection pooling by default for the Prisma adapter to prevent
transaction state issues during seed operations. Without a pool, each
operation creates a new connection which can lead to 'Transaction already
closed' errors during heavy database operations like seeding.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Revert "fix: always use connection pool for Prisma PostgreSQL adapter"

This reverts commit 6724bb08e42bc0a94846069de83b04db0aeb8e8b.

* fix: enable connection pool for db-seed in cache-db action

Set USE_POOL=true when running yarn db-seed to use connection pooling
with the Prisma PostgreSQL adapter. This prevents 'Transaction already
closed' errors during seeding by maintaining stable database connections.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add safety check for undefined ownerForEvent in seed script

Prevent 'Cannot read properties of undefined' error when orgMembersInDBWithProfileId
is empty. This can happen if organization members fail to create or when there's a
duplicate constraint violation causing early return.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: v1 _patch reference check bookingRef

* fix: increase pool size and add timeout settings to prevent transaction errors

- Increase max connections from 5 to 10
- Add connectionTimeoutMillis: 30000 (30 seconds)
- Add statement_timeout: 60000 (60 seconds)

These settings help prevent 'Unknown transaction status' errors during
heavy database operations like seeding by giving transactions more time
to complete and allowing more concurrent connections.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Revert "fix: increase pool size and add timeout settings to prevent transaction errors"

This reverts commit 148264f1f1861dfb09a082937a3e2b49e78fc41a.

* fix: remove standalone execution in seed-app-store to prevent premature disconnect

The seed-app-store.ts file had a standalone main() call at the bottom
that would execute immediately when imported, including a prisma.$disconnect()
in its .finally() block.

This caused issues because:
1. seed.ts imports and calls mainAppStore()
2. The import triggers the standalone main() execution
3. This standalone execution disconnects prisma after completion
4. seed.ts then tries to call mainHugeEventTypesSeed() but prisma is disconnected
5. This leads to 'Unknown transaction status' errors

Fixed by removing the standalone execution since mainAppStore() is already
called programmatically from seed.ts which manages the connection lifecycle.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use require.main check to prevent premature disconnect when imported

Added require.main === module check so seed-app-store.ts:
- Runs standalone with proper connection management when executed directly
  via 'yarn seed-app-store' or 'ts-node seed-app-store.ts'
- Does NOT run standalone when imported as a module by seed.ts,
  preventing premature prisma disconnect

This fixes 'Unknown transaction status' errors while maintaining
backward compatibility for direct execution.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: seed apps before creating users to prevent foreign key constraint violation

Reordered seeding operations to call mainAppStore() before main() because:
- main() creates users with credentials that reference apps via appId foreign key
- mainAppStore() seeds the App table with app records
- Apps must exist before credentials can reference them

This fixes the 'Foreign key constraint violated on Credential_appId_fkey' error
that occurred when creating credentials before the apps they reference existed.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Apply suggestion from @cubic-dev-ai[bot]

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Removing functional changes of deleted: null

* Apply suggestion from @keithwillcode

* refactor: move seedAppData call to bottom of main() in seed.ts

Moved seedAppData() call from seed-app-store.ts to the bottom of main()
in seed.ts to ensure the 'pro' user is created before attempting to
create routing form data for them.

Changes:
- Exported seedAppData function from seed-app-store.ts
- Removed seedAppData() call from the main() export in seed-app-store.ts
- Added seedAppData() call at the bottom of main() in seed.ts
- Updated standalone execution in seed-app-store.ts to still call
  seedAppData() when run directly via 'yarn seed-app-store'

This ensures proper ordering: apps seeded → users created → routing
form data created for existing users.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: move routing form seeding from seed-app-store.ts to seed.ts

Moved the routing form seeding logic (previously in seedAppData function)
from seed-app-store.ts to be inline at the bottom of main() in seed.ts.

This ensures the 'pro' user is created before attempting to create routing
form data for them.

Changes:
- Removed seedAppData function and seededForm export from seed-app-store.ts
- Removed import of seedAppData from seed.ts
- Added routing form seeding logic inline at bottom of main() in seed.ts

Seeding order: apps → users (including 'pro') → routing forms

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: add deleted: null filter to bookingReference update operations

- Add deleted: null filter to API v1 PATCH endpoint to prevent updating soft-deleted booking references
- Add deleted: null filter to DailyVideo updateMeetingTokenIfExpired and setEnableRecordingUIAndUserIdForOrganizer
- Add comprehensive test coverage for PATCH endpoint soft-delete behavior
- Tests verify that soft-deleted booking references cannot be updated
- Tests verify that only active booking references can be updated successfully

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* revert: remove deleted: null filters to preserve existing functionality

Per @keithwillcode's feedback, reverting the soft-delete filtering changes to preserve existing functionality in this PR. This PR should focus only on the Prisma upgrade itself.

- Reverted API v1 PATCH endpoint change
- Reverted DailyVideo adapter changes (updateMeetingTokenIfExpired and setEnableRecordingUIAndUserIdForOrganizer)
- Removed test file that was added for soft-delete behavior testing

Addresses comments:
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448854197
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448860594
- https://github.com/calcom/cal.com/pull/23816#discussion_r2448860833

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: restore and update booking reference tests to match existing functionality

Updated tests to verify existing behavior where PATCH endpoint can update
booking references regardless of their deleted status. This matches the
current implementation after reverting the deleted: null filters.

Changes:
- Restored test file that was previously deleted
- Updated PATCH tests to expect successful updates of soft-deleted references
- Renamed test suite to 'Existing functionality' to clarify intent
- Tests now verify that the PATCH endpoint preserves existing behavior

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* test: rename booking-references test to integration-test

The test requires a database connection and should run in the integration
test job, not the unit test job. Renamed from .test.ts to .integration-test.ts
to match the repository's testing conventions.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 20:02:03 +01:00

2744 lines
99 KiB
Plaintext

// This is your Prisma Schema file
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_DIRECT_URL")
}
generator client {
provider = "prisma-client"
previewFeatures = ["views"]
output = "./generated/prisma"
engineType = "client"
moduleFormat = "cjs"
importFileExtension = ""
}
generator zod {
provider = "zod-prisma-types"
output = "./zod"
useMultipleFiles = true
createInputTypes = false
addIncludeType = false
addSelectType = false
validateWhereUniqueInput = false
prismaClientPath = "../../generated/prisma/client"
moduleFormat = "cjs"
/// createRelationValuesTypes = true
}
generator kysely {
provider = "prisma-kysely"
output = "../kysely"
fileName = "types.ts"
}
generator enums {
provider = "ts-node --transpile-only ./enum-generator.ts"
}
enum SchedulingType {
ROUND_ROBIN @map("roundRobin")
COLLECTIVE @map("collective")
MANAGED @map("managed")
}
enum PeriodType {
UNLIMITED @map("unlimited")
ROLLING @map("rolling")
ROLLING_WINDOW @map("rolling_window")
RANGE @map("range")
}
enum CreationSource {
API_V1 @map("api_v1")
API_V2 @map("api_v2")
WEBAPP @map("webapp")
}
model Host {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
isFixed Boolean @default(false)
priority Int?
weight Int?
// weightAdjustment is deprecated. We not calculate the calibratino value on the spot. Plan to drop this column.
weightAdjustment Int?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
createdAt DateTime @default(now())
group HostGroup? @relation(fields: [groupId], references: [id])
groupId String?
memberId Int?
member Membership? @relation(fields: [memberId], references: [id], onDelete: Cascade)
@@id([userId, eventTypeId])
@@index([memberId])
@@index([userId])
@@index([eventTypeId])
@@index([scheduleId])
}
model HostGroup {
id String @id @default(uuid())
name String
hosts Host[]
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name])
@@index([eventTypeId])
}
model CalVideoSettings {
eventTypeId Int @id
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
disableRecordingForOrganizer Boolean @default(false)
disableRecordingForGuests Boolean @default(false)
enableAutomaticTranscription Boolean @default(false)
enableAutomaticRecordingForOrganizer Boolean @default(false)
redirectUrlOnExit String?
disableTranscriptionForGuests Boolean @default(false)
disableTranscriptionForOrganizer Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model EventType {
id Int @id @default(autoincrement())
/// @zod.string.min(1)
title String
/// @zod.import(["import { eventTypeSlug } from '../../zod-utils'"]).custom.use(eventTypeSlug)
slug String
description String?
interfaceLanguage String?
position Int @default(0)
/// @zod.import(["import { eventTypeLocations } from '../../zod-utils'"]).custom.use(eventTypeLocations)
locations Json?
/// @zod.number.min(1)
length Int
offsetStart Int @default(0)
hidden Boolean @default(false)
hosts Host[]
users User[] @relation("user_eventtype")
owner User? @relation("owner", fields: [userId], references: [id], onDelete: Cascade)
userId Int?
profileId Int?
profile Profile? @relation(fields: [profileId], references: [id])
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
hashedLink HashedLink[]
bookings Booking[]
availability Availability[]
webhooks Webhook[]
destinationCalendar DestinationCalendar?
useEventLevelSelectedCalendars Boolean @default(false)
eventName String?
customInputs EventTypeCustomInput[]
parentId Int?
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
children EventType[] @relation("managed_eventtype")
/// @zod.import(["import { eventTypeBookingFields } from '../../zod-utils'"]).custom.use(eventTypeBookingFields)
bookingFields Json?
timeZone String?
periodType PeriodType @default(UNLIMITED)
/// @zod.import(["import { coerceToDate } from '../../zod-utils'"]).custom.use(coerceToDate)
periodStartDate DateTime?
/// @zod.import(["import { coerceToDate } from '../../zod-utils'"]).custom.use(coerceToDate)
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
lockTimeZoneToggleOnBookingPage Boolean @default(false)
lockedTimeZone String?
requiresConfirmation Boolean @default(false)
requiresConfirmationWillBlockSlot Boolean @default(false)
requiresConfirmationForFreeEmail Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
canSendCalVideoTranscriptionEmails Boolean @default(true)
autoTranslateDescriptionEnabled Boolean @default(false)
/// @zod.import(["import { recurringEventType } from '../../zod-utils'"]).custom.use(recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
hideCalendarEventDetails Boolean @default(false)
/// @zod.number.min(0)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
showOptimizedSlots Boolean? @default(false)
disableCancelling Boolean? @default(false)
disableRescheduling Boolean? @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
allowReschedulingCancelledBookings Boolean? @default(false)
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
price Int @default(0)
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
currency String @default("usd")
slotInterval Int?
/// @zod.import(["import { EventTypeMetaDataSchema } from '../../zod-utils'"]).custom.use(EventTypeMetaDataSchema)
metadata Json?
/// @zod.import(["import { successRedirectUrl } from '../../zod-utils'"]).custom.use(successRedirectUrl)
successRedirectUrl String?
forwardParamsSuccessRedirect Boolean? @default(true)
workflows WorkflowsOnEventTypes[]
/// @zod.import(["import { intervalLimitsType } from '../../zod-utils'"]).custom.use(intervalLimitsType)
bookingLimits Json?
/// @zod.import(["import { intervalLimitsType } from '../../zod-utils'"]).custom.use(intervalLimitsType)
durationLimits Json?
isInstantEvent Boolean @default(false)
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
instantMeetingScheduleId Int?
instantMeetingSchedule Schedule? @relation("InstantMeetingSchedule", fields: [instantMeetingScheduleId], references: [id])
instantMeetingParameters String[]
assignAllTeamMembers Boolean @default(false)
// It is applicable only when assignAllTeamMembers is true and it filters out all the team members using rrSegmentQueryValue
assignRRMembersUsingSegment Boolean @default(false)
/// @zod.import(["import { rrSegmentQueryValueSchema } from '../../zod-utils'"]).custom.use(rrSegmentQueryValueSchema)
rrSegmentQueryValue Json?
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
isRRWeightsEnabled Boolean @default(false)
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
includeNoShowInRRCalculation Boolean @default(false)
selectedCalendars SelectedCalendar[]
allowReschedulingPastBookings Boolean @default(false)
hideOrganizerEmail Boolean @default(false)
maxActiveBookingsPerBooker Int?
maxActiveBookingPerBookerOfferReschedule Boolean @default(false)
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
customReplyToEmail String?
calVideoSettings CalVideoSettings?
/// @zod.import(["import { eventTypeColor } from '../../zod-utils'"]).custom.use(eventTypeColor)
eventTypeColor Json?
rescheduleWithSameRoundRobinHost Boolean @default(false)
secondaryEmailId Int?
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)
useBookerTimezone Boolean @default(false)
restrictionScheduleId Int?
restrictionSchedule Schedule? @relation("restrictionSchedule", fields: [restrictionScheduleId], references: [id])
hostGroups HostGroup[]
bookingRequiresAuthentication Boolean @default(false)
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
@@unique([userId, slug])
@@unique([teamId, slug])
@@unique([userId, parentId])
@@index([userId])
@@index([teamId])
@@index([profileId])
@@index([scheduleId])
@@index([secondaryEmailId])
@@index([parentId])
@@index([restrictionScheduleId])
}
model Credential {
id Int @id @default(autoincrement())
// @@type is deprecated
type String
key Json
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
// How to make it a required column?
appId String?
// paid apps
subscriptionId String?
paymentStatus String?
billingCycleStart Int?
destinationCalendars DestinationCalendar[]
selectedCalendars SelectedCalendar[]
invalid Boolean? @default(false)
CalendarCache CalendarCache[]
references BookingReference[]
delegationCredentialId String?
delegationCredential DelegationCredential? @relation(fields: [delegationCredentialId], references: [id], onDelete: Cascade)
@@index([appId])
@@index([subscriptionId])
@@index([invalid])
@@index([userId, delegationCredentialId])
}
enum IdentityProvider {
CAL
GOOGLE
SAML
}
model DestinationCalendar {
id Int @id @default(autoincrement())
integration String
externalId String
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
primaryEmail String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int? @unique
booking Booking[]
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int? @unique
credentialId Int?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
delegationCredential DelegationCredential? @relation(fields: [delegationCredentialId], references: [id], onDelete: Cascade)
delegationCredentialId String?
domainWideDelegation DomainWideDelegation? @relation(fields: [domainWideDelegationCredentialId], references: [id], onDelete: Cascade)
domainWideDelegationCredentialId String?
@@index([userId])
@@index([eventTypeId])
@@index([credentialId])
}
enum UserPermissionRole {
USER
ADMIN
}
// It holds the password of a User, separate from the User model to avoid leaking the password hash
model UserPassword {
hash String
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model TravelSchedule {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
timeZone String
startDate DateTime
endDate DateTime?
prevTimeZone String?
@@index([startDate])
@@index([endDate])
}
// It holds Personal Profiles of a User plus it has email, password and other core things..
model User {
id Int @id @default(autoincrement())
username String?
name String?
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
email String
emailVerified DateTime?
password UserPassword?
bio String?
avatarUrl String?
timeZone String @default("Europe/London")
travelSchedules TravelSchedule[]
weekStart String @default("Sunday")
// DEPRECATED - TO BE REMOVED
startTime Int @default(0)
endTime Int @default(1440)
// </DEPRECATED>
bufferTime Int @default(0)
hideBranding Boolean @default(false)
// TODO: should be renamed since it only affects the booking page
theme String?
appTheme String?
createdDate DateTime @default(now()) @map(name: "created")
trialEndsAt DateTime?
lastActiveAt DateTime?
eventTypes EventType[] @relation("user_eventtype")
credentials Credential[]
teams Membership[]
bookings Booking[]
schedules Schedule[]
defaultScheduleId Int?
selectedCalendars SelectedCalendar[]
completedOnboarding Boolean @default(false)
locale String?
timeFormat Int? @default(12)
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
backupCodes String?
identityProvider IdentityProvider @default(CAL)
identityProviderId String?
availability Availability[]
invitedTo Int?
webhooks Webhook[]
brandColor String?
darkBrandColor String?
// the location where the events will end up
destinationCalendar DestinationCalendar?
// participate in dynamic group booking or not
allowDynamicBooking Boolean? @default(true)
// participate in SEO indexing or not
allowSEOIndexing Boolean? @default(true)
// receive monthly digest email for teams or not
receiveMonthlyDigestEmail Boolean? @default(true)
// require email verification when someone books using your email
requiresBookerEmailVerification Boolean? @default(false)
/// @zod.import(["import { userMetadata } from '../../zod-utils'"]).custom.use(userMetadata)
metadata Json?
verified Boolean? @default(false)
role UserPermissionRole @default(USER)
disableImpersonation Boolean @default(false)
impersonatedUsers Impersonations[] @relation("impersonated_user")
impersonatedBy Impersonations[] @relation("impersonated_by_user")
apiKeys ApiKey[]
accounts Account[]
sessions Session[]
Feedback Feedback[]
ownedEventTypes EventType[] @relation("owner")
workflows Workflow[]
routingForms App_RoutingForms_Form[] @relation("routing-form")
updatedRoutingForms App_RoutingForms_Form[] @relation("updated-routing-form")
verifiedNumbers VerifiedNumber[]
verifiedEmails VerifiedEmail[]
hosts Host[]
// organizationId is deprecated. Instead, rely on the Profile to search profiles by organizationId and then get user from the profile.
organizationId Int?
organization Team? @relation("scope", fields: [organizationId], references: [id], onDelete: SetNull)
accessCodes AccessCode[]
bookingRedirects OutOfOfficeEntry[]
bookingRedirectsTo OutOfOfficeEntry[] @relation(name: "toUser")
// Used to lock the user account
locked Boolean @default(false)
platformOAuthClients PlatformOAuthClient[]
AccessToken AccessToken[]
RefreshToken RefreshToken[]
PlatformAuthorizationToken PlatformAuthorizationToken[]
profiles Profile[]
movedToProfileId Int?
movedToProfile Profile? @relation("moved_to_profile", fields: [movedToProfileId], references: [id], onDelete: SetNull)
secondaryEmails SecondaryEmail[]
isPlatformManaged Boolean @default(false)
OutOfOfficeReasons OutOfOfficeReason[]
smsLockState SMSLockState @default(UNLOCKED)
smsLockReviewedByAdmin Boolean @default(false)
NotificationsSubscriptions NotificationsSubscriptions[]
referralLinkId String?
features UserFeatures[]
reassignedBookings Booking[] @relation("reassignByUser")
createdAttributeToUsers AttributeToUser[] @relation("createdBy")
updatedAttributeToUsers AttributeToUser[] @relation("updatedBy")
createdTranslations EventTypeTranslation[] @relation("CreatedEventTypeTranslations")
updatedTranslations EventTypeTranslation[] @relation("UpdatedEventTypeTranslations")
reportedBookings BookingReport[] @relation("ReportedBy")
BookingInternalNote BookingInternalNote[]
creationSource CreationSource?
createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings")
filterSegments FilterSegment[]
filterSegmentPreferences UserFilterSegmentPreference[]
creditBalance CreditBalance?
whitelistWorkflows Boolean @default(false)
calAiPhoneNumbers CalAiPhoneNumber[]
agents Agent[]
@@unique([email])
@@unique([email, username])
@@unique([username, organizationId])
@@unique([movedToProfileId])
@@index([username])
@@index([emailVerified])
@@index([identityProvider])
@@index([identityProviderId])
@@map(name: "users")
}
model NotificationsSubscriptions {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
subscription String
@@index([userId, subscription])
}
// It holds Organization Profiles as well as User Profiles for users that have been added to an organization
model Profile {
id Int @id @default(autoincrement())
// uid allows us to set an identifier chosen by us which is helpful in migration when we create the Profile from User directly.
uid String
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organizationId Int
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
username String
eventTypes EventType[]
movedFromUser User? @relation("moved_to_profile")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// A user can have multiple profiles in different organizations
@@unique([userId, organizationId])
// Allow username reuse only across different organizations
@@unique([username, organizationId])
@@index([uid])
@@index([userId])
@@index([organizationId])
}
model Team {
id Int @id @default(autoincrement())
/// @zod.string.min(1)
name String
// It is unique across teams and organizations. We don't have a strong reason for organization and team slug to be conflicting, could be fixed.
// Sub-teams could have same slug across different organizations but not within the same organization.
/// @zod.string.min(1)
slug String?
logoUrl String?
calVideoLogo String?
appLogo String?
appIconLogo String?
bio String?
hideBranding Boolean @default(false)
hideTeamProfileLink Boolean @default(false)
isPrivate Boolean @default(false)
hideBookATeamMember Boolean @default(false)
members Membership[]
eventTypes EventType[]
workflows Workflow[]
createdAt DateTime @default(now())
/// @zod.import(["import { teamMetadataSchema } from '../../zod-utils'"]).custom.use(teamMetadataSchema)
metadata Json?
theme String?
rrResetInterval RRResetInterval? @default(MONTH)
rrTimestampBasis RRTimestampBasis @default(CREATED_AT)
brandColor String?
darkBrandColor String?
verifiedNumbers VerifiedNumber[]
verifiedEmails VerifiedEmail[]
bannerUrl String?
parentId Int?
parent Team? @relation("organization", fields: [parentId], references: [id], onDelete: Cascade)
children Team[] @relation("organization")
orgUsers User[] @relation("scope")
inviteTokens VerificationToken[]
webhooks Webhook[]
timeFormat Int?
timeZone String @default("Europe/London")
weekStart String @default("Sunday")
routingForms App_RoutingForms_Form[]
apiKeys ApiKey[]
credentials Credential[]
accessCodes AccessCode[]
isOrganization Boolean @default(false)
organizationSettings OrganizationSettings?
instantMeetingTokens InstantMeetingToken[]
orgProfiles Profile[]
pendingPayment Boolean @default(false)
dsyncTeamGroupMapping DSyncTeamGroupMapping[]
isPlatform Boolean @default(false)
// Organization's OAuth clients. Organization has them but a team does not.
platformOAuthClient PlatformOAuthClient[]
// OAuth client used to create team of an organization. Team has it but organization does not.
createdByOAuthClient PlatformOAuthClient? @relation("CreatedByOAuthClient", fields: [createdByOAuthClientId], references: [id], onDelete: Cascade)
createdByOAuthClientId String?
smsLockState SMSLockState @default(UNLOCKED)
platformBilling PlatformBilling?
activeOrgWorkflows WorkflowsOnTeams[]
attributes Attribute[]
smsLockReviewedByAdmin Boolean @default(false)
// Available for Organization only
delegationCredentials DelegationCredential[]
domainWideDelegations DomainWideDelegation[]
roles Role[] // Added for Role relation
calAiPhoneNumbers CalAiPhoneNumber[]
agents Agent[]
bookingReports BookingReport[]
features TeamFeatures[]
/// @zod.import(["import { intervalLimitsType } from '../../zod-utils'"]).custom.use(intervalLimitsType)
bookingLimits Json?
includeManagedEventsInLimits Boolean @default(false)
internalNotePresets InternalNotePreset[]
creditBalance CreditBalance?
organizationOnboarding OrganizationOnboarding?
// note(Lauris): if a Team has parentId it is a team, if parentId is null it is an organization, but if parentId is null and managedOrganization is set,
// it means that it is an organization managed by another organization.
managedOrganization ManagedOrganization? @relation("ManagedOrganization")
managedOrganizations ManagedOrganization[] @relation("ManagerOrganization")
filterSegments FilterSegment[]
teamBilling TeamBilling? @relation("TeamBilling")
organizationBilling OrganizationBilling? @relation("OrganizationBilling")
@@unique([slug, parentId])
@@index([parentId])
}
model CreditBalance {
id String @id @default(uuid())
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int? @unique
// user credit balances will be supported in the future
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int? @unique
additionalCredits Int @default(0)
limitReachedAt DateTime?
warningSentAt DateTime?
expenseLogs CreditExpenseLog[]
purchaseLogs CreditPurchaseLog[]
}
model CreditPurchaseLog {
id String @id @default(uuid())
creditBalanceId String
creditBalance CreditBalance @relation(fields: [creditBalanceId], references: [id], onDelete: Cascade)
credits Int
createdAt DateTime @default(now())
}
enum CreditUsageType {
SMS
CAL_AI_PHONE_CALL
}
model CreditExpenseLog {
id String @id @default(uuid())
creditBalanceId String
creditBalance CreditBalance @relation(fields: [creditBalanceId], references: [id], onDelete: Cascade)
bookingUid String?
booking Booking? @relation(fields: [bookingUid], references: [uid], onDelete: Cascade)
credits Int?
creditType CreditType
date DateTime
smsSid String?
smsSegments Int?
phoneNumber String?
email String?
callDuration Int?
creditFor CreditUsageType?
externalRef String? @unique
}
enum CreditType {
MONTHLY
ADDITIONAL
}
model OrganizationSettings {
id Int @id @default(autoincrement())
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId Int @unique
isOrganizationConfigured Boolean @default(false)
// It decides if new organization members can be auto-accepted or not
isOrganizationVerified Boolean @default(false)
// It is a domain e.g "acme.com". Any email with this domain might be auto-accepted
// Also, it is the domain to which the organization profile is redirected.
orgAutoAcceptEmail String
lockEventTypeCreationForUsers Boolean @default(false)
adminGetsNoSlotsNotification Boolean @default(false)
// It decides if instance ADMIN has reviewed the organization or not.
// It is used to allow super sensitive operations like 'impersonation of Org members by Org admin'
isAdminReviewed Boolean @default(false)
dSyncData DSyncData?
isAdminAPIEnabled Boolean @default(false)
allowSEOIndexing Boolean @default(false)
orgProfileRedirectsToVerifiedDomain Boolean @default(false)
disablePhoneOnlySMSNotifications Boolean @default(false)
}
enum MembershipRole {
MEMBER
ADMIN
OWNER
}
model Membership {
id Int @id @default(autoincrement())
teamId Int
userId Int
accepted Boolean @default(false)
role MembershipRole
customRoleId String?
customRole Role? @relation(fields: [customRoleId], references: [id])
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
disableImpersonation Boolean @default(false)
AttributeToUser AttributeToUser[]
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
Host Host[]
@@unique([userId, teamId])
@@index([teamId])
@@index([userId])
@@index([accepted])
@@index([role])
@@index([customRoleId])
}
model VerificationToken {
id Int @id @default(autoincrement())
identifier String
token String @unique
expires DateTime
expiresInDays Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamId Int?
team Team? @relation(fields: [teamId], references: [id])
secondaryEmailId Int?
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id])
@@unique([identifier, token])
@@index([token])
@@index([teamId])
@@index([secondaryEmailId])
}
model InstantMeetingToken {
id Int @id @default(autoincrement())
token String @unique
expires DateTime
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
bookingId Int? @unique
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([token])
}
model BookingReference {
id Int @id @default(autoincrement())
/// @zod.string.min(1)
type String
/// @zod.string.min(1)
uid String
meetingId String?
thirdPartyRecurringEventId String?
meetingPassword String?
meetingUrl String?
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
bookingId Int?
externalCalendarId String?
deleted Boolean?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)
credentialId Int?
delegationCredential DelegationCredential? @relation(fields: [delegationCredentialId], references: [id], onDelete: SetNull)
delegationCredentialId String?
domainWideDelegation DomainWideDelegation? @relation(fields: [domainWideDelegationCredentialId], references: [id], onDelete: SetNull)
domainWideDelegationCredentialId String?
@@index([bookingId])
@@index([type])
@@index([uid])
}
model Attendee {
id Int @id @default(autoincrement())
email String
name String
timeZone String
phoneNumber String?
locale String? @default("en")
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
bookingId Int?
bookingSeat BookingSeat?
noShow Boolean? @default(false)
@@index([email])
@@index([bookingId])
}
enum BookingStatus {
CANCELLED @map("cancelled")
ACCEPTED @map("accepted")
REJECTED @map("rejected")
PENDING @map("pending")
AWAITING_HOST @map("awaiting_host")
}
model Booking {
id Int @id @default(autoincrement())
uid String @unique
// (optional) UID based on slot start/end time & email against duplicates
idempotencyKey String? @unique
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
// User's email at the time of booking
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
userPrimaryEmail String?
references BookingReference[]
eventType EventType? @relation(fields: [eventTypeId], references: [id])
eventTypeId Int?
title String
description String?
customInputs Json?
/// @zod.import(["import { bookingResponses } from '../../zod-utils'"]).custom.use(bookingResponses)
responses Json?
startTime DateTime
endTime DateTime
attendees Attendee[]
location String?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
status BookingStatus @default(ACCEPTED)
paid Boolean @default(false)
payment Payment[]
destinationCalendar DestinationCalendar? @relation(fields: [destinationCalendarId], references: [id])
destinationCalendarId Int?
cancellationReason String?
rejectionReason String?
reassignReason String?
reassignBy User? @relation("reassignByUser", fields: [reassignById], references: [id])
reassignById Int?
dynamicEventSlugRef String?
dynamicGroupSlugRef String?
rescheduled Boolean?
fromReschedule String?
recurringEventId String?
smsReminderNumber String?
workflowReminders WorkflowReminder[]
scheduledJobs String[] // scheduledJobs is deprecated, please use scheduledTriggers instead
seatsReferences BookingSeat[]
/// @zod.import(["import { bookingMetadataSchema } from '../../zod-utils'"]).custom.use(bookingMetadataSchema)
metadata Json?
isRecorded Boolean @default(false)
iCalUID String? @default("")
iCalSequence Int @default(0)
instantMeetingToken InstantMeetingToken?
rating Int?
ratingFeedback String?
noShowHost Boolean? @default(false)
scheduledTriggers WebhookScheduledTriggers[]
oneTimePassword String? @unique @default(uuid())
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
cancelledBy String?
/// @zod.import(["import { emailSchema } from '@calcom/lib/emailSchema'"]).custom.use(emailSchema)
rescheduledBy String?
// Ah, made a typo here. Should have been routedFromRoutingFormRe"s"ponse. Live with it :(
routedFromRoutingFormReponse App_RoutingForms_FormResponse?
assignmentReason AssignmentReason[]
internalNote BookingInternalNote[]
creationSource CreationSource?
tracking Tracking?
routingFormResponses RoutingFormResponseDenormalized[]
expenseLogs CreditExpenseLog[]
report BookingReport?
@@index([eventTypeId])
@@index([userId])
@@index([destinationCalendarId])
@@index([recurringEventId])
@@index([uid])
@@index([status])
@@index([startTime, endTime, status])
}
model Tracking {
id Int @id @default(autoincrement())
bookingId Int
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
utm_source String?
utm_medium String?
utm_campaign String?
utm_term String?
utm_content String?
@@unique([bookingId])
}
model Schedule {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
eventType EventType[]
instantMeetingEvents EventType[] @relation("InstantMeetingSchedule")
restrictionSchedule EventType[] @relation("restrictionSchedule")
name String
timeZone String?
availability Availability[]
Host Host[]
@@index([userId])
}
model Availability {
id Int @id @default(autoincrement())
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int?
eventType EventType? @relation(fields: [eventTypeId], references: [id])
eventTypeId Int?
days Int[]
startTime DateTime @db.Time
endTime DateTime @db.Time
date DateTime? @db.Date
Schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
@@index([userId])
@@index([eventTypeId])
@@index([scheduleId])
}
model SelectedCalendar {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
integration String
externalId String
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
credentialId Int?
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
// Used to identify a watched calendar channel in Google Calendar
// @deprecated use channelId instead
googleChannelId String?
// @deprecated use channelKind instead
googleChannelKind String?
// @deprecated use channelResourceId instead
googleChannelResourceId String?
// @deprecated use channelResourceUri instead
googleChannelResourceUri String?
// @deprecated use channelExpoiration instead
googleChannelExpiration String?
// Used to identify a watched calendar
channelId String?
channelKind String?
channelResourceId String?
channelResourceUri String?
channelExpiration DateTime? @db.Timestamp(3)
// Used to calendar cache and sync
syncSubscribedAt DateTime? @db.Timestamp(3)
syncToken String?
syncedAt DateTime?
syncErrorAt DateTime?
syncErrorCount Int? @default(0)
delegationCredential DelegationCredential? @relation(fields: [delegationCredentialId], references: [id], onDelete: Cascade)
delegationCredentialId String?
// Deprecated and unused: Use delegationCredential instead
domainWideDelegationCredential DomainWideDelegation? @relation(fields: [domainWideDelegationCredentialId], references: [id], onDelete: Cascade)
domainWideDelegationCredentialId String?
error String?
lastErrorAt DateTime?
watchAttempts Int @default(0)
unwatchAttempts Int @default(0)
maxAttempts Int @default(3)
eventTypeId Int?
eventType EventType? @relation(fields: [eventTypeId], references: [id])
calendarCacheEvents CalendarCacheEvent[]
// It could still allow multiple user-level(eventTypeId is null) selected calendars for same userId, integration, externalId because NULL is not equal to NULL
// We currently ensure uniqueness by checking for the existence of the record before creating a new one
// Think about introducing a generated unique key ${userId}_${integration}_${externalId}_${eventTypeId}
@@unique([userId, integration, externalId, eventTypeId])
@@unique([googleChannelId, eventTypeId])
@@index([userId])
@@index([externalId])
@@index([eventTypeId])
@@index([credentialId])
// Composite indices to optimize calendar-cache queries
@@index([integration, googleChannelExpiration, error, watchAttempts, maxAttempts], name: "SelectedCalendar_watch_idx")
@@index([integration, googleChannelExpiration, error, unwatchAttempts, maxAttempts], name: "SelectedCalendar_unwatch_idx")
}
enum EventTypeCustomInputType {
TEXT @map("text")
TEXTLONG @map("textLong")
NUMBER @map("number")
BOOL @map("bool")
RADIO @map("radio")
PHONE @map("phone")
}
model EventTypeCustomInput {
id Int @id @default(autoincrement())
eventTypeId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
label String
type EventTypeCustomInputType
/// @zod.import(["import { customInputOptionSchema } from '../../zod-utils'"]).custom.use(customInputOptionSchema)
options Json?
required Boolean
placeholder String @default("")
@@index([eventTypeId])
}
model ResetPasswordRequest {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String
expires DateTime
}
enum ReminderType {
PENDING_BOOKING_CONFIRMATION
}
model ReminderMail {
id Int @id @default(autoincrement())
referenceId Int
reminderType ReminderType
elapsedMinutes Int
createdAt DateTime @default(now())
@@index([referenceId])
@@index([reminderType])
}
model Payment {
id Int @id @default(autoincrement())
uid String @unique
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String?
bookingId Int
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
amount Int
fee Int
currency String
success Boolean
refunded Boolean
data Json
externalId String @unique
paymentOption PaymentOption? @default(ON_BOOKING)
@@index([bookingId])
@@index([externalId])
}
enum PaymentOption {
ON_BOOKING
HOLD
}
enum WebhookTriggerEvents {
BOOKING_CREATED
BOOKING_PAYMENT_INITIATED
BOOKING_PAID
BOOKING_RESCHEDULED
BOOKING_REQUESTED
BOOKING_CANCELLED
BOOKING_REJECTED
BOOKING_NO_SHOW_UPDATED
FORM_SUBMITTED
MEETING_ENDED
MEETING_STARTED
RECORDING_READY
INSTANT_MEETING
RECORDING_TRANSCRIPTION_GENERATED
OOO_CREATED
AFTER_HOSTS_CAL_VIDEO_NO_SHOW
AFTER_GUESTS_CAL_VIDEO_NO_SHOW
FORM_SUBMITTED_NO_EVENT
}
model Webhook {
id String @id @unique
userId Int?
teamId Int?
eventTypeId Int?
platformOAuthClientId String?
/// @zod.string.url()
subscriberUrl String
payloadTemplate String?
createdAt DateTime @default(now())
active Boolean @default(true)
eventTriggers WebhookTriggerEvents[]
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
platformOAuthClient PlatformOAuthClient? @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String?
secret String?
platform Boolean @default(false)
scheduledTriggers WebhookScheduledTriggers[]
time Int?
timeUnit TimeUnit?
@@unique([userId, subscriberUrl], name: "courseIdentifier")
@@unique([platformOAuthClientId, subscriberUrl], name: "oauthclientwebhook")
@@index([active])
}
model Impersonations {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
impersonatedUser User @relation("impersonated_user", fields: [impersonatedUserId], references: [id], onDelete: Cascade)
impersonatedBy User @relation("impersonated_by_user", fields: [impersonatedById], references: [id], onDelete: Cascade)
impersonatedUserId Int
impersonatedById Int
@@index([impersonatedUserId])
@@index([impersonatedById])
}
model ApiKey {
id String @id @unique @default(cuid())
userId Int
teamId Int?
note String?
createdAt DateTime @default(now())
expiresAt DateTime?
lastUsedAt DateTime?
hashedKey String @unique()
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String?
rateLimits RateLimit[]
@@index([userId])
}
model RateLimit {
id String @id @default(uuid())
name String
apiKeyId String
ttl Int
limit Int
blockDuration Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
@@index([apiKeyId])
}
model HashedLink {
id Int @id @default(autoincrement())
link String @unique
eventTypeId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
expiresAt DateTime?
maxUsageCount Int @default(1)
usageCount Int @default(0)
@@index([eventTypeId])
}
model Account {
id String @id @default(cuid())
userId Int
type String
provider String
providerAccountId String
providerEmail String?
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
@@index([type])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId Int
expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
enum AppCategories {
calendar
messaging
other
payment
video // deprecated, please use 'conferencing' instead
web3 // deprecated, we should no longer have any web3 apps
automation
analytics
// Wherever video is in use, conferencing should also be used for legacy apps can have it.
conferencing
crm
}
model App {
// The slug for the app store public page inside `/apps/[slug]`
slug String @id @unique
// The directory name for `/packages/app-store/[dirName]`
dirName String @unique
// Needed API Keys
keys Json?
// One or multiple categories to which this app belongs
categories AppCategories[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
credentials Credential[]
payments Payment[]
Webhook Webhook[]
ApiKey ApiKey[]
enabled Boolean @default(false)
@@index([enabled])
}
model App_RoutingForms_Form {
id String @id @default(cuid())
description String?
position Int @default(0)
routes Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
fields Json?
user User @relation("routing-form", fields: [userId], references: [id], onDelete: Cascade)
updatedBy User? @relation("updated-routing-form", fields: [updatedById], references: [id], onDelete: SetNull)
updatedById Int?
// This is the user who created the form and also the user who has read-write access to the form
// If teamId is set, the members of the team would also have access to form readOnly or read-write depending on their permission level as team member.
userId Int
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
responses App_RoutingForms_FormResponse[]
queuedResponses App_RoutingForms_QueuedFormResponse[]
disabled Boolean @default(false)
/// @zod.import(["import { RoutingFormSettings } from '../../zod-utils'"]).custom.use(RoutingFormSettings)
settings Json?
incompleteBookingActions App_RoutingForms_IncompleteBookingActions[]
workflows WorkflowsOnRoutingForms[]
@@index([userId])
@@index([disabled])
}
model App_RoutingForms_FormResponse {
id Int @id @default(autoincrement())
uuid String? @default(uuid())
formFillerId String @default(cuid())
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
response Json
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
routedToBookingUid String? @unique
// We should not cascade delete the booking, because we want to keep the form response even if the routedToBooking is deleted
routedToBooking Booking? @relation(fields: [routedToBookingUid], references: [uid])
chosenRouteId String?
routingFormResponseFields RoutingFormResponseField[]
routingFormResponses RoutingFormResponseDenormalized[]
queuedFormResponse App_RoutingForms_QueuedFormResponse?
@@unique([formFillerId, formId])
@@index([formFillerId])
@@index([formId])
@@index([routedToBookingUid])
}
model App_RoutingForms_QueuedFormResponse {
id String @id @default(cuid())
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
response Json
chosenRouteId String?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
actualResponseId Int? @unique
actualResponse App_RoutingForms_FormResponse? @relation(fields: [actualResponseId], references: [id], onDelete: Cascade)
}
model RoutingFormResponseField {
id Int @id @default(autoincrement())
responseId Int
fieldId String
valueString String?
valueNumber Decimal?
valueStringArray String[]
response App_RoutingForms_FormResponse @relation(fields: [responseId], references: [id], map: "RoutingFormResponseField_response_fkey", onDelete: Cascade)
denormalized RoutingFormResponseDenormalized @relation("DenormalizedResponseToFields", fields: [responseId], references: [id], onDelete: Cascade)
@@index([responseId])
@@index([fieldId])
@@index([valueNumber])
@@index([valueStringArray], type: Gin)
}
view RoutingFormResponse {
id Int @unique
response Json
responseLowercase Json
formId String
formName String
formTeamId Int?
formUserId Int?
bookingUid String?
bookingStatus BookingStatus?
bookingStatusOrder Int?
bookingCreatedAt DateTime?
bookingAttendees Json? // Array of {timeZone: string, email: string}
bookingUserId Int?
bookingUserName String?
bookingUserEmail String?
bookingUserAvatarUrl String?
bookingAssignmentReason String?
bookingAssignmentReasonLowercase String?
bookingStartTime DateTime?
bookingEndTime DateTime?
createdAt DateTime
utm_source String?
utm_medium String?
utm_campaign String?
utm_term String?
utm_content String?
}
model RoutingFormResponseDenormalized {
id Int @id
uuid String?
formId String
formName String
formTeamId Int?
formUserId Int
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: SetNull)
bookingUid String?
bookingId Int?
bookingStatus BookingStatus?
bookingStatusOrder Int?
bookingCreatedAt DateTime? @db.Timestamp(3)
bookingStartTime DateTime? @db.Timestamp(3)
bookingEndTime DateTime? @db.Timestamp(3)
bookingUserId Int?
bookingUserName String?
bookingUserEmail String?
bookingUserAvatarUrl String?
bookingAssignmentReason String?
eventTypeId Int?
eventTypeParentId Int?
eventTypeSchedulingType String?
createdAt DateTime @db.Timestamp(3)
utm_source String?
utm_medium String?
utm_campaign String?
utm_term String?
utm_content String?
response App_RoutingForms_FormResponse @relation(fields: [id], references: [id], onDelete: Cascade)
fields RoutingFormResponseField[] @relation("DenormalizedResponseToFields")
@@index([formId])
@@index([formTeamId])
@@index([formUserId])
@@index([formId, createdAt])
@@index([bookingId])
@@index([bookingUserId])
@@index([eventTypeId, eventTypeParentId])
}
model Feedback {
id Int @id @default(autoincrement())
date DateTime @default(now())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
rating String
comment String?
@@index([userId])
@@index([rating])
}
enum WorkflowTriggerEvents {
BEFORE_EVENT
EVENT_CANCELLED
NEW_EVENT
AFTER_EVENT
RESCHEDULE_EVENT
AFTER_HOSTS_CAL_VIDEO_NO_SHOW
AFTER_GUESTS_CAL_VIDEO_NO_SHOW
FORM_SUBMITTED
FORM_SUBMITTED_NO_EVENT
BOOKING_REJECTED
BOOKING_REQUESTED
BOOKING_PAYMENT_INITIATED
BOOKING_PAID
BOOKING_NO_SHOW_UPDATED
}
enum WorkflowActions {
EMAIL_HOST
EMAIL_ATTENDEE
SMS_ATTENDEE
SMS_NUMBER
EMAIL_ADDRESS
WHATSAPP_ATTENDEE
WHATSAPP_NUMBER
CAL_AI_PHONE_CALL
}
enum WorkflowType {
EVENT_TYPE
ROUTING_FORM
}
model WorkflowStep {
id Int @id @default(autoincrement())
stepNumber Int
action WorkflowActions
workflowId Int
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
sendTo String?
reminderBody String?
emailSubject String?
template WorkflowTemplates @default(REMINDER)
workflowReminders WorkflowReminder[]
numberRequired Boolean?
sender String?
numberVerificationPending Boolean @default(true)
includeCalendarEvent Boolean @default(false)
verifiedAt DateTime?
agentId String? @unique
agent Agent? @relation(fields: [agentId], references: [id], onDelete: SetNull)
inboundAgentId String? @unique
inboundAgent Agent? @relation("WorkflowStepInboundAgent", fields: [inboundAgentId], references: [id], onDelete: SetNull)
@@index([workflowId])
}
model Workflow {
id Int @id @default(autoincrement())
position Int @default(0)
name String
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
activeOn WorkflowsOnEventTypes[]
activeOnTeams WorkflowsOnTeams[]
activeOnRoutingForms WorkflowsOnRoutingForms[]
isActiveOnAll Boolean @default(false)
trigger WorkflowTriggerEvents
time Int?
timeUnit TimeUnit?
steps WorkflowStep[]
type WorkflowType @default(EVENT_TYPE)
@@index([userId])
@@index([teamId])
}
model AIPhoneCallConfiguration {
id Int @id @default(autoincrement())
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
templateType String @default("CUSTOM_TEMPLATE")
schedulerName String?
generalPrompt String?
yourPhoneNumber String
numberToCall String
guestName String?
guestEmail String?
guestCompany String?
enabled Boolean @default(false)
beginMessage String?
llmId String?
@@unique([eventTypeId])
@@index([eventTypeId])
}
model WorkflowsOnEventTypes {
id Int @id @default(autoincrement())
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
workflowId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
@@unique([workflowId, eventTypeId])
@@index([workflowId])
@@index([eventTypeId])
}
model WorkflowsOnRoutingForms {
id Int @id @default(autoincrement())
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
workflowId Int
routingForm App_RoutingForms_Form @relation(fields: [routingFormId], references: [id], onDelete: Cascade)
routingFormId String
@@unique([workflowId, routingFormId])
@@index([workflowId])
@@index([routingFormId])
}
model WorkflowsOnTeams {
id Int @id @default(autoincrement())
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
workflowId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int
@@unique([workflowId, teamId])
@@index([workflowId])
@@index([teamId])
}
model Deployment {
/// This is a single row table, so we use a fixed id
id Int @id @default(1)
logo String?
/// @zod.import(["import { DeploymentTheme } from '../../zod-utils'"]).custom.use(DeploymentTheme)
theme Json?
licenseKey String?
// We encrypt the signature token in the deployment table with the current calendso encryption key
signatureTokenEncrypted String?
agreedLicenseAt DateTime?
}
enum TimeUnit {
DAY @map("day")
HOUR @map("hour")
MINUTE @map("minute")
}
model WorkflowReminder {
id Int @id @default(autoincrement())
uuid String? @unique @default(uuid())
bookingUid String?
booking Booking? @relation(fields: [bookingUid], references: [uid])
method WorkflowMethods
scheduledDate DateTime
referenceId String? @unique
scheduled Boolean
workflowStepId Int?
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id], onDelete: Cascade)
cancelled Boolean?
seatReferenceId String?
isMandatoryReminder Boolean? @default(false)
retryCount Int @default(0)
@@index([bookingUid])
@@index([workflowStepId])
@@index([seatReferenceId])
@@index([method, scheduled, scheduledDate])
@@index([cancelled, scheduledDate])
}
model WebhookScheduledTriggers {
id Int @id @default(autoincrement())
jobName String? // jobName is deprecated, not needed when webhook and booking is set
subscriberUrl String
payload String
startAfter DateTime
retryCount Int @default(0)
createdAt DateTime? @default(now())
appId String?
webhookId String?
webhook Webhook? @relation(fields: [webhookId], references: [id], onDelete: Cascade)
bookingId Int?
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
}
enum WorkflowTemplates {
REMINDER
CUSTOM
CANCELLED
RESCHEDULED
COMPLETED
RATING
}
enum WorkflowMethods {
EMAIL
SMS
WHATSAPP
AI_PHONE_CALL
}
model BookingSeat {
id Int @id @default(autoincrement())
referenceUid String @unique
bookingId Int
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
attendeeId Int @unique
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
/// @zod.import(["import { bookingSeatDataSchema } from '../../zod-utils'"]).custom.use(bookingSeatDataSchema)
data Json?
metadata Json?
@@index([bookingId])
@@index([attendeeId])
}
model VerifiedNumber {
id Int @id @default(autoincrement())
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
phoneNumber String
@@index([userId])
@@index([teamId])
}
model VerifiedEmail {
id Int @id @default(autoincrement())
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
email String
@@index([userId])
@@index([teamId])
}
model Feature {
// The feature slug, ex: 'v2-workflows'
slug String @id @unique
// If the feature is currently enabled
enabled Boolean @default(false)
// A short description of the feature
description String?
// The type of feature flag
type FeatureType? @default(RELEASE)
// If the flag is considered stale
stale Boolean? @default(false)
lastUsedAt DateTime?
createdAt DateTime? @default(now())
updatedAt DateTime? @default(now()) @updatedAt
updatedBy Int?
users UserFeatures[]
teams TeamFeatures[]
@@index([enabled])
@@index([stale])
}
model UserFeatures {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
feature Feature @relation(fields: [featureId], references: [slug], onDelete: Cascade)
featureId String
assignedAt DateTime @default(now())
assignedBy String
updatedAt DateTime @updatedAt
@@id([userId, featureId])
@@index([userId, featureId])
}
model TeamFeatures {
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int
feature Feature @relation(fields: [featureId], references: [slug], onDelete: Cascade)
featureId String
assignedAt DateTime @default(now())
assignedBy String
updatedAt DateTime @updatedAt
@@id([teamId, featureId])
@@index([teamId, featureId])
}
enum FeatureType {
RELEASE
EXPERIMENT
OPERATIONAL
KILL_SWITCH
PERMISSION
}
enum RRResetInterval {
MONTH
DAY
}
enum RRTimestampBasis {
CREATED_AT
START_TIME
}
model SelectedSlots {
id Int @id @default(autoincrement())
eventTypeId Int
userId Int
slotUtcStartDate DateTime
slotUtcEndDate DateTime
uid String
releaseAt DateTime
isSeat Boolean @default(false)
@@unique(fields: [userId, slotUtcStartDate, slotUtcEndDate, uid], name: "selectedSlotUnique")
}
model OAuthClient {
clientId String @id @unique
redirectUri String
clientSecret String
name String
logo String?
accessCodes AccessCode[]
}
model AccessCode {
id Int @id @default(autoincrement())
code String
clientId String?
client OAuthClient? @relation(fields: [clientId], references: [clientId], onDelete: Cascade)
expiresAt DateTime
scopes AccessScope[]
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
}
enum AccessScope {
READ_BOOKING
READ_PROFILE
}
view BookingTimeStatus {
id Int @unique
uid String?
eventTypeId Int?
title String?
description String?
startTime DateTime?
endTime DateTime?
createdAt DateTime?
location String?
paid Boolean?
status BookingStatus?
rescheduled Boolean?
userId Int?
teamId Int?
eventLength Int?
timeStatus String?
eventParentId Int?
userEmail String?
username String?
ratingFeedback String?
rating Int?
noShowHost Boolean?
isTeamBooking Boolean
}
model BookingDenormalized {
id Int @id @unique
uid String
eventTypeId Int?
title String
description String?
startTime DateTime
endTime DateTime
createdAt DateTime
updatedAt DateTime?
location String?
paid Boolean
status BookingStatus
rescheduled Boolean?
userId Int?
teamId Int?
eventLength Int?
eventParentId Int?
userEmail String?
userName String?
userUsername String?
ratingFeedback String?
rating Int?
noShowHost Boolean?
isTeamBooking Boolean
@@index([userId])
@@index([createdAt])
@@index([eventTypeId])
@@index([eventParentId])
@@index([teamId])
@@index([startTime])
@@index([endTime])
@@index([status])
@@index([teamId, isTeamBooking])
@@index([userId, isTeamBooking])
@@index([startTime, endTime])
}
view BookingTimeStatusDenormalized {
id Int @unique
uid String
eventTypeId Int?
title String
description String?
startTime DateTime
endTime DateTime
createdAt DateTime
updatedAt DateTime?
location String?
paid Boolean
status BookingStatus
rescheduled Boolean?
userId Int?
teamId Int?
eventLength Int?
eventParentId Int?
userEmail String?
userName String?
userUsername String?
ratingFeedback String?
rating Int?
noShowHost Boolean?
isTeamBooking Boolean
timeStatus String? // this is the addition on top of BookingDenormalized
}
model CalendarCache {
// To be made required in a followup
id String? @default(uuid())
// The key would be the unique URL that is requested by the user
key String
value Json
expiresAt DateTime
// Provide an initial value for legacy rows and future raw inserts
updatedAt DateTime @default(now()) @updatedAt
credentialId Int
userId Int?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
@@id([credentialId, key])
@@unique([credentialId, key])
@@index([userId, key])
}
enum RedirectType {
UserEventType @map("user-event-type")
TeamEventType @map("team-event-type")
User @map("user")
Team @map("team")
}
model TempOrgRedirect {
id Int @id @default(autoincrement())
// Better would be to have fromOrgId and toOrgId as well and then we should have just to instead toUrl
from String
// 0 would mean it is non org
fromOrgId Int
type RedirectType
// It doesn't have any query params
toUrl String
enabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([from, type, fromOrgId])
}
model Avatar {
// e.g. NULL(0), organization ID or team logo
teamId Int @default(0)
// Avatar, NULL(0) if team logo
userId Int @default(0)
// base64 string
data String
// different every time to pop the cache.
objectKey String @unique
isBanner Boolean @default(false)
@@unique([teamId, userId, isBanner])
@@map(name: "avatars")
}
model OutOfOfficeEntry {
id Int @id @default(autoincrement())
uuid String @unique
start DateTime
end DateTime
notes String?
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
toUserId Int?
toUser User? @relation(name: "toUser", fields: [toUserId], references: [id], onDelete: Cascade)
reasonId Int?
reason OutOfOfficeReason? @relation(fields: [reasonId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([uuid])
@@index([userId])
@@index([toUserId])
@@index([start, end])
}
model OutOfOfficeReason {
id Int @id @default(autoincrement())
emoji String
reason String @unique
enabled Boolean @default(true)
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
entries OutOfOfficeEntry[]
}
// Platform
model PlatformOAuthClient {
id String @id @default(cuid())
name String
secret String
permissions Int
users User[]
logo String?
redirectUris String[]
organizationId Int
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
teams Team[] @relation("CreatedByOAuthClient")
accessTokens AccessToken[]
refreshToken RefreshToken[]
authorizationTokens PlatformAuthorizationToken[]
webhook Webhook[]
bookingRedirectUri String?
bookingCancelRedirectUri String?
bookingRescheduleRedirectUri String?
areEmailsEnabled Boolean @default(false)
areDefaultEventTypesEnabled Boolean @default(true)
areCalendarEventsEnabled Boolean @default(true)
createdAt DateTime @default(now())
}
model PlatformAuthorizationToken {
id String @id @default(cuid())
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
createdAt DateTime @default(now())
@@unique([userId, platformOAuthClientId])
}
model AccessToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
}
model RefreshToken {
id Int @id @default(autoincrement())
secret String @unique
createdAt DateTime @default(now())
expiresAt DateTime
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
client PlatformOAuthClient @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
platformOAuthClientId String
userId Int
}
model DSyncData {
id Int @id @default(autoincrement())
directoryId String @unique
tenant String
organizationId Int? @unique
org OrganizationSettings? @relation(fields: [organizationId], references: [organizationId], onDelete: Cascade)
teamGroupMapping DSyncTeamGroupMapping[]
createdAttributeToUsers AttributeToUser[] @relation("createdByDSync")
updatedAttributeToUsers AttributeToUser[] @relation("updatedByDSync")
}
model DSyncTeamGroupMapping {
id Int @id @default(autoincrement())
organizationId Int
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
directoryId String
directory DSyncData @relation(fields: [directoryId], references: [directoryId], onDelete: Cascade)
groupName String
@@unique([teamId, groupName])
}
model SecondaryEmail {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
email String
emailVerified DateTime?
verificationTokens VerificationToken[]
eventTypes EventType[]
@@unique([email])
@@unique([userId, email])
@@index([userId])
}
// Needed to store tasks that need to be processed by a background worker or Tasker
model Task {
id String @id @unique @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// The time at which the task should be executed
scheduledAt DateTime @default(now())
// The time at which the task was successfully executed
succeededAt DateTime?
// The task type to be executed. Left it as a freeform string to avoid more migrations for now. Will be enforced at type level.
type String
// Generic payload for the task
payload String
// The number of times the task has been attempted
attempts Int @default(0)
// The maximum number of times the task can be attempted
maxAttempts Int @default(3)
lastError String?
lastFailedAttemptAt DateTime?
referenceUid String?
// unique index on referenceUid,type to avoid duplicate tasks
@@unique([referenceUid, type])
// for finding succeeded tasks
@@index([succeededAt])
// for finding tasks that are scheduled to be executed
@@index([scheduledAt, succeededAt])
}
enum SMSLockState {
LOCKED
UNLOCKED
REVIEW_NEEDED
}
model ManagedOrganization {
managedOrganizationId Int @unique
managedOrganization Team @relation("ManagedOrganization", fields: [managedOrganizationId], references: [id], onDelete: Cascade)
managerOrganizationId Int
managerOrganization Team @relation("ManagerOrganization", fields: [managerOrganizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([managerOrganizationId, managedOrganizationId])
@@index([managerOrganizationId])
}
model PlatformBilling {
id Int @id @unique // team id
customerId String
subscriptionId String?
priceId String?
plan String @default("none")
billingCycleStart Int?
billingCycleEnd Int?
overdue Boolean? @default(false)
// note(Lauris): in case of a platform managed organization's billing record this field points to the manager organization's billing record.
managerBillingId Int?
managerBilling PlatformBilling? @relation("PlatformManagedBilling", fields: [managerBillingId], references: [id])
// note(Lauris): in case of a manager organization's billing record this field points to billing records of its platform managed organizations.
managedBillings PlatformBilling[] @relation("PlatformManagedBilling")
team Team @relation(fields: [id], references: [id], onDelete: Cascade)
}
enum AttributeType {
TEXT
NUMBER
SINGLE_SELECT
MULTI_SELECT
}
model AttributeOption {
id String @id @default(uuid())
attribute Attribute @relation(fields: [attributeId], references: [id], onDelete: Cascade)
attributeId String
value String
slug String
isGroup Boolean @default(false)
// It is a list of AttributeOptions ids that are contained in the group option
// You could think of a person having the group option to actually have all the options in the contains list.
// We are not using relation here because it would be a many to many relation because a group option can contain many non-group options and a non-group option can be contained in many group options
// Such a relation would require its own table to be managed and we don't need it for now.
contains String[]
assignedUsers AttributeToUser[]
}
model Attribute {
id String @id @default(uuid())
// This is organization
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
// This is organizationId
teamId Int
type AttributeType
name String
slug String
enabled Boolean @default(true)
usersCanEditRelation Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
options AttributeOption[]
isWeightsEnabled Boolean @default(false)
isLocked Boolean @default(false)
@@unique([teamId, slug])
@@index([teamId])
}
model AttributeToUser {
id String @id @default(uuid())
// This is the membership of the organization
member Membership @relation(fields: [memberId], references: [id], onDelete: Cascade)
// This is the membership id of the organization
memberId Int
attributeOption AttributeOption @relation(fields: [attributeOptionId], references: [id], onDelete: Cascade)
attributeOptionId String
weight Int?
// We don't intentionally delete assignments on deletion of a user/directory sync
createdAt DateTime @default(now())
createdById Int?
createdBy User? @relation("createdBy", fields: [createdById], references: [id], onDelete: SetNull)
createdByDSyncId String?
createdByDSync DSyncData? @relation("createdByDSync", fields: [createdByDSyncId], references: [directoryId], onDelete: SetNull)
updatedAt DateTime? @updatedAt
updatedBy User? @relation("updatedBy", fields: [updatedById], references: [id], onDelete: SetNull)
updatedById Int?
updatedByDSyncId String?
updatedByDSync DSyncData? @relation("updatedByDSync", fields: [updatedByDSyncId], references: [directoryId], onDelete: SetNull)
@@unique([memberId, attributeOptionId])
}
enum AssignmentReasonEnum {
ROUTING_FORM_ROUTING
ROUTING_FORM_ROUTING_FALLBACK
REASSIGNED
RR_REASSIGNED
REROUTED
SALESFORCE_ASSIGNMENT
}
model AssignmentReason {
id Int @id @unique @default(autoincrement())
createdAt DateTime @default(now())
bookingId Int
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
reasonEnum AssignmentReasonEnum
reasonString String
@@index([bookingId])
}
enum EventTypeAutoTranslatedField {
DESCRIPTION
TITLE
}
model DelegationCredential {
id String @id @default(uuid())
workspacePlatform WorkspacePlatform @relation(fields: [workspacePlatformId], references: [id], onDelete: Cascade)
workspacePlatformId Int
// Provides possibility to have different service accounts for different organizations if the need arises, but normally they should be the same
/// @zod.import(["import { serviceAccountKeySchema } from '../../zod-utils'"]).custom.use(serviceAccountKeySchema)
serviceAccountKey Json
enabled Boolean @default(false)
// lastEnabledAt is set when the delegation credential is enabled
lastEnabledAt DateTime?
// lastDisabledAt is set when the delegation credential is disabled. So, lastDisabledAt could be earlier then lastEnabledAt if the delegation credential was enabled -> then disabled -> then enabled again.
lastDisabledAt DateTime?
organizationId Int
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
domain String
selectedCalendars SelectedCalendar[]
destinationCalendar DestinationCalendar[]
bookingReferences BookingReference[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
credentials Credential[]
// Should be fair to assume that one domain can be only on one workspace platform at a time. So, one can't have two different workspace platforms for the same domain
// Because we don't know which domain the organization might have, we couldn't make "domain" unique here as that would prevent an actual owner of the domain to be unable to use that domain if it is used by someone else.
@@unique([organizationId, domain])
@@index([enabled])
}
// Deprecated and probably unused - Use DelegationCredential instead
model DomainWideDelegation {
id String @id @default(uuid())
workspacePlatform WorkspacePlatform @relation(fields: [workspacePlatformId], references: [id], onDelete: Cascade)
workspacePlatformId Int
// Provides possibility to have different service accounts for different organizations if the need arises, but normally they should be the same
/// @zod.import(["import { serviceAccountKeySchema } from '../../zod-utils'"]).custom.use(serviceAccountKeySchema)
serviceAccountKey Json
enabled Boolean @default(false)
organizationId Int
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
domain String
selectedCalendars SelectedCalendar[]
destinationCalendar DestinationCalendar[]
bookingReferences BookingReference[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Should be fair to assume that one domain can be only on one workspace platform at a time. So, one can't have two different workspace platforms for the same domain
// Because we don't know which domain the organization might have, we couldn't make "domain" unique here as that would prevent an actual owner of the domain to be unable to use that domain if it is used by someone else.
@@unique([organizationId, domain])
}
// It is for delegation credential
model WorkspacePlatform {
id Int @id @default(autoincrement())
/// @zod.string.min(1)
slug String
/// @zod.string.min(1)
name String
description String
/// @zod.import(["import { serviceAccountKeySchema } from '../../zod-utils'"]).custom.use(serviceAccountKeySchema)
defaultServiceAccountKey Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
enabled Boolean @default(false)
delegationCredentials DelegationCredential[]
domainWideDelegations DomainWideDelegation[]
@@unique([slug])
}
model EventTypeTranslation {
uid String @id @default(cuid())
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
field EventTypeAutoTranslatedField
sourceLocale String
targetLocale String
translatedText String @db.Text
createdAt DateTime @default(now())
createdBy Int
updatedAt DateTime @updatedAt
updatedBy Int?
creator User @relation("CreatedEventTypeTranslations", fields: [createdBy], references: [id])
updater User? @relation("UpdatedEventTypeTranslations", fields: [updatedBy], references: [id], onDelete: SetNull)
@@unique([eventTypeId, field, targetLocale])
@@index([eventTypeId, field, targetLocale])
}
enum WatchlistType {
EMAIL
DOMAIN
USERNAME
}
enum WatchlistAction {
REPORT
BLOCK
ALERT
}
enum WatchlistSource {
MANUAL
FREE_DOMAIN_POLICY
}
model Watchlist {
id String @id @default(uuid()) @db.Uuid
type WatchlistType
value String
description String?
isGlobal Boolean @default(false)
organizationId Int?
action WatchlistAction @default(REPORT)
source WatchlistSource @default(MANUAL)
lastUpdatedAt DateTime @default(now())
bookingReports BookingReport[]
audits WatchlistAudit[]
@@unique([type, value, organizationId])
@@index([type, value, organizationId, action])
}
model WatchlistAudit {
id String @id @default(uuid(7)) @db.Uuid
type WatchlistType
value String
description String?
action WatchlistAction @default(REPORT)
changedAt DateTime @default(now())
changedByUserId Int?
watchlistId String @db.Uuid
watchlist Watchlist? @relation(fields: [watchlistId], references: [id], onDelete: Cascade)
@@index([watchlistId, changedAt])
}
model WatchlistEventAudit {
id String @id @default(uuid(7)) @db.Uuid
watchlistId String @db.Uuid
eventTypeId Int
actionTaken WatchlistAction
timestamp DateTime @default(now())
}
enum BookingReportReason {
SPAM
DONT_KNOW_PERSON @map("dont_know_person")
OTHER
}
model BookingReport {
id String @id @default(uuid()) @db.Uuid
bookingUid String @unique
booking Booking @relation(fields: [bookingUid], references: [uid], onDelete: Cascade)
bookerEmail String
reportedById Int?
organizationId Int?
reason BookingReportReason
description String?
cancelled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
watchlistId String? @db.Uuid
reportedBy User? @relation("ReportedBy", fields: [reportedById], references: [id], onDelete: SetNull)
organization Team? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
watchlist Watchlist? @relation(fields: [watchlistId], references: [id], onDelete: SetNull)
@@index([bookerEmail])
@@index([reportedById])
@@index([organizationId])
@@index([watchlistId])
@@index([createdAt])
}
enum BillingPeriod {
MONTHLY
ANNUALLY
}
model OrganizationOnboarding {
// TODO: Use uuid for id
id String @id @default(uuid())
// User who started the onboarding. It is different from orgOwnerEmail in case Cal.com admin is doing the onboarding for someone else.
createdBy User @relation("CreatedOrganizationOnboardings", fields: [createdById], references: [id], onDelete: Cascade)
createdById Int
createdAt DateTime @default(now())
// We keep the email only here and don't need to connect it with user because on User deletion, we don't delete the entry here.
// It is unique because an email can be the owner of only one organization at a time.
orgOwnerEmail String @unique
error String?
updatedAt DateTime @updatedAt
// TODO: updatedBy to be added when we support marking updatedBy using webhook too, as webhook also updates it
// Set after organization payment is done and the organization is created
organizationId Int? @unique
organization Team? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
billingPeriod BillingPeriod
pricePerSeat Float
seats Int
isPlatform Boolean @default(false)
// Organization info
name String
// We don't keep it unique because we don't want self-serve flows to block a slug if it isn't paid for yet.
slug String
logo String?
bio String?
brandColor String?
bannerUrl String?
isDomainConfigured Boolean @default(false)
// Set when payment intent is there.
stripeCustomerId String? @unique
// TODO: Can we make it required
stripeSubscriptionId String?
stripeSubscriptionItemId String?
/// @zod.import(["import { orgOnboardingInvitedMembersSchema } from '../../zod-utils'"]).custom.use(orgOnboardingInvitedMembersSchema)
invitedMembers Json @default("[]")
/// @zod.import(["import { orgOnboardingTeamsSchema } from '../../zod-utils'"]).custom.use(orgOnboardingTeamsSchema)
teams Json @default("[]")
// Completion status
isComplete Boolean @default(false)
@@index([orgOwnerEmail])
@@index([stripeCustomerId])
}
enum IncompleteBookingActionType {
SALESFORCE
}
model App_RoutingForms_IncompleteBookingActions {
id Int @id @default(autoincrement())
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
formId String
actionType IncompleteBookingActionType
data Json
enabled Boolean @default(true)
credentialId Int?
}
model InternalNotePreset {
id Int @id @default(autoincrement())
name String
cancellationReason String?
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int
createdAt DateTime @default(now())
BookingInternalNote BookingInternalNote[]
@@unique([teamId, name])
@@index([teamId])
}
enum FilterSegmentScope {
USER
TEAM
}
model FilterSegment {
id Int @id @default(autoincrement())
name String
// Identifies which data table this segment belongs to (e.g. "organization_members", "team_members", "bookings", etc.)
tableIdentifier String
scope FilterSegmentScope
// Filter configuration
activeFilters Json?
sorting Json?
columnVisibility Json?
columnSizing Json?
perPage Int
searchTerm String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Creator of the segment
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
// Team scope - optional, only set when scope is TEAM
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
userPreferences UserFilterSegmentPreference[]
// For user-scoped segments: scope + userId + tableIdentifier
@@index([scope, userId, tableIdentifier])
// For team-scoped segments: scope + teamId + tableIdentifier
@@index([scope, teamId, tableIdentifier])
}
model UserFilterSegmentPreference {
id Int @id @default(autoincrement())
userId Int
tableIdentifier String
segmentId Int?
systemSegmentId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
segment FilterSegment? @relation(fields: [segmentId], references: [id], onDelete: Cascade)
@@unique([userId, tableIdentifier])
@@index([userId])
@@index([segmentId])
}
model BookingInternalNote {
id Int @id @default(autoincrement())
notePreset InternalNotePreset? @relation(fields: [notePresetId], references: [id], onDelete: Cascade)
notePresetId Int?
text String?
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
bookingId Int
createdBy User @relation(fields: [createdById], references: [id])
createdById Int
createdAt DateTime @default(now())
@@unique([bookingId, notePresetId])
@@index([bookingId])
}
enum WorkflowContactType {
PHONE
EMAIL
}
model WorkflowOptOutContact {
id Int @id @default(autoincrement())
type WorkflowContactType
value String
optedOut Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, value])
}
enum RoleType {
SYSTEM
CUSTOM
}
model Role {
id String @id @default(cuid())
name String
color String?
description String?
teamId Int? // null for global roles
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
permissions RolePermission[]
memberships Membership[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type RoleType @default(CUSTOM)
@@unique([name, teamId])
@@index([teamId])
}
model RolePermission {
id String @id @default(uuid())
roleId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
resource String
action String
createdAt DateTime @default(now())
@@unique([roleId, resource, action])
@@index([roleId])
// TODO: come back to this with indexs.
@@index([action])
}
enum PhoneNumberSubscriptionStatus {
ACTIVE
PAST_DUE
CANCELLED
INCOMPLETE
INCOMPLETE_EXPIRED
TRIALING
UNPAID
}
// Cal AI Voice Agent
model Agent {
id String @id @default(uuid())
name String
// Either user-owned OR team-owned
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
// Provider-specific agent ID (e.g., Retell AI agent ID)
providerAgentId String @unique
// Event type association for inbound agents
inboundEventTypeId Int?
enabled Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
inboundPhoneNumbers CalAiPhoneNumber[] @relation("InboundAgent")
outboundPhoneNumbers CalAiPhoneNumber[] @relation("OutboundAgent")
workflowStep WorkflowStep?
inboundWorkflowStep WorkflowStep? @relation("WorkflowStepInboundAgent")
@@index([userId])
@@index([teamId])
@@index([inboundEventTypeId])
}
model CalAiPhoneNumber {
id Int @id @default(autoincrement())
// Either user-owned OR team-owned
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
phoneNumber String @unique
provider String
// Provider-specific phone number ID (e.g., Retell phone number ID)
providerPhoneNumberId String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
stripeCustomerId String?
stripeSubscriptionId String? @unique
subscriptionStatus PhoneNumberSubscriptionStatus?
// Agent associations
inboundAgentId String?
inboundAgent Agent? @relation("InboundAgent", fields: [inboundAgentId], references: [id], onDelete: SetNull)
outboundAgentId String?
outboundAgent Agent? @relation("OutboundAgent", fields: [outboundAgentId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([teamId])
@@index([inboundAgentId])
@@index([outboundAgentId])
}
model TeamBilling {
id String @id @default(uuid())
teamId Int @unique
team Team? @relation("TeamBilling", fields: [teamId], references: [id], onDelete: Cascade)
subscriptionId String @unique
subscriptionItemId String
customerId String
status String
planName String
subscriptionStart DateTime?
subscriptionTrialEnd DateTime?
subscriptionEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OrganizationBilling {
id String @id @default(uuid())
teamId Int @unique
team Team @relation("OrganizationBilling", fields: [teamId], references: [id], onDelete: Cascade)
subscriptionId String @unique
subscriptionItemId String
customerId String
status String
planName String
subscriptionStart DateTime?
subscriptionTrialEnd DateTime?
subscriptionEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum CalendarCacheEventStatus {
confirmed @map("confirmed")
tentative @map("tentative")
cancelled @map("cancelled")
}
model CalendarCacheEvent {
id String @id @default(uuid())
selectedCalendarId String
externalId String
externalEtag String
iCalUID String?
iCalSequence Int @default(0)
// Event details
summary String?
description String?
location String?
start DateTime
end DateTime
isAllDay Boolean @default(false)
timeZone String?
status CalendarCacheEventStatus @default(confirmed)
recurringEventId String?
originalStartTime DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
externalCreatedAt DateTime?
externalUpdatedAt DateTime?
selectedCalendar SelectedCalendar @relation(fields: [selectedCalendarId], references: [id], onDelete: Cascade)
@@unique([selectedCalendarId, externalId])
@@index([start, end, status])
@@index([selectedCalendarId, iCalUID])
}