From 705caab2b01c4cadd04c8ae2ef7c6655dfde6f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 19 Jun 2026 14:45:12 +0200 Subject: [PATCH] fix(onboarding): refresh stale workspace in currentWorkspace field resolver (#21839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why After sign-up, users are redirected to `/sync/emails` and the page loads forever; a full refresh fixes it. This blocks production deploy. **Root cause** — a GraphQL field resolver returns an unrefreshed (stale) workspace: - `@AuthWorkspace()` (`request.workspace`) is read from the per-instance core entity cache and can still be `PENDING_CREATION` / `ONGOING_CREATION` right after `activateWorkspace`. - The `currentUser` query resolver and the `onboardingStatus` field already guard against this by calling `refreshWorkspaceIfPendingOrOngoingCreation(...)`. - But the `currentWorkspace` `@ResolveField` returned the raw `@AuthWorkspace()` workspace. Because a field resolver takes precedence over any value the query resolver attaches to its returned object, the client receives that stale workspace. So right after activation the client got an inconsistent payload: - `onboardingStatus: SYNC_EMAIL` (fresh — computed from a direct DB read) - `currentWorkspace.activationStatus: ONGOING/PENDING_CREATION` (stale) On the frontend, metadata loading is gated on `isWorkspaceActiveOrSuspended(currentWorkspace)`, and `MinimalMetadataGater` does **not** exclude `/sync/emails`. So the workspace looked inactive → metadata never loaded (no metadata GraphQL request was even issued) → the gater's loader showed indefinitely. A full refresh worked because the cache had since refreshed to `ACTIVE`. ## Why it surfaces on staging but isn't caught by tests The stale window only opens on a real fresh sign-up followed by immediate activation, against a workspace cache that hasn't refreshed yet (multi-instance / cache TTL). Single-instance local dev and the existing `successful-user-and-workspace-creation` integration test exercise `activateWorkspace` + `getCurrentUser` against one consistent cache, so `currentWorkspace` already looks `ACTIVE` and they pass — which is why this reproduces on staging/production but not locally, and why a manual refresh recovers. ## How Refresh the workspace in the `currentWorkspace` field resolver too, so it is consistent with `onboardingStatus`. For active workspaces this is a no-op (no extra DB read). ```ts async currentWorkspace(@AuthWorkspace({ allowUndefined: true }) workspace) { if (!isDefined(workspace)) return workspace; return this.userService.refreshWorkspaceIfPendingOrOngoingCreation(workspace); } ``` This is preferred over the frontend alternative (excluding `/sync/emails` from `MinimalMetadataGater`), which would only hide the symptom while every other consumer still received a wrong `activationStatus`. ## Verification - `nx typecheck twenty-server` and `nx lint:diff-with-main twenty-server` (oxlint + oxfmt) are green. https://claude.ai/code/session_018c1X6CwDgttMXA5tB797yS --------- Co-authored-by: Claude --- .../src/engine/core-modules/user/user.resolver.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index 428bebecf1..0db5ee51c2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -577,7 +577,13 @@ export class UserResolver { @AuthWorkspace({ allowUndefined: true }) workspace: WorkspaceEntity | undefined, ) { - return workspace; + if (!isDefined(workspace)) { + return null; + } + + return this.userService.refreshWorkspaceIfPendingOrOngoingCreation( + workspace, + ); } @ResolveField(() => [UserWorkspaceEntity], {