Generalizing the sync step: the App Store integration pattern
EventManager doesn't special-case Google Calendar or Daily.co: its constructor calls getApps() to turn every credential the organizer has connected — calendar, video, or CRM — into one uniform, credential-bearing list, then buckets that list by type suffix into calendarCredentials, videoCredentials, and crmCredentials. From there, create/update/delete simply loop those buckets and call the same shared manager functions (CalendarManager.createEvent, videoClient.createMeeting, CrmManager) against each entry, so the App Store becomes the single place new third-party integrations plug into the booking lifecycle.
getApps(): turning installed apps into one credential-bearing array
packages/app-store/utils.ts · lines 47–108getApps walks ALL_APPS — every app registered in the App Store, generated from each app package's config.json — and for each one filters the organizer's credentials down to just the rows whose appId matches that app's slug, producing appCredentials. If filterOnCredentials is true and none of the organizer's credentials match a given app (and it isn't global), that app is dropped from the result entirely.
Some apps aren't something the organizer explicitly connects — Cal Video (daily-video) is isGlobal, so getApps synthesizes a fake credential with the exact same shape as a real one (id, type, key, userId, appId, delegatedToId...) and pushes it into appCredentials. This is how a video fallback can exist for an organizer who never installed anything.
Whatever path an app took to get here, every surviving entry is pushed in the same shape: the app's metadata spread out, plus credentials: appCredentials. A calendar app's slug and a CRM app's slug produce identical-looking records — the only thing that varies is data, never structure. That uniformity is what lets EventManager treat every connected app the same way downstream.
function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) { const apps = ALL_APPS.reduce((reducedArray, appMeta) => { const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug); if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray; let locationOption: LocationOption | null = null; /** If the app is a globally installed one, let's inject it's key */ if (appMeta.isGlobal) { const credential = { id: 0, type: appMeta.type, key: appMeta.key!, userId: 0, user: { email: "" }, teamId: null, appId: appMeta.slug, invalid: false, encryptedKey: null, delegatedTo: null, delegatedToId: null, delegationCredentialId: null, team: { name: "Default", }, }; logger.debug( `${appMeta.type} is a global app, injecting credential`, safeStringify(getPiiFreeCredential(credential)) ); appCredentials.push(credential); } /** Check if app has location option AND add it if user has credentials for it */ if (appCredentials.length > 0 && appMeta?.appData?.location) { locationOption = { value: appMeta.appData.location.type, label: appMeta.appData.location.label || "No label set", disabled: false, }; } const credential: (typeof appCredentials)[number] | null = appCredentials[0] || null; reducedArray.push({ ...appMeta, /** * @deprecated use `credentials` */ credential, credentials: appCredentials, /** Option to display in `location` field while editing event types */ locationOption, }); return reducedArray; }, [] as (App & { credential: CredentialDataWithTeamName; credentials: CredentialDataWithTeamName[]; locationOption: LocationOption | null })[]); return apps;}
EventManager splits the uniform list by lifecycle role
packages/features/bookings/lib/EventManager.ts · lines 143–173getApps(user.credentials, true) returns the uniform per-app list you just traced; flatMap flattens every app's credentials array into one flat list of individual credentials, tagging each one with the human-readable appName it came from (used later in logs and result records).
calendarCredentials keeps only credentials whose type ends in _calendar (excluding the legacy other_calendar bucket), sorted so delegation credentials and the most recently connected calendar win any tie. This is the exact array createAllCalendarEvents will later loop over — Google Calendar, Office 365, CalDAV, whatever the organizer connected.
videoCredentials keeps anything whose type ends in _video or _conferencing — Daily.co, Zoom, MS Teams, Google Meet all land in this one bucket regardless of vendor, sorted so the latest connection is tried first.
crmCredentials keeps _crm and _other_calendar types — Salesforce, HubSpot, and similar. Same partitioning idea, third bucket: the constructor doesn't know or care which specific CRM vendor it is, only that the type string ends the right way.
constructor(user: EventManagerUser, eventTypeAppMetadata?: Record<string, any>) { log.silly("Initializing EventManager", safeStringify({ user: getPiiFreeUser(user) })); const appCredentials = getApps(user.credentials, true).flatMap((app) => app.credentials.map((creds) => ({ ...creds, appName: app.name })) ); // This includes all calendar-related apps, traditional calendars such as Google Calendar this.calendarCredentials = appCredentials .filter( // Backwards compatibility until CRM manager is implemented (cred) => cred.type.endsWith("_calendar") && !cred.type.includes("other_calendar") ) // see https://github.com/calcom/cal.diy/issues/11671#issue-1923600672 // This sorting is mostly applicable for fallback which happens when there is no explicit destinationCalendar set. // That could be true for really old accounts but not for new .sort(latestCredentialFirst) // Keep Delegation Credentials first so because those credentials never expire and are preferred. // Also, those credentials have consistent permission for all the members avoiding the scenario where user doesn't give all permissions .sort(delegatedCredentialFirst); this.videoCredentials = appCredentials .filter((cred) => cred.type.endsWith("_video") || cred.type.endsWith("_conferencing")) // Whenever a new video connection is added, latest credentials are added with the highest ID. // Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order // We also don't have updatedAt or createdAt dates on credentials so this is the best we can do .sort(latestCredentialFirst); this.crmCredentials = appCredentials.filter( (cred) => cred.type.endsWith("_crm") || cred.type.endsWith("_other_calendar") ); this.appOptions = eventTypeAppMetadata; }
Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
What does getApps(credentials, true) return in packages/app-store/utils.ts?
In EventManager's constructor, what determines whether a credential ends up in calendarCredentials, videoCredentials, or crmCredentials?
Why does CalendarManager.createEvent call getCalendar(credential, ...) before doing anything provider-specific?