From b91c2a6457578ea4a017242fc1fe8efd1f463eb0 Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Thu, 23 Jul 2026 19:12:32 +0200
Subject: [PATCH] fix(server): repair missing
applicationRegistration.logoFileId on upgraded instances (#23215)
## Problem
closes https://github.com/twentyhq/twenty/issues/23210
Self-hosted instances on 2.23.x fail their workspace upgrade with:
```
column ApplicationEntity__ApplicationEntity_applicationRegistration.logoFileId does not exist
at UpgradePeopleDataLabsApplicationCommand.runOnWorkspace
```
The `2-21` instance command that adds
`core."applicationRegistration"."logoFileId"` was merged ~20 minutes
after the 2.22 version bump (PR #22827, `94192a2164`), so it first
shipped in 2.22 while registered under
`@RegisteredInstanceCommand('2.21.0', ...)`.
The upgrade runner resolves its start position from the last recorded
command and only moves forward. Any instance that had already run a
2.21.x binary has its cursor past that slot, so the command is skipped
permanently and the column is never created.
`UpgradeAwareEntityMetadataAdapter` decides column visibility
positionally (`index < currentCursor`), not by whether the command
actually ran, so it keeps `logoFileId` in the SELECT list and the
instance reports "Up to date" while the column is absent.
**Affected:** instances that ran 2.21.x, then upgraded to >= 2.22.
Instances that went from <= 2.20 straight to >= 2.22 replayed the full
sequence and are fine.
`logoFileId` is populated lazily by design (NULL is a supported state),
so no backfill is added.
## Changes
**1. Idempotent DDL guard in the failing workspace command**
`2-23-workspace-command-...-upgrade-people-data-labs-application.command.ts`
now ensures the column exists at the top of `runOnWorkspace`, before the
`findOne` that crashes on affected instances. It uses the core
`DataSource` (`@InjectDataSource()`) because
`core."applicationRegistration"` is instance-global, guards with a
per-process boolean in addition to the SQL-level `IF NOT EXISTS`, and
copies the full statement list (column + unique + FK constraints)
verbatim from the 2.21 command. In dry-run it probes
`information_schema.columns` and returns instead of running the crashing
query.
**2. Fast instance command in 2.23**
New
`2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts`,
registered at the end of the 2.23 fast segment (highest timestamp),
running the same idempotent DDL. This covers the normal 2.22 -> 2.23
path and, critically, instances with zero provisioned workspaces where
the workspace command body never executes. The shared DDL lives in
`2-23/utils/ensure-application-registration-logo-file-id-column.util.ts`
so both paths stay byte-for-byte identical. Class name follows the
`Early2_4` / `Early2_5` precedent to avoid colliding with the 2.21
command.
The fix lives entirely in 2.23: instances stuck at the failing workspace
command retry it every run, and 2.22 -> 2.24 jumps still replay the 2.23
segment.
## Ops note
Instances failing right now can be unblocked immediately by running the
same `ALTER TABLE` block by hand against their core database
(byte-for-byte what the command does). Worth including in the 2.23 patch
release note.
## Verification
- New fast instance command re-slotted last in the 2.23 fast segment
(timestamp `1784823473532` > current max `1784659343818`).
- Manual repro path: boot `twentycrm/twenty:v2.21`, seed, stop, run
`upgrade` from this branch, assert the column exists and
`upgrade:status` reports 0 failed. The default v1.22 baseline does not
reproduce it (replays from cursor 0).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YAuDR585cx7FyAKoiT32j3)_
---------
Co-authored-by: Paul Rastoin
---
...ogo-file-id-to-application-registration.ts | 28 +++++++++++
...de-people-data-labs-application.command.ts | 47 ++++++++++++++++++-
...n-registration-logo-file-id-column.util.ts | 36 ++++++++++++++
.../instance-commands.constant.ts | 2 +
4 files changed, 111 insertions(+), 2 deletions(-)
create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts
create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util.ts
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts
new file mode 100644
index 0000000000..5f53350e09
--- /dev/null
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts
@@ -0,0 +1,28 @@
+import { type QueryRunner } from 'typeorm';
+
+import {
+ dropApplicationRegistrationLogoFileIdColumn,
+ ensureApplicationRegistrationLogoFileIdColumn,
+} from 'src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util';
+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';
+
+// Re-slots the 2.21 add-logo-file-id DDL at the end of the 2.23 fast segment:
+// the 2.21 command landed after the 2.22 bump and is skipped forever on
+// instances whose cursor is already past that slot.
+@RegisteredInstanceCommand('2.23.0', 1784823473532)
+export class AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand
+ implements FastInstanceCommand
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ await ensureApplicationRegistrationLogoFileIdColumn((sql) =>
+ queryRunner.query(sql),
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await dropApplicationRegistrationLogoFileIdColumn((sql) =>
+ queryRunner.query(sql),
+ );
+ }
+}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts
index ce6411397f..c92759f4ec 100644
--- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts
@@ -1,13 +1,14 @@
-import { InjectRepository } from '@nestjs/typeorm';
+import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import * as semver from 'semver';
import { isDefined } from 'twenty-shared/utils';
-import { Repository } from 'typeorm';
+import { DataSource, Repository } from 'typeorm';
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
+import { ensureApplicationRegistrationLogoFileIdColumn } from 'src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util';
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
@@ -28,11 +29,16 @@ const PEOPLE_DATA_LABS_TARGET_VERSION = '1.0.9';
'Upgrade the people-data-labs application to 1.0.9 right after the system relation field universal identifier backfill, so its views reference the re-derived name-free identifiers instead of the stale pre-2.23 ones and its front components run on React 19. Workspaces already at or above 1.0.9 are left untouched.',
})
export class UpgradePeopleDataLabsApplicationCommand extends ProvisionedWorkspaceCommandRunner {
+ // Instance-global DDL: run once per process, not per workspace.
+ private hasEnsuredLogoFileIdColumn = false;
+
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly applicationUpgradeService: ApplicationUpgradeService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository,
+ @InjectDataSource()
+ private readonly coreDataSource: DataSource,
) {
super(workspaceIteratorService);
}
@@ -43,6 +49,28 @@ export class UpgradePeopleDataLabsApplicationCommand extends ProvisionedWorkspac
}: RunOnWorkspaceArgs): Promise {
const isDryRun = options.dryRun ?? false;
+ // The 2.21 logoFileId command was skipped on instances that ran a 2.21
+ // binary, so the column is absent and the findOne below crashes on it.
+ // Repair it here since this is the command that breaks.
+ if (!this.hasEnsuredLogoFileIdColumn) {
+ if (isDryRun) {
+ const columnExists = await this.logoFileIdColumnExists();
+
+ if (!columnExists) {
+ this.logger.log(
+ 'Would repair the missing core."applicationRegistration"."logoFileId" column',
+ );
+
+ return;
+ }
+ } else {
+ await ensureApplicationRegistrationLogoFileIdColumn((sql) =>
+ this.coreDataSource.query(sql),
+ );
+ this.hasEnsuredLogoFileIdColumn = true;
+ }
+ }
+
const application = await this.applicationRepository.findOne({
where: {
workspaceId,
@@ -106,4 +134,19 @@ export class UpgradePeopleDataLabsApplicationCommand extends ProvisionedWorkspac
);
}
}
+
+ private async logoFileIdColumnExists(): Promise {
+ // pg_attribute scoped to the single table, not the instance-wide
+ // information_schema.columns view, which is slow on many-tenant instances.
+ const rows = await this.coreDataSource.query(
+ `SELECT EXISTS (
+ SELECT 1 FROM pg_attribute
+ WHERE attrelid = to_regclass('core."applicationRegistration"')
+ AND attname = 'logoFileId'
+ AND NOT attisdropped
+ ) AS "exists"`,
+ );
+
+ return rows[0]?.exists === true;
+ }
}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util.ts
new file mode 100644
index 0000000000..51aa59b1b3
--- /dev/null
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util.ts
@@ -0,0 +1,36 @@
+type QueryExecutor = (sql: string) => Promise;
+
+// Verbatim 2.21 add-logo-file-id DDL, shared so both 2.23 repair paths match.
+export const ensureApplicationRegistrationLogoFileIdColumn = async (
+ query: QueryExecutor,
+): Promise => {
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" ADD COLUMN IF NOT EXISTS "logoFileId" uuid',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "UQ_796819fb23559c233e6ebd49f34"',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "UQ_796819fb23559c233e6ebd49f34" UNIQUE ("logoFileId")',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_796819fb23559c233e6ebd49f34"',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" ADD CONSTRAINT "FK_796819fb23559c233e6ebd49f34" FOREIGN KEY ("logoFileId") REFERENCES "core"."file"("id") ON DELETE SET NULL ON UPDATE NO ACTION',
+ );
+};
+
+export const dropApplicationRegistrationLogoFileIdColumn = async (
+ query: QueryExecutor,
+): Promise => {
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "FK_796819fb23559c233e6ebd49f34"',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" DROP CONSTRAINT IF EXISTS "UQ_796819fb23559c233e6ebd49f34"',
+ );
+ await query(
+ 'ALTER TABLE "core"."applicationRegistration" DROP COLUMN IF EXISTS "logoFileId"',
+ );
+};
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 5d18bc6e74..8a6d9d692b 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
@@ -117,6 +117,7 @@ import { AddCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-22/2
import { AddKanbanAndCalendarWidgetViewTypesFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784620626405-add-kanban-and-calendar-widget-view-types';
import { WidenViewCalendarIntegrityConstraintFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784620626406-widen-view-calendar-integrity-constraint';
import { AddApplicationIdToKeyValuePairFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784659343818-add-application-id-to-key-value-pair';
+import { AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand } from './2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration';
import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status';
import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations';
import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index';
@@ -242,6 +243,7 @@ export const INSTANCE_COMMANDS = [
AddAutoUpgradeToApplicationFastInstanceCommand,
AddApplicationIdToKeyValuePairFastInstanceCommand,
AddSdkClientCoreChecksumToApplicationFastInstanceCommand,
+ AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand,
AddStatusesToBillingSubscriptionIndexSlowInstanceCommand,
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
];