Files
calendar/packages/features/delegation-credentials
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Morgan
bc665cbeab fix: APIV2 team membership - Member not getting added to event-type automatically (#24780)
* fix: APIV2 team membership addition

* feat: Add trimming for email domain and orgAutoAcceptEmail in auto-accept logic

- Trim whitespace from both user email domain and orgAutoAcceptEmail
- Ensures consistent matching even with accidental whitespace
- Addresses feedback from PR review

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* simplify

* feat: Use shared OrganizationMembershipService in TRPC for consistent auto-accept logic

- Create OrganizationMembershipService.container.ts for DI in TRPC
- Update getOrgConnectionInfo to apply trimming + case-insensitive comparison
- Precompute auto-accept decisions in createNewUsersConnectToOrgIfExists using the service
- Use service in handleNewUsersInvites for consistent auto-accept determination
- Ensures both API v2 and TRPC paths use identical trimming and normalization logic

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Revert "feat: Use shared OrganizationMembershipService in TRPC for consistent auto-accept logic"

This reverts commit 0b2bd28b6e32e8c1d3dea139ca8ff9cbf402ac00.

* refactor: Unify OrganizationRepository and remove duplicate PrismaOrganizationRepository (#24869)

* refactor: Convert OrganizationRepository from static to instance methods

- Add constructor accepting deps object with prismaClient
- Convert all static methods to instance methods
- Add getOrganizationAutoAcceptSettings method
- Create singleton instance export in repository barrel file
- Update API v2 OrganizationsRepository to extend from OrganizationRepository
- Update all call sites to use singleton instance
- Add platform-libraries organizations.ts export
- Fix mock imports to use repository barrel
- Fix unsafe optional chaining in next-auth-options.ts
- Fix any types in test files with proper type inference

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update all imports to use OrganizationRepository barrel export

- Update imports from direct OrganizationRepository file to barrel export
- This ensures mocks work correctly in tests
- Fixes 202 failing tests related to organizationRepository mock

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update test mocks to use partial mock pattern

- Convert organizationMock to partial mock that preserves real class
- Add proper prisma mocks to failing test files
- Remove old OrganizationRepository mocks from test files
- This fixes test failures related to mock interception

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Export mocked singleton and update tests to use it directly

- Export mockedSingleton as organizationRepositoryMock from organizationMock
- Update delegationCredential.test.ts to import and use the exported mock
- This fixes 'vi.mocked(...).mockResolvedValue is not a function' errors

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Use platform-libraries import for API v2 OrganizationRepository

API v2 should import shared features through @calcom/platform-libraries
instead of directly from @calcom/features to maintain proper architectural
boundaries and packaging/licensing separation.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Implement DI pattern for OrganizationRepository

- Create OrganizationRepository.module.ts and .container.ts for DI
- Replace singleton pattern with getOrganizationRepository() across 20 files
- Update platform-libraries to export getOrganizationRepository
- Delete duplicate PrismaOrganizationRepository.ts
- Remove singleton export file (repositories/index.ts)
- Update test mocks to use DI container pattern
- All type checks and unit tests passing

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Implement read/write client separation in OrganizationRepository

- Updated OrganizationRepository constructor to accept optional prismaWriteClient parameter
- Routed all write operations (create, update) through prismaWrite client
- Routed all read operations (find, get) through prismaRead client
- Updated API v2 OrganizationsRepository to pass both dbRead.prisma and dbWrite.prisma to super()
- Optimized getOrganizationRepository() calls by storing in local variables to avoid repeated function calls
- This fixes the critical issue where API v2 was passing read-only client to base class with write methods

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Fix mock setup and optimize getOrganizationRepository() calls

- Fixed verify-email.test.ts mock to return mocked repository instance instead of scenario helper object
- Added mockReset to organizationMock.ts beforeEach to properly reset mock implementations between tests
- Added local variables in page.tsx to store getOrganizationRepository() result for consistency

This fixes the issue where getOrganizationRepository() was returning organizationScenarios.organizationRepository (scenario helper) instead of the actual mocked repository instance, causing findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail to be undefined.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Simplify OrganizationRepository to use single prismaClient

- Updated base OrganizationRepository constructor to accept only { prismaClient } instead of { prismaClient, prismaWriteClient? }
- Replaced this.prismaRead and this.prismaWrite with single this.prismaClient property
- Updated API v2 OrganizationsRepository to pass only dbWrite.prisma as prismaClient
- Removed unused PrismaReadService import from API v2
- All read and write operations now use the same client instance

This simplifies the architecture as requested - API v2 uses write client for all operations, and apps/web uses the client from DI container.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: Match OrganizationsRepository.findById signature with base class

The findById method in OrganizationsRepository was using a different signature
than the base OrganizationRepository class, causing type errors in CI.

Changed from: findById(organizationId: number)
Changed to: findById({ id }: { id: number })

This matches the base class signature and resolves the CI unit test failures.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update all findById call sites to use object parameter

Fixed 6 call sites in API v2 that were calling findById with a number
instead of the required { id: number } object parameter:

- is-org.guard.ts
- is-admin-api-enabled.guard.ts
- is-webhook-in-org.guard.ts
- organizations.service.ts
- managed-organizations.service.ts (2 call sites)

This resolves the API v2 build failure caused by the signature change
in OrganizationsRepository.findById to match the base class.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove redundant findById override from OrganizationsRepository

The findById method was duplicating the base class OrganizationRepository
implementation. Both methods had identical logic (filtering by isOrganization: true),
so the override was unnecessary.

Since OrganizationsRepository extends OrganizationRepository and passes
dbWrite.prisma to the base constructor, the base class method already
provides the exact same functionality.

This resolves the API v2 build failure by eliminating the duplicate method
that was causing conflicts.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Remove unncessary changes

* Store in variable

* Revert "Remove unncessary changes"

This reverts commit af9351786a21616c9508c441191c17f2374fb2cc.

* Revert dbRead/dbWrite changes

* Add organizations library to tsconfig.json

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-11-07 10:50:00 +00:00
..

Delegation Credentials

Setting up Delegation Credential for Google Calendar API

Step 0: Create a Workspace Platform(to be done once for the Cal.com instance, by the Cal.com instance admin)

  • Create a Workspace Platform through admin interface at https://app.cal.com/settings/admin/workspace-platforms
    • Use slug="google". Slug has to be exactly this. This is how we know we need to use Google Calendar and Google Meet.
    • Use "Google" as the name of the workspace platform. Change it as per your liking.
    • Optionally provide description for the workspace platform.

Step 1: Create a Google Cloud Project or use existing one(to be done once for the Cal.com instance)

Before you can create a service account, you'll need to set up a Google Cloud project.

  1. Create a Google Cloud Project:
    1. Go to the Google Cloud Console
    2. Select Create Project
    3. Give your project a name and select your billing account (if applicable)
    4. Click Create
  2. Enable the Google Calendar API:
    1. Go to the Google Cloud Console
    2. Select API & Services → Library
    3. Search for "Google Calendar API"
    4. Click Enable

Step 2: Create a Service Account in Google Cloud Console(to be done for every organization)

A service account is needed to act on behalf of users

  1. Navigate to the Service Accounts page:
    • In the Google Cloud Console, go to IAM & Admin → Service Accounts
  2. Create a New Service Account:
    • Click on Create Service Account
    • Give your service account a name and description
    • Click Create and Continue(Optional steps can be skipped)
  3. Download the Service Account Key JSON file

Step 3: Create Delegation Credential(To Be taken by Cal.com instance admin):

  • Impersonate the organization owner and go to https://app.cal.com/settings/organizations/delegation-credential to create a Delegation Credential

    • Use domain as "acme.com" if @acme.com is the email address for your Google Workspace
    • Choose Workspace Platform as "Google"
    • Add Service Account Key JSON file(obtained in Step 2)
    • Click on "Create" button

Step 4: Copy the Client ID and OAuth Scope (To Be Taken By Cal.com organization Owner/Admin in Cal.com):

Step 5: Add Client ID under Domain-Wide Delegation (To Be taken By Google Workspace Admin):

  • Go to your Google Admin Console(admin.google.com)
    • Navigate to Security → Access and Data Controls -> API controls -> Manage Domain-Wide Delegation
    • Here, you'll authorize the Client ID to access the Google Calendar API
    • Add the following API scope for Google Calendar(Full access to Google Calendar. We use it to read freebusy time and create/update events in the members' calendars)
      • https://www.googleapis.com/auth/calendar

Step 6: Enable Delegation Credential(To Be taken By Cal.com organization Owner/Admin in Cal.com):

  • Prerequisite: The owner/admin must be part of the Google Workspace to enable Delegation Credential
  • Go to https://app.cal.com/settings/organizations/delegation-credential
    • Enable Delegation Credential
      • If you have added the Client ID for correct Google Workspace, the Delegation Credential would be enabled, otherwise you would see an error message, that should help and contact support if you still face issues.

Onboarding Improvements

  • Just adding a member to the organization would do the following:
    • Member to receive events in their calendar, even if they don't login to their account and complete the onboarding process.
    • The booking location would be Google Meet, even if the user hasn't set it as default(Though Cal Video would show up as default, but we still use Google Meet in this case. We will fix it later.)
    • It would still not use their calendar for conflict checking, but user can complete the onboarding(just select one calendar there for conflict checking)
  • Onboarding process: Google Calendar is pre-installed for any new member of the organization(assuming the user has an email of the Delegation Credential domain) and Destination Calendar and Selected Calendar are configurable. On next step, Google Meet is pre-installed and shown at the top and could be set as default.

Restrictions after enabling Delegation Credential

  • Enabling Delegation Credential for a particular workspace in Cal.com(only google/outlook supported at the moment) disables the user from disconnecting that credential.

Who can create Delegation Credential and enable it?

  • Only the owner/admin of the organization can create Delegation Credential
  • Only the owner/admin of the organization can enable the created Delegation Credential. Following additional requirements are there:
    • The client ID must be added to the Google Admin Console
    • The user's email must be a member of the Google Workspace
    • The user's email must be verified.

Disabling Delegation Credential

  • Disabling Delegation Credential maintains the user's preferences in terms of SelectedCalendars and DestinationCalendar.

Developer Notes

Terminology

  • Delegation Credential: A Delegation Credential service account key along with user's email becomes the Delegation Credential which is an alternative to regular Credential in DB.
  • Delegation User Credential: A Delegation User Credential is a Credential record in DB that uses DelegationCredential record to actually access the user's calendar. A Credential record with delegationCredentialId set is a Delegation User Credential.
  • In-DB Delegation Credential: Another name for Delegation User Credential. This is used to build the CalendarCache records.
  • In-Memory Delegation Credential: It is a Credential like object but only in-memory and has id=-1. This is used to to connect with the third party Calendar. We might want to move away from In-Memory Delegation Credential to use In-DB Delegation Credential in future.

How Delegation Credential works

  • We use the Cal.com user's email to impersonate that user using Delegation Credential(which is just a service account key at the moment)
    • That gives us read/write permission to get availability of the user and create new events in their calendar.

What is a Delegation Credential?

  • A Delegation Credential service account key along with user's email becomes the Delegation Credential which is an alternative to regular Credential in DB.
  • Delegation Credential doesn't completely replace the regular credentials. Delegation Credential gives access to the cal.com user's email in Google Calendar. So, if the user needs to connect to some other email's calendar, we need to use the regular credentials.

Cron Jobs

Cron jobs ensure that for each and every member of the organization that has Delegation Credential enabled, corresponding SelectedCalendar records are there. These crons currently run every 5 minutes and process a batch in one run to avoid overloading the DB and third party CalendarAPIs, look at vercel.json for the up-to-date schedule.

  • credentials cron job creates Delegation User Credential records for all the members of the organization who don't have Delegation User Credentials yet. It also ensures that on disabling Delegation Credential, the Delegation User Credentials are deleted which automatically deletes the SelectedCalendar and CalendarCache records through DB cascade.
  • selected-calendars cron job creates SelectedCalendar records for all the Delegation User Credentials of the organization who don't have Selected Calendars yet.

Important Points

  • No Credential table entry is created when enabling Delegation Credential. The workspace platform's related apps will be considered as "installed" for the users with email matching dwd domain. An in-memory credential like object is created for this purpose. It allows avoiding creation of thousands of records for all the members of the organization when Delegation Credential is enabled.
  • Delegation Credential is applicable to Users only.
    • For team, we don't use Delegation Credential as you can impersonate a user and not team through Delegation Credential. Currently supported apps(Google Calendar and Google Meet) don't support team installation, so we could simply allow enabling Delegation Credential without any issues.
  • Disabling a workspace platform stops it from being used for any new organizations and also disables any Delegation Credential using the workspace platform from being edited.
    • It still all existing Delegation Credentials to keep on working
  • Workspace default service account is unused and is to be removed.

How apps/installed loads the credentials

  1. Identify the logged in user's email
  2. Identify the domainWideDelegations for that email's domain
  3. Build in-memory credentials for the domainWideDelegations and use them along with the actual credentials(that user might have connected) of the user
  4. We don't show the non DelegationCredential connected calendar(if there is a corresponding DelegationCredential connected calendar). Though we use the non DelegationCredential credentials to identify the selected calendars, for the DelegationCredential connected calendar.

Impact of disabling Delegation Credential

  • It immediately stops generating in-memory delegation user credentials. So, any members who haven't manually connected their Calendar and thus their calendar connections were working only because of Delegation Credential, would have their calendar connections broken.
  • Credentials cron job would delete the Delegation User Credentials which will then cascade to delete the SelectedCalendar and CalendarCache records.

Impact of enabling Delegation Credential

  • Existing calendar-cache records are re-used as we identify the relevant record by userId and key of CalendarCache record.
    • Any updates to those calendar-cache records keep on working by using the non-delegation credential attached with the SelectedCalendar record.
    • In case there is an error while watching the SelectedCalendar using non-delegation credential, we will delete the SelectedCalendar record and create a new one using Delegation User Credential.
  • For any new members, we create Credential records and SelectedCalendar records through cron jobs and thus their calendar-cache records will also be created.

Notes when testing locally

  • You need to enable the feature through feature flag.
  • You could use Acme org and login as owner1-acme@example.com
  • Make sure to change the email of the user above to your workspace owner's email(other member's email might also work). This is necessary otherwise you won't be able to enable Delegation Credential for the organization.
    • Note: After changing the email, you would have to logout and login again