fix(server): bypass workspace cache in onboardingStatus resolver (#20322)

## Summary

In multi-instance deployments, `coreEntityCacheService` memoizes the
workspace entity per server for ~10s, bypassing Redis hash invalidation.
After `activateWorkspace`, if the next `currentUser` query is routed to
a stale replica, the server returns `onboardingStatus:
WORKSPACE_ACTIVATION` and `workspaceMember: null`, the client redirects
to `/create/profile`, and submitting the form throws "User is not logged
in". Reproduces on prod/staging only (local dev = single instance).

Fix: in `OnboardingService.getOnboardingStatus({ user, workspaceId })`,
read the workspace directly from `WorkspaceEntity` repository (bypassing
the per-instance core entity cache) so `onboardingStatus` reflects the
freshest `activationStatus` right after `activateWorkspace`, even when
the request hits a replica with a stale cached workspace.

## Test plan

- Prod/staging: sign up + create workspace, verify `/create/profile`
works and form submits.
- Local: regression on the full onboarding flow.
This commit is contained in:
Charles Bochet
2026-05-06 17:41:46 +02:00
committed by GitHub
parent b94b198a3b
commit 83c40bb8cc
3 changed files with 35 additions and 4 deletions
@@ -1,13 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [BillingModule, UserVarsModule, FeatureFlagModule],
imports: [
BillingModule,
UserVarsModule,
FeatureFlagModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
],
exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver],
})
@@ -1,9 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { type QueryRunner } from 'typeorm';
import { type QueryRunner, Repository } from 'typeorm';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
@@ -32,6 +33,8 @@ export class OnboardingService {
private readonly billingService: BillingService,
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
private readonly twentyConfigService: TwentyConfigService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
private isWorkspaceActivationPending(workspace: WorkspaceEntity) {
@@ -40,7 +43,25 @@ export class OnboardingService {
);
}
async getOnboardingStatus(user: UserEntity, workspace: WorkspaceEntity) {
async getOnboardingStatus({
user,
workspaceId,
}: {
user: UserEntity;
workspaceId: string;
}): Promise<OnboardingStatus | null> {
// We always read the workspace directly from the database here (bypassing
// the per-instance core entity cache) so that onboardingStatus reflects the
// freshest activationStatus right after activateWorkspace, even when a
// sibling server instance still has a stale cached workspace.
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
});
if (!isDefined(workspace)) {
return null;
}
if (
await this.billingService.isSubscriptionIncompleteOnboardingStatus(
workspace.id,
@@ -557,7 +557,10 @@ export class UserResolver {
): Promise<OnboardingStatus | null> {
if (!workspace) return null;
return this.onboardingService.getOnboardingStatus(user, workspace);
return this.onboardingService.getOnboardingStatus({
user,
workspaceId: workspace.id,
});
}
@ResolveField(() => WorkspaceEntity, {