From 19903f89e5ac21a191f64c6837f520ce8cf89364 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 28 Jul 2026 09:54:15 +0200 Subject: [PATCH] Add indexes for channel webhook subscription external IDs (#23386) ## Context Incoming Microsoft messaging, Microsoft calendar, and Google calendar webhook notifications resolve their channel through `webhookSubscriptionExternalId`. The column was added without an index, so PostgreSQL has to scan the corresponding channel table for every notification. Under sustained webhook traffic, these repeated scans add unnecessary database work and keep core database connections occupied longer. ## What changed - Add a partial B-tree index on `messageChannel.webhookSubscriptionExternalId` - Add the equivalent index on `calendarChannel.webhookSubscriptionExternalId` - Register both indexes in the TypeORM entity metadata - Add an idempotent 2.25 fast instance upgrade command to create and remove them The webhook handlers and their queries remain unchanged. ## Why this design - The indexes contain only non-null subscription IDs, channels without an active subscription do not add index entries - A single-column index supports both the equality lookup used by Google and the `IN` lookup used by Microsoft - The indexes are intentionally non-unique, this preserves existing behavior and avoids making the upgrade fail if historical duplicate values exist - Subscription IDs are read much more often than they are updated, so index maintenance overhead should be negligible ## Expected impact Webhook channel resolution should require a targeted index lookup instead of a table scan. This reduces database work, shortens connection occupancy, and improves latency on webhook notification paths. This is a targeted database optimization. It complements the database pool changes, but is not expected to resolve every source of API tail latency by itself. ## Validation - Server typecheck passes - Oxlint and formatting checks pass - Upgrade command uses idempotent `CREATE INDEX IF NOT EXISTS` and `DROP INDEX IF EXISTS` statements Review in cubic --- ...ebhook-subscription-external-id-indexes.ts | 30 +++++++++++++++++++ .../instance-commands.constant.ts | 2 ++ .../entities/calendar-channel.entity.ts | 5 ++++ .../entities/message-channel.entity.ts | 5 ++++ 4 files changed, 42 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes.ts new file mode 100644 index 0000000000..eed4871296 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes.ts @@ -0,0 +1,30 @@ +import { type QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +const CALENDAR_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME = + 'IDX_CALENDAR_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID'; +const MESSAGE_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME = + 'IDX_MESSAGE_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID'; + +@RegisteredInstanceCommand('2.25.0', 1785173910915) +export class AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand implements FastInstanceCommand { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "${CALENDAR_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME}" ON "core"."calendarChannel" ("webhookSubscriptionExternalId") WHERE "webhookSubscriptionExternalId" IS NOT NULL`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "${MESSAGE_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME}" ON "core"."messageChannel" ("webhookSubscriptionExternalId") WHERE "webhookSubscriptionExternalId" IS NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."${MESSAGE_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME}"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."${CALENDAR_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID_INDEX_NAME}"`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index 0718617a1f..d658c05640 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -127,6 +127,7 @@ import { RepairKeyValuePairApplicationIdFastInstanceCommand } from './2-24/2-24- import { AddAgentForeignKeyToRoleTargetFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784820332810-add-agent-foreign-key-to-role-target'; import { AddAppTokenSsoExchangeIndexFastInstanceCommand } from './2-25/2-25-instance-command-fast-1785143586000-add-app-token-sso-exchange-index'; import { AddPageLayoutCascadeDeleteIndexesFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784904030251-add-page-layout-cascade-delete-indexes'; +import { AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -256,4 +257,5 @@ export const INSTANCE_COMMANDS = [ AddAgentForeignKeyToRoleTargetFastInstanceCommand, AddAppTokenSsoExchangeIndexFastInstanceCommand, AddPageLayoutCascadeDeleteIndexesFastInstanceCommand, + AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts index 08be7d5cd5..fba0c6ad8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity.ts @@ -42,6 +42,11 @@ registerEnumType(CalendarChannelContactAutoCreationPolicy, { 'isSyncEnabled', 'syncStage', ]) +@Index( + 'IDX_CALENDAR_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID', + ['webhookSubscriptionExternalId'], + { where: '"webhookSubscriptionExternalId" IS NOT NULL' }, +) export class CalendarChannelEntity extends WorkspaceRelatedEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts b/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts index eeb3035c85..f3d438ae63 100644 --- a/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/message-channel/entities/message-channel.entity.ts @@ -54,6 +54,11 @@ registerEnumType(MessageChannelPendingGroupEmailsAction, { 'isSyncEnabled', 'syncStage', ]) +@Index( + 'IDX_MESSAGE_CHANNEL_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID', + ['webhookSubscriptionExternalId'], + { where: '"webhookSubscriptionExternalId" IS NOT NULL' }, +) export class MessageChannelEntity extends WorkspaceRelatedEntity { @PrimaryGeneratedColumn('uuid') id: string;