From ad271ee6396ac7c3b1ff9573203c6b016e860af1 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 30 Jul 2026 16:35:24 +0200 Subject: [PATCH] Add connected account handle/provider index (#23580) ## Context Google messaging webhook notifications resolve connected accounts with an equality lookup on both `handle` and `provider`: ```ts connectedAccountRepository.find({ where: { handle: decodedData.emailAddress, provider: ConnectedAccountProvider.GOOGLE, }, }); ``` This lookup runs for incoming Gmail notifications, but `connectedAccount` currently has no index matching either predicate. As the table grows, PostgreSQL has to inspect unrelated connected-account rows for each notification. Under sustained webhook traffic, that adds avoidable database work and keeps database connections occupied longer. ## What changed - Add a composite B-tree index on `connectedAccount(handle, provider)`. - Register the index in the TypeORM entity metadata. - Add an idempotent 2.26 fast instance command to create the index for existing installations and remove it on rollback. The webhook handler and query behavior remain unchanged. ## Why this index - Both query predicates are equality conditions, so the composite index supports a targeted lookup. - `handle` is first because it is the more selective value and also makes the index useful for handle-prefixed lookups. - The index is intentionally non-unique. The same provider handle may legitimately belong to connected accounts in different workspaces, and this change must not introduce a new data constraint. - Connected accounts are read by webhooks much more frequently than their handle or provider changes, so index maintenance overhead should remain small. ## Expected impact Webhook account resolution should use an index lookup instead of scanning the connected-account table. This reduces cumulative PostgreSQL work and connection occupancy on the Gmail notification path. This is a targeted database optimization. It should reduce pressure generated by this high-frequency query, but it is not expected to resolve every source of API tail latency by itself. ## Safety and rollout - The instance command uses `CREATE INDEX IF NOT EXISTS` and `DROP INDEX IF EXISTS`. - No uniqueness or application behavior changes are introduced. - Existing rows require no data backfill. - The index adds bounded storage and write-maintenance overhead. ## Validation - `yarn nx typecheck twenty-server` - Type-aware Oxlint on the changed files - Oxfmt on the changed files - `git diff --check` Review in cubic --- ...connected-account-handle-provider-index.ts | 19 +++++++++++++++++++ .../instance-commands.constant.ts | 2 ++ .../entities/connected-account.entity.ts | 1 + 3 files changed, 22 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index.ts new file mode 100644 index 0000000000..a0d588f8f8 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index.ts @@ -0,0 +1,19 @@ +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'; + +@RegisteredInstanceCommand('2.26.0', 1785420705255) +export class AddConnectedAccountHandleProviderIndexFastInstanceCommand implements FastInstanceCommand { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE INDEX IF NOT EXISTS "IDX_CONNECTED_ACCOUNT_HANDLE_PROVIDER" ON "core"."connectedAccount" ("handle", "provider")', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'DROP INDEX IF EXISTS "core"."IDX_CONNECTED_ACCOUNT_HANDLE_PROVIDER"', + ); + } +} 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 76c90625a4..9f8ddf8ac5 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 @@ -130,6 +130,7 @@ import { AddAppTokenSsoExchangeIndexFastInstanceCommand } from './2-25/2-25-inst 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'; import { AddIsHiddenToAgentMessageFastInstanceCommand } from './2-25/2-25-instance-command-fast-1785230296000-add-is-hidden-to-agent-message'; +import { AddConnectedAccountHandleProviderIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -262,4 +263,5 @@ export const INSTANCE_COMMANDS = [ AddPageLayoutCascadeDeleteIndexesFastInstanceCommand, AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand, AddIsHiddenToAgentMessageFastInstanceCommand, + AddConnectedAccountHandleProviderIndexFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts index 0640b7135f..bfd2144fab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/connected-account/entities/connected-account.entity.ts @@ -27,6 +27,7 @@ export type ConnectedAccountVisibility = 'user' | 'workspace'; @Entity({ name: 'connectedAccount', schema: 'core' }) @Index('IDX_CONNECTED_ACCOUNT_CONNECTION_PROVIDER_ID', ['connectionProviderId']) @Index('IDX_CONNECTED_ACCOUNT_APPLICATION_ID', ['applicationId']) +@Index('IDX_CONNECTED_ACCOUNT_HANDLE_PROVIDER', ['handle', 'provider']) @Check( 'CHK_connectedAccount_accessToken_encrypted', `"accessToken" IS NULL OR "accessToken" LIKE 'enc:v2:%'`,