From ba5cb6ba158f39a7bd2d43b5a17dd9949af96b0f Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Fri, 24 Jul 2026 15:46:59 +0200
Subject: [PATCH] fix(server): repair missing keyValuePair.applicationId on
2.23 upgrades (#23272)
Fixes #23254
## Problem
Upgrading a self-hosted instance from `2.23.x` to `2.24.0` leaves
`core.keyValuePair` without the `applicationId` column. Database-backed
config loading then fails on startup and on every refresh (~every 15s)
with:
```
column KeyValuePairEntity.applicationId does not exist
```
The frontend shows "Unable to reach the backend".
## Root cause
`AddApplicationIdToKeyValuePairFastInstanceCommand` was added in #23089
(after `2.23.x` shipped) but registered under the already-released
`2.23.0` segment:
```ts
@RegisteredInstanceCommand('2.23.0', 1784659343818)
```
The upgrade cursor is **positional and forward-only**:
- `resolveStartCursor` resumes at `lastAttemptedIndex + 1`. A
fully-upgraded `2.23.x` instance has its cursor at the last `2.23`
workspace command, which sits *after* this newly-inserted fast command
in the sequence. So the runner steps right over it and the DDL never
runs.
- The upgrade-aware metadata layer decides "applied" the same way
(`stepIndex < currentCursor` in
`upgrade-aware-entity-metadata.adapter.ts`). Since the step index is
below the cursor, the column is considered applied and is **not** hidden
from TypeORM SELECTs, so every query references a column that was never
created.
Fresh `2.24.0` installs replay the whole sequence, so only `2.23.x ->
2.24.0` upgrades are affected. The instance log `1 fast instance ... for
2.24.0` confirms the command landed in the `2.23.0` bundle rather than
`2.24.0`.
## Fix
- Add `RepairKeyValuePairApplicationIdFastInstanceCommand` under the
current version (`2.24.0`) with a fresh timestamp, so it sorts last in
the sequence and runs for every existing instance regardless of cursor
position. Its DDL mirrors the original command and is fully idempotent
(`ADD COLUMN IF NOT EXISTS`, `DROP INDEX IF EXISTS` + recreate, `ADD
VALUE IF NOT EXISTS`), so it is a no-op on healthy instances. `down()`
is intentionally empty: the column lifecycle is owned by the `2.23.0`
introduction command.
- Repoint the entity's `@WasIntroducedInUpgrade` to the new command so
the column stays hidden from queries until the repair has actually run,
eliminating the error window during the migration itself.
## Notes
- `2.24.0` (`TWENTY_CURRENT_VERSION`) is the correct target: the upgrade
sequence only covers previous + current versions, so a command under
`2.25.0` (a next version) would not run. If a version bump lands before
this merges, the command should be moved to the new current version.
- Follow-up worth considering: nothing currently prevents registering a
command under a version in `TWENTY_PREVIOUS_VERSIONS`. A startup
validation rejecting that would have caught this at PR time.
---
...51-repair-key-value-pair-application-id.ts | 57 +++++++++++++++++++
.../instance-commands.constant.ts | 2 +
.../key-value-pair/key-value-pair.entity.ts | 2 +-
3 files changed, 60 insertions(+), 1 deletion(-)
create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id.ts
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id.ts
new file mode 100644
index 0000000000..94e5d1cd6c
--- /dev/null
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id.ts
@@ -0,0 +1,57 @@
+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';
+
+// Repairs instances that upgraded from 2.23.x, where
+// AddApplicationIdToKeyValuePairFastInstanceCommand was registered under the
+// already-released 2.23.0 segment and got skipped by the forward-only upgrade
+// cursor, leaving core.keyValuePair without applicationId. The DDL mirrors that
+// command and is fully idempotent, so it is a no-op on healthy instances.
+@RegisteredInstanceCommand('2.24.0', 1784897347051)
+export class RepairKeyValuePairApplicationIdFastInstanceCommand
+ implements FastInstanceCommand
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TYPE "core"."keyValuePair_type_enum" ADD VALUE IF NOT EXISTS 'APPLICATION_VARIABLE'`,
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."keyValuePair" ADD COLUMN IF NOT EXISTS "applicationId" uuid',
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."keyValuePair" DROP CONSTRAINT IF EXISTS "FK_e31d245e30cd82307e5416450fc"',
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."keyValuePair" ADD CONSTRAINT "FK_e31d245e30cd82307e5416450fc" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
+ );
+ await queryRunner.query(
+ 'CREATE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_APPLICATION_ID" ON "core"."keyValuePair" ("applicationId")',
+ );
+
+ await queryRunner.query(
+ 'DROP INDEX IF EXISTS "core"."IDX_KEY_VALUE_PAIR_KEY_WORKSPACE_ID_NULL_USER_ID_UNIQUE"',
+ );
+ await queryRunner.query(
+ 'CREATE UNIQUE INDEX "IDX_KEY_VALUE_PAIR_KEY_WORKSPACE_ID_NULL_USER_ID_UNIQUE" ON "core"."keyValuePair" ("key", "workspaceId") WHERE "userId" IS NULL AND "applicationId" IS NULL',
+ );
+ await queryRunner.query(
+ 'DROP INDEX IF EXISTS "core"."IDX_KEY_VALUE_PAIR_KEY_NULL_USER_ID_NULL_WORKSPACE_ID_UNIQUE"',
+ );
+ await queryRunner.query(
+ 'CREATE UNIQUE INDEX "IDX_KEY_VALUE_PAIR_KEY_NULL_USER_ID_NULL_WORKSPACE_ID_UNIQUE" ON "core"."keyValuePair" ("key") WHERE "userId" IS NULL AND "workspaceId" IS NULL AND "applicationId" IS NULL',
+ );
+
+ await queryRunner.query(
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_KEY_APPLICATION_ID_WORKSPACE_UNIQUE" ON "core"."keyValuePair" ("key", "applicationId") WHERE "applicationId" IS NOT NULL AND "workspaceId" IS NOT NULL',
+ );
+ await queryRunner.query(
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_KEY_VALUE_PAIR_KEY_APPLICATION_ID_GLOBAL_UNIQUE" ON "core"."keyValuePair" ("key", "applicationId") WHERE "applicationId" IS NOT NULL AND "workspaceId" IS NULL',
+ );
+ }
+
+ // No-op: the applicationId column lifecycle is owned by the 2.23.0
+ // introduction command. This command only repairs instances that skipped it,
+ // so rolling it back must not drop the column or its constraints.
+ public async down(): Promise {}
+}
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 ebcd291fd0..7ab2c46eb0 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
@@ -123,6 +123,7 @@ import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from '.
import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index';
import { AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider';
import { AddAgentForeignKeyToRoleTargetFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-24/2-24-instance-command-fast-1784820332810-add-agent-foreign-key-to-role-target';
+import { RepairKeyValuePairApplicationIdFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -248,4 +249,5 @@ export const INSTANCE_COMMANDS = [
AddStatusesToBillingSubscriptionIndexSlowInstanceCommand,
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
AddAgentForeignKeyToRoleTargetFastInstanceCommand,
+ RepairKeyValuePairApplicationIdFastInstanceCommand,
];
diff --git a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
index fde51565c5..9eb7940190 100644
--- a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
+++ b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
@@ -108,7 +108,7 @@ export class KeyValuePairEntity {
@Column({ nullable: true, type: 'uuid' })
@WasIntroducedInUpgrade({
upgradeCommandName:
- '2.23.0_AddApplicationIdToKeyValuePairFastInstanceCommand_1784659343818',
+ '2.24.0_RepairKeyValuePairApplicationIdFastInstanceCommand_1784897347051',
})
applicationId: string | null;