## Summary After a user completes a multi-workspace social-SSO sign-in, [auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011) issues a **workspace-agnostic** access + refresh token pair and lands them on `app.twenty.com/welcome?tokenPair=…`. [SignInUpGlobalScopeFormEffect.tsx](packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx) reads the URL param, writes the cookie, pushes them to `SignInUpStep.WorkspaceSelection`. The problem: if the user revisits `app.twenty.com/welcome` later (e.g. ChatGPT pings `/authorize` and the global page-change effect redirects them to `/welcome` with `returnToPath=/authorize?…`), the existing branch is a no-op — the URL param is gone. The user sees the regular email/SSO form and has to re-authenticate, even though the workspace-agnostic cookie is still valid. This PR adds a second branch in the same `useEffect` that handles the "valid cookie, no URL param" case: ```ts if (signInUpStep !== SignInUpStep.Init) return; if (!hasAccessTokenPair) return; loadCurrentUser(); setSignInUpStep(SignInUpStep.WorkspaceSelection); ``` Single `useEffect`, no `useRef`, no async then/catch. The synchronous `setSignInUpStep(WorkspaceSelection)` is the gate — once the step transitions, subsequent effect runs early-return. Mirrors the existing URL-param branch's pattern exactly. If the cookie is stale, `loadCurrentUser` triggers Apollo's renewal middleware. Renewal of a workspace-agnostic refresh token is supported end-to-end (verified in audit, see below) — if it succeeds the user sees their workspaces; if both tokens are expired, `onUnauthenticatedError` clears the cookie and the next render lands them on the regular sign-in form. Same fallback as if the cookie had never been there. ## Behavior matrix | State on /welcome mount | Before | After | |---|---|---| | No tokenPair anywhere | Show sign-in form | Show sign-in form | | tokenPair in URL (just bounced from SSO) | Set tokens → WorkspaceSelection | (unchanged) Set tokens → WorkspaceSelection | | tokenPair in cookie, access valid | Show sign-in form ❌ | **→ WorkspaceSelection ✓** | | tokenPair in cookie, access expired, refresh valid | Show sign-in form (Apollo eventually 401s on a query) | Renewal succeeds silently → WorkspaceSelection ✓ | | tokenPair in cookie, both expired | Show sign-in form | `onUnauthenticatedError` clears cookie → fall back to sign-in form | ## Workspace-agnostic renewal: confirmed working end-to-end Audit summary: - **Refresh token carries the type**: [refresh-token.service.ts:104](packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.ts) preserves `targetedTokenType` in the JWT payload and returns it from `verifyRefreshToken`. - **Renewal branches on type** ([renew-token.service.ts:70-87](packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts)): ```ts const accessToken = isDefined(authProvider) && targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC && !isDefined(workspaceId) ? await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken({...}) : await this.accessTokenService.generateAccessToken({...}); ``` Renewed refresh token preserves `targetedTokenType` (line 93). - **Resolver is workspace-agnostic**: `@UseGuards(PublicEndpointGuard, NoPermissionGuard)` on `renewToken` ([auth.resolver.ts:796-804](packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts)) — no `@AuthWorkspace()` requirement, callable from `app.twenty.com`. - **Frontend middleware is type-agnostic**: [apollo.factory.ts:180-209](packages/twenty-front/src/modules/apollo/services/apollo.factory.ts) just passes the refresh token blob. Net: no backend change needed. The full workspace-agnostic lifecycle (issue → cookie → renew → re-issue) already works. ## Test plan - [x] `npx oxlint` + `prettier --check` — clean. - [x] `npx nx typecheck twenty-front` — clean. - [ ] Manual: complete one full SSO flow ending on a workspace subdomain. Visit `https://app.twenty.com/welcome` directly — expect the workspace picker, not the sign-in form. - [ ] Manual: same but with tokenPair cookie cleared — expect the regular sign-in form (no regression). - [ ] Manual: sign-out from a workspace, then visit `app.twenty.com/welcome` — expect the regular form (sign-out clears the cookie via full page reload). - [ ] Manual: stale/expired tokenPair cookie — Apollo renewal kicks in transparently; if renewal fails, regular form (no infinite loop, no crash). - [ ] Manual: pair with #20572 — visit `app.twenty.com/authorize?…` with a stale workspace-agnostic cookie. Expected chain: `/authorize` renders → `PageChangeEffect` redirects to `/welcome?returnToPath=/authorize?…` → this effect lands the user on WorkspaceSelection → picking a workspace bounces to `<workspace>/authorize?…` where consent renders. ## Out of scope - Fixing `lastAuthenticatedWorkspaceDomain` for custom-domain users (separate cookie-scoping issue, tracked separately).
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty deploy
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





