## What & why
A single, consistent **workspace-creation step** for both
multi-workspace and single-workspace self-host — collecting **name +
logo** (and the **subdomain** in multi-workspace) — which **removes the
duplicate name/logo prompt** that previously reappeared on the workspace
subdomain (reported after #21641).
## Changes
**One creation form for both modes**
- With 0 workspaces, both multi-workspace and single-workspace route to
the shared `SignInUpWorkspaceCreationForm`; `SignInUp` renders it for
the `WorkspaceCreation` step regardless of domain/scope.
- The subdomain field shows only in multi-workspace; single-workspace
keeps its fixed address.
**Logo on the creation step**
- New scoped `uploadNewWorkspaceLogo(workspaceId, file)` mutation: the
creator sets a logo on their just-created `PENDING_CREATION` workspace
via the workspace-agnostic token (membership enforced — only the creator
is a member at that point), reusing `uploadWorkspacePicture`. Upload
size is capped via `settings.storage.maxFileSize` (also applied to the
existing logo / profile-picture uploads).
- The picked file is held locally (object-URL preview, revoked on
unmount) and uploaded right after creation (non-fatal on failure).
**Onboarding step → pure activation loader**
- The old "Create your workspace" form (name + logo) is removed. The
onboarding step now activates the pending workspace on mount and shows
the loader, with a **Retry** action on failure.
## Testing
- typecheck (front + server) ✅; oxlint + oxfmt clean on changed files ✅
- Unit tests: `auth.resolver.spec`, `useWorkspaceSubdomainField`,
`SignInUpWorkspaceCreationForm` (multi + single-workspace), `useAuth` ✅
- Metadata GraphQL + `twenty-client-sdk` schema regenerated.
Follow-up to #21641.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Xw37hR5seiCyWnppG9z4op
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.
- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
## Summary
- **Delete 10 unused files**: 7 hooks (`useWorkflowRunUnsafe`,
`useGetViewById`, `useCreateViewFieldGroup`, `useDeleteViewFieldGroup`,
`useUpdateViewFieldGroup`, `useCreateManyViewFieldGroups`,
`useMoveViewColumns` + test), 1 component (`SettingsSummaryCard`), 1
utility (`createEventContext`)
- **Rename `objectMetadataItemsState` → `objectMetadataItemsSelector`**
across ~85 files to accurately reflect it is a derived selector (via
`createAtomSelector`), not a base Jotai atom
## Details
### Dead code removed
| Type | Name | Reason |
|------|------|--------|
| Hook | `useWorkflowRunUnsafe` | Never imported — duplicate of
`useWorkflowRun` without schema validation |
| Hook | `useGetViewById` | Never imported — `useViewById` is used
instead |
| Hook | `useCreateViewFieldGroup` | Never imported — CRUD done via
`usePerformViewFieldGroupAPIPersist` |
| Hook | `useDeleteViewFieldGroup` | Same as above |
| Hook | `useUpdateViewFieldGroup` | Same as above |
| Hook | `useCreateManyViewFieldGroups` | Same as above |
| Hook | `useMoveViewColumns` | Only imported by its own test — no
production usage |
| Component | `SettingsSummaryCard` | Never imported anywhere |
| Utility | `createEventContext` | Never imported anywhere |
### Rename
`objectMetadataItemsState` is created via `createAtomSelector` (it
derives from `objectMetadataItemsWithFieldsSelector`), so naming it
`*State` is misleading. Renamed to `objectMetadataItemsSelector` for
consistency with sibling selectors like
`objectMetadataItemsByNamePluralMapSelector`.
## Summary
- Implement a return-to-path mechanism that preserves the user's
intended destination across authentication flows (login, magic link,
cross-domain redirects)
- Uses layered persistence: Jotai atom (in-memory), sessionStorage with
TTL (tab-switch resilience), URL query parameter (cross-domain
propagation)
- Includes path validation to prevent open redirects, automatic cleanup
after successful login, and comprehensive test coverage
- Replaces the unused `previousUrlState` with a robust
`returnToPathState` system
## Test plan
- [ ] Visit a deep link (e.g. `/objects/tasks`) while logged out —
should redirect to login, then back to `/objects/tasks` after logging in
- [ ] Visit an OAuth authorize link while logged out — should redirect
to login, then to the authorize page
- [ ] Test magic link flow: click sign-in link that opens new tab —
should still redirect to original destination
- [ ] Test cross-domain: visit `app.twenty.com/objects/tasks` — should
preserve path through workspace domain redirect
- [ ] Verify auth/onboarding paths are excluded from being saved as
return paths
- [ ] Verify return-to-path is cleared after successful navigation
- [ ] All 215 existing `usePageChangeEffectNavigateLocation` tests pass
Made with [Cursor](https://cursor.com)
## Reduce type leakage between GraphQL schemas
### Why
Twenty runs two separate GraphQL schemas: **core** and **metadata**.
NestJS's `@nestjs/graphql` uses a global `TypeMetadataStorage` that
accumulates all decorated types across all modules. When each schema is
built, every registered type leaks into both schemas regardless of which
module it belongs to.
This means the core schema's generated TypeScript
(`generated/graphql.ts`) contained ~2,700 lines of types that only
belong to the metadata schema (and vice versa). This creates confusion
about type ownership, inflates generated code, and makes it harder to
reason about which API surface each schema actually exposes.
### How
**1. Patch `@nestjs/graphql` to support schema-scoped type resolution**
- **(Already done)** Added a `resolverSchemaScope` option to
`GqlModuleOptions`, allowing each schema to declare a scope (e.g.
`'metadata'`)
- `ResolversExplorerService` now filters resolvers by a
`RESOLVER_SCHEMA_SCOPE` metadata key, so each schema only sees its own
resolvers
- `GraphQLSchemaFactory` now performs a **reachability walk**
(`computeReachableTypes`) starting from scoped resolver return types and
arguments, only including types that are transitively referenced —
handling unions, interfaces, and prototype chains
- Type definition storage and orphaned reference registry are cleared
between schema builds to prevent cross-contamination
**2. Register `ClientConfig` as orphaned type in metadata schema**
Since `ClientConfig` is needed in the metadata schema but not directly
returned by a resolver, it's explicitly declared via
`buildSchemaOptions.orphanedTypes`.
**3. Regenerate frontend types and fix imports**
- `generated/graphql.ts` shrank by ~2,700 lines (types moved to where
they belong)
- `generated-metadata/graphql.ts` gained types like `ClientConfig` that
were previously missing
- ~500 frontend files updated to import from the correct generated file
## The Issue
When CALENDAR_BOOKING_PAGE_ID env variable is not configured, the
onboarding flow still sets the booking step as pending in the database.
This causes users to get stuck on a broken booking page after
logout/login, as the Cal.com iframe tries to load with an empty calendar
link.
## The Fix
Made the booking step handling idempotent across the stack:
Backend:
- setOnboardingBookOnboardingPending now checks if calendar is actually
configured before setting the step as pending
- getOnboardingStatus auto-cleans invalid booking states when detected
(booking pending but no calendar configured)
- Empty strings in env are now treated as undefined in client config
Frontend:
- Added navigation protection to redirect away from booking pages when
calendar isn't configured
- Existing defensive logic in useSetNextOnboardingStatus already skips
booking when no calendar ID
Result
- New users won't get invalid booking states
- Existing bad data self-heals when users interact with the system
- Backend and frontend stay in sync about when booking should be shown
Fixes the issue Felix reported where users saw a broken booking page in
production.
I think we should keep the old CAL_LINK constant for now - while we
could remove the booking onboarding step entirely, it would break the
plan/pricing modal which uses it as a fallback when no calendar is
configured. Open for discussion! -- May be we dont show the `Book a
Call` button if the env is not set -- but we should keep it as it is if
we want two different behaviors :)
closes https://github.com/twentyhq/core-team-issues/issues/1558
## Problem
**CRITICAL:** Two PRs were accidentally reverted when PR #14347 "Prevent
csv export injections" was merged:
1. **PR #14348** "[Page Layout] - Review Refactor" - ✅ **RESTORED**
2. **PR #14352** "Fix wrong path used by backend" - ✅ **RESTORED**
## Root Cause Analysis
During the merge of PR #14347, there was a complex merge conflict with
PR #14352 "Fix wrong path used by backend". The merge commit
`324d7204bb` in the PR #14347 branch brought in changes from PR #14352,
but during the conflict resolution, **BOTH PR #14348 and PR #14352's
changes were accidentally overwritten**.
## What This PR Restores
This PR restores **BOTH** PRs by cherry-picking their commits:
### ✅ PR #14348 Changes Restored:
- `GraphWidgetRenderer.tsx` - was deleted, now restored
- `WidgetRenderer.tsx` - was missing, now restored
- `SettingsPageLayoutTabsInstanceId.ts` - was deleted, now restored
- `useUpdatePageLayoutWidget.ts` - was renamed back, now restored with
correct name
- Multiple test files that were deleted
- Several hook files that were renamed/reverted
- File renames: `usePageLayoutWidgetUpdate.ts` →
`useUpdatePageLayoutWidget.ts`
- Hook refactoring and test file organization
- Page layout component improvements
### ✅ PR #14352 Changes Restored:
- **Types moved to twenty-shared:**
- `packages/twenty-shared/src/types/AppBasePath.ts` ✅ RESTORED
- `packages/twenty-shared/src/types/AppPath.ts` ✅ RESTORED
- `packages/twenty-shared/src/types/SettingsPath.ts` ✅ RESTORED
- **Navigation utilities moved to twenty-shared:**
- `packages/twenty-shared/src/utils/navigation/getAppPath.ts` ✅ RESTORED
- `packages/twenty-shared/src/utils/navigation/getSettingsPath.ts` ✅
RESTORED
- **200+ import statements updated** across the codebase to use
twenty-shared
- **Old type files deleted** from twenty-front/src/modules/types/
## Evidence of Complete Restoration
**Before (reverted state):**
- ❌ Types were in `packages/twenty-front/src/modules/types/`
- ❌ Page layout files missing
- ❌ Hook files incorrectly named
**After (this PR):**
- ✅ Types correctly in `packages/twenty-shared/src/types/`
- ✅ All page layout files restored
- ✅ Hook files correctly named
- ✅ All import statements updated
## Verification
**Total changes:**
- PR #14348: 36 files changed, 863 insertions(+), 442 deletions(-)
- PR #14352: 243 files changed, 492 insertions(+), 461 deletions(-)
- **Combined: 279 files changed, 1355 insertions(+), 903 deletions(-)**
## Impact
This completely restores both PRs that were accidentally lost, ensuring:
1. Page layout refactoring work is back
2. Type organization and path utilities are correctly in twenty-shared
3. Backend email paths work correctly again
4. No functionality is lost
Fixes the reversion caused by the merge conflict in PR #14347.
---------
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
**Small Security Issue:** CSV exports were vulnerable to formula
injection attacks when users entered values starting with =, +, -, or @.
(only happens if a logged-in user injects corrupted data)
Solution:
- Added ZWJ (Zero-Width Joiner) protection that prefixes dangerous
values with invisible Unicode character
- This is the best way to preserve original data while preventing Excel
from executing formulas
- Added import cleanup to restore original values when re-importing
Changes:
- New sanitizeValueForCSVExport() function for security
- Updated all CSV export paths to use both security + formatting
functions
- Added comprehensive tests covering attack vectors and international
characters
- Also added cursor rules for better code consistency
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
After moving a section on the frontend, this broke the path that was
sent by email on the backend.
This kind of error comes back every ~2-3 month under different forms so
we need a more robust solution: I moved routes to the shared folder,
that way we will share one common source of truth between the frontend
and the backend.
Fixes#14343
- Replaced `getAuthTokensFromLoginToken` with
`getAccessTokensFromLoginToken` for clarity.
- Introduced `getWorkspaceAgnosticTokenFromEmailVerificationToken`.
- Extended mutation inputs to include `locale` and
`verifyEmailNextPath`.
- Added email verification check and sending to various handlers.
- Updated GraphQL types and hooks to reflect these changes.
Fix#13412
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Context :
Plan choice [on pricing page on website](https://twenty.com/pricing)
should redirect you the right plan on app /plan-required page (after
sign in), thanks to query parameters and BillingCheckoutSessionState
sync.
With email verification, an other session starts at CTA click in
verification email. Initial BillingCheckoutSessionState is lost and user
can't submit to the plan he choose.
Solution :
Pass a nextPath query parameter in email verification link
To test :
- Modify .env to add IS_BILLING_ENABLED (+ reset db + sync billing) +
IS_EMAIL_VERIFICATION_REQUIRED
- Start test from this page
http://app.localhost:3001/welcome?billingCheckoutSession={%22plan%22:%22ENTERPRISE%22,%22interval%22:%22Year%22,%22requirePaymentMethod%22:true}
- After verification, check you arrive on /plan-required page with
Enterprise plan on a yearly interval (default is Pro/monthly).
closes https://github.com/twentyhq/twenty/issues/12288
Fixes https://github.com/twentyhq/core-team-issues/issues/950
This issue was due to the memoization inside `useIsMatchingLocation`,
which was rerendered only if the pathname changed but not the search
params.
After discussion with @lucasbordeau, we decided to remove the hook
`useIsMatchingLocation` and to create an equivalent util function which
takes the location as an argument.
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
# Introduction
In this PR we've migrated `twenty-shared` from a `vite` app
[libary-mode](https://vite.dev/guide/build#library-mode) to a
[preconstruct](https://preconstruct.tools/) "atomic" application ( in
the future would like to introduce preconstruct to handle of all our
atomic dependencies such as `twenty-emails` `twenty-ui` etc it will be
integrated at the monorepo's root directly, would be to invasive in the
first, starting incremental via `twenty-shared`)
For more information regarding the motivations please refer to nor:
- https://github.com/twentyhq/core-team-issues/issues/587
-
https://github.com/twentyhq/core-team-issues/issues/281#issuecomment-2630949682
close https://github.com/twentyhq/core-team-issues/issues/589
close https://github.com/twentyhq/core-team-issues/issues/590
## How to test
In order to ease the review this PR will ship all the codegen at the
very end, the actual meaning full diff is `+2,411 −114`
In order to migrate existing dependent packages to `twenty-shared` multi
barrel new arch you need to run in local:
```sh
yarn tsx packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts && \
npx nx run-many -t lint --fix -p twenty-front twenty-ui twenty-server twenty-emails twenty-shared twenty-zapier
```
Note that `migrateFromSingleToMultiBarrelImport` is idempotent, it's atm
included in the PR but should not be merged. ( such as codegen will be
added before merging this script will be removed )
## Misc
- related opened issue preconstruct
https://github.com/preconstruct/preconstruct/issues/617
## Closed related PR
- https://github.com/twentyhq/twenty/pull/11028
- https://github.com/twentyhq/twenty/pull/10993
- https://github.com/twentyhq/twenty/pull/10960
## Upcoming enhancement: ( in others dedicated PRs )
- 1/ refactor generate barrel to export atomic module instead of `*`
- 2/ generate barrel own package with several files and tests
- 3/ Migration twenty-ui the same way
- 4/ Use `preconstruct` at monorepo global level
## Conclusion
As always any suggestions are welcomed !
In this PR:
- adding logs to track workspace creation performance
- refactor useIsWorkspaceSuspended to be more generic
- only fetch favorites and views if workspace is Active to avoid error
messages on sign up (workspace is not created yet)
Adjusted URL construction to properly handle trailing slashes in base
paths, ensuring consistent matching logic. Added logic for setting the
hotkey scope when navigating to the domain settings path.
### Context
- Update /plan-required page to let users get free trial without credit
card plan
- Update usePageChangeEffectNavigateLocation to redirect paused and
canceled subscription (suspended workspace) to /settings/billing page
### To do
- [x] Update usePageChangeEffectNavigateLocation test
- [x] Update ChooseYourPlan sb test
closes#9520
---------
Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
Closestwentyhq/twenty#8240
This PR introduces email verification for non-Microsoft/Google Emails:
## Email Verification SignInUp Flow:
https://github.com/user-attachments/assets/740e9714-5413-4fd8-b02e-ace728ea47ef
The email verification link is sent as part of the
`SignInUpStep.EmailVerification`. The email verification token
validation is handled on a separate page (`AppPath.VerifyEmail`). A
verification email resend can be triggered from both pages.
## Email Verification Flow Screenshots (In Order):



## Sent Email Details (Subject & Template):


### Successful Email Verification Redirect:

### Unsuccessful Email Verification (invalid token, invalid email, token
expired, user does not exist, etc.):

### Force Sign In When Email Not Verified:

# TODOs:
## Sign Up Process
- [x] Introduce server-level environment variable
IS_EMAIL_VERIFICATION_REQUIRED (defaults to false)
- [x] Ensure users joining an existing workspace through an invite are
not required to validate their email
- [x] Generate an email verification token
- [x] Store the token in appToken
- [x] Send email containing the verification link
- [x] Create new email template for email verification
- [x] Create a frontend page to handle verification requests
## Sign In Process
- [x] After verifying user credentials, check if user's email is
verified and prompt to to verify
- [x] Show an option to resend the verification email
## Database
- [x] Rename the `emailVerified` colum on `user` to to `isEmailVerified`
for consistency
## During Deployment
- [x] Run a script/sql query to set `isEmailVerified` to `true` for all
users with a Google/Microsoft email and all users that show an
indication of a valid subscription (e.g. linked credit card)
- I have created a draft migration file below that shows one possible
approach to implementing this change:
```typescript
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateEmailVerifiedForActiveUsers1733318043628
implements MigrationInterface
{
name = 'UpdateEmailVerifiedForActiveUsers1733318043628';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE core."user_email_verified_backup" AS
SELECT id, email, "isEmailVerified"
FROM core."user"
WHERE "deletedAt" IS NULL;
`);
await queryRunner.query(`
-- Update isEmailVerified for users who have been part of workspaces with active subscriptions
UPDATE core."user" u
SET "isEmailVerified" = true
WHERE EXISTS (
-- Check if user has been part of a workspace through userWorkspace table
SELECT 1
FROM core."userWorkspace" uw
JOIN core."workspace" w ON uw."workspaceId" = w.id
WHERE uw."userId" = u.id
-- Check for valid subscription indicators
AND (
w."activationStatus" = 'ACTIVE'
-- Add any other subscription-related conditions here
)
)
AND u."deletedAt" IS NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE core."user" u
SET "isEmailVerified" = b."isEmailVerified"
FROM core."user_email_verified_backup" b
WHERE u.id = b.id;
`);
await queryRunner.query(`DROP TABLE core."user_email_verified_backup";`);
}
}
```
---------
Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
- move front `onboardingStatus` computing to server side
- add logic to `useSetNextOnboardingStatus`
- update some missing redirections in
`usePageChangeEffectNavigateLocation`
- separate subscriptionStatus from onboardingStatus
- make invitation and reset password available on every page
- add a sleep after setKeyPair as tokens are sometimes not updated when
redirecting to Index
- refactor sleep