## 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>
## Summary
Follow-up to #21640. In production, invite suggestions took ~1 minute to
appear because `FetchOnboardingInviteSuggestionsJob` ran on the shared
`calendarQueue` behind heavy calendar-sync jobs.
- **Drop the background job entirely.** `getInviteSuggestions` now
resolves the connected account from the authenticated
`@AuthUserWorkspaceId()` and computes suggestions on demand:
cache-first, with a bounded calendar fetch + cache write on a miss.
Removes the Google/Microsoft enqueues, the
`shouldComputeInviteSuggestions` threading through the auth controllers,
and the now-unused `shouldComputeInviteSuggestionsOnConnect` /
`isOnboardingConnectAccountPending` helpers.
- **Prefetch one step earlier.** New `usePrefetchInviteSuggestions` hook
fires the query from `CreateProfile` so the server cache is warm by the
time the invite step renders. `InviteTeam` switches from `network-only`
→ `cache-first`. If the profile step is skipped, the invite step still
computes on-demand (~1–3s, no queue) — no more minute-long waits.
No GraphQL schema change.
## Test plan
- [ ] Connect Google calendar in onboarding → invite step renders
prefilled teammates with no perceivable wait
- [ ] Connect Microsoft calendar in onboarding → same
- [ ] Onboard with workspace name already set so profile step is skipped
→ invite step still prefills (just with a brief on-demand fetch instead
of 1 min)
- [ ] Connect a non-work-email account → invite step renders empty form
(no suggestions)
- [ ] `npx nx typecheck twenty-server` ✅
- [ ] `npx nx lint:diff-with-main twenty-server` ✅
- [ ] `npx nx lint:diff-with-main twenty-front` ✅ (changed files clean)
- [ ] `google-apis.service.spec.ts` + `microsoft-apis.service.spec.ts`
pass
https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY
---
_Generated by [Claude
Code](https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21696?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Co-authored-by: Claude <noreply@anthropic.com>
## What & why
Implements core-team-issues#1414: move the calendar/email connection
earlier in onboarding and use the freshly connected calendar to prefill
the **Invite your team** step with likely teammates, so users don't
start from an empty form.
## Approach
Everything is behind the feature flag
`IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` (off by default).
**Reorder** — onboarding becomes `Workspace activation → Connect account
→ Create profile → Invite team`. Connecting before profile gives the
calendar sync a head start; connecting *before* the workspace exists
isn't possible (a connected account requires an activated workspace +
workspace member + OAuth transient token). Gated in both
`OnboardingService.getOnboardingStatus` (backend) and
`useSetNextOnboardingStatus` (frontend) so the two agree.
**Fast teammate lookup** — on Google/Microsoft connect *during
onboarding*, a background job (`FetchOnboardingInviteSuggestionsJob`)
runs a single bounded calendar fetch (recent events, attendees inline),
keeps same-work-email-domain colleagues (excludes self + aliases;
personal mailboxes yield nothing), ranks by meeting frequency, and
caches the top 5. The invite step reads the cache via a new
`getInviteSuggestions` query and prefills the form — polling briefly
while the cache warms, and never overwriting input the user has already
typed.
Providers: **Google** (Calendar `events.list`) and **Microsoft** (Graph
`calendarView`), routed by a `CalendarAttendeesService` dispatcher
(mirrors the existing `CalendarGetCalendarEventsService`). Any fetch
failure (missing scope, API error) degrades to today's empty form via
the orchestrator's best-effort catch.
## How to enable
Turn on `IS_ONBOARDING_INVITE_SUGGESTIONS_ENABLED` for a workspace
(admin panel).
## Notes
- New-workspace creators only (invitees never see the connect/invite
steps). Skipping the connect step, or signing up with a personal email,
falls back to the current empty form.
- The "We found teammates from your calendar" subtitle only shows once
suggestions are actually prefilled.
- i18n: the new `<Trans>` strings are extracted on merge to `main` by
the existing Crowdin workflow.
## Testing
- Frontend unit tests for the reorder state machine (both flag states).
- `npx nx typecheck` and `npx nx lint:diff-with-main` green for
`twenty-front` and `twenty-server`.
- Server boots with the new DI wiring (no circular dependency);
`getInviteSuggestions` / `InviteSuggestion` present in the live metadata
schema.
- Not exercised in CI: live Google/Microsoft OAuth end-to-end (requires
real accounts + calendar data).
https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY
---
_Generated by [Claude
Code](https://claude.ai/code/session_019MyY3bfAEij4AwSXMCLtWY)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21640?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## 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.
## Summary
Aligns **workspace member** editing and **onboarding** with how the
product is actually used: profile and other “settings” fields go through
**`updateWorkspaceMemberSettings`**, while **`/graphql`** record APIs
follow **object-level** permissions for the `workspaceMember` object.
## Product behaviour
### Completing “Create profile” onboarding
Users who must create a profile (empty name at sign-up) get
`ONBOARDING_CREATE_PROFILE_PENDING` set. The onboarding UI saves the
name with **`updateWorkspaceMemberSettings`**, not with a workspace
record **`updateOne`**.
**Before:** The server only cleared the pending flag on
**`workspaceMember.updateOne`**, so the flag could stay set and
onboarding appeared stuck.
**After:** Clearing the profile step runs when
**`updateWorkspaceMemberSettings`** persists an update that includes a
**name** (same rules as before: non-empty name parts). Onboarding can
advance normally after **Continue** on Create profile.
### Two ways to change workspace member data
| Path | Typical use | Who can change what |
|------|----------------|---------------------|
| **`updateWorkspaceMemberSettings`** (metadata API) | Standard member
fields the app treats as “my profile / preferences” (name,
avatar-related settings, locale, time zone, etc.) | **Always** your
**own** workspace member. Changing **another** member still requires
**Workspace members** in role settings (`WORKSPACE_MEMBERS`). Custom
fields are **not** allowed on this endpoint (unchanged). |
| **`/graphql`** record mutations on **`workspaceMember`** | Custom
fields, integrations, anything that goes through the generic record API
| **`WorkspaceMember`** is special-cased in permissions: **read** stays
**on** for everyone, but **update / create / delete** require
**`WORKSPACE_MEMBERS`**, including updating **your own** row via
`/graphql`. So a **Member** without that permission cannot fix their
name through **`updateWorkspaceMember`**; they use **Settings** /
**`updateWorkspaceMemberSettings`** instead. |
This matches **`WorkspaceRolesPermissionsCacheService`**: for the
workspace member object, `canReadObjectRecords` is always true;
`canUpdateObjectRecords` (and delete-related flags) follow
**`WORKSPACE_MEMBERS`**.
### Hooks and delete side-effects
- Removed **`workspaceMember.updateOne`** pre-query hook and
**`WorkspaceMemberPreQueryHookService`**: they duplicated the same rules
the permission cache already enforces for `/graphql`.
- **`WorkspaceMember.deleteOne`** pre-hook still tells users to remove
members via the dedicated flow; the post-hook only runs the
**`deleteUserWorkspace`** side-effect when a member row is actually
removed—**no** extra settings-permission check there, since only callers
that already passed **object** delete permission can remove the row.
## Tests
- **`workspace-members.integration-spec.ts`**: clarifies and extends
coverage so **`/graphql`** **`updateOne`** is denied for **own** record
on a **standard** name field and on a **custom** field when the role
lacks **`WORKSPACE_MEMBERS`**.
## Implementation notes
- **`OnboardingService.completeOnboardingProfileStepIfNameProvided`**
centralises the “clear profile pending if name present” logic;
**`UserResolver.updateWorkspaceMemberSettings`** calls it after save,
using the typed update payload’s **`name`** (no cast).
- **`UserWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace`**:
drops a redundant **`coreEntityCacheService.invalidate`**;
**`updateWorkspaceMemberSettings`** still invalidates the user-workspace
cache after the mutation.
# Introduction
Previously the auth jwt stragegy would lod the whole user entity in the
auth user context
On an exception it would completely get logged on the pods
## Security layer
- 0/ Updating the type system ( devxp only though )
- 1/ The jwt auth stragegy only load a specific sub set of the user
entity
- 2/ Sanitizing at the exception log level directly in case of a user
context
- 3/ Sanitizing at the console driver
The last two sanitization could sound a bit redundant though they're
still good fallback to keep in case new path occurs in the cb
## Fix resolver schema leaking between `/metadata` and `/graphql`
endpoints
### Summary
- Patch `@nestjs/graphql` to support a `resolverSchemaScope` option that
filters resolvers at both schema generation and runtime, preventing
cross-endpoint leaking
- Introduce `@CoreResolver()` and `@MetadataResolver()` decorators to
explicitly scope each resolver to its endpoint
- Move most resolvers (auth, billing, workspace, user, etc.) to the
metadata schema where the frontend expects them; only workflow and
timeline calendar/messaging resolvers remain on `/graphql`
- Fix frontend `SSEQuerySubscribeEffect` to use the default (metadata)
Apollo client instead of the core client
### Problem
NestJS GraphQL's module-based resolver discovery traverses transitive
imports, causing resolvers from `/metadata` modules to leak into the
`/graphql` schema and vice versa. This made the schemas unpredictable
and tightly coupled to module import order.
### Approach
- Added `resolverSchemaScope` to `GqlModuleOptions` via a patch on
`@nestjs/graphql`, filtering in both `filterResolvers()` (runtime
binding) and `getAllCtors()` (schema generation)
- Each resolver is explicitly decorated with `@CoreResolver()` or
`@MetadataResolver()`
- Organized decorator, constant, and type files under `graphql-config/`
following project conventions
Core GQL Schema: (see: no more fields!)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/668f3f0f-485e-43f0-92be-4345aeccacb6"
/>
Metadata GQL Schema (see no more getTimelineCalendarEventsFromCompany)
<img width="827" height="894" alt="image"
src="https://github.com/user-attachments/assets/443913db-e5fe-4161-b0e7-4a971cc80a71"
/>
# Introduction
related to https://github.com/twentyhq/core-team-issues/issues/1833
In this PR we're starting the sync-metadata and standardIds deprecation
by introducing `twenty-standard` application that will regroup every
standard object such as company and opportunities. But also the
`custom-workspace-application` which is an app created at the same time
as a workspace and that will regroup everything configure within the
workspace ( custom objects fields etc )
## What's done
On both new workspace and seeded workspace creation:
- Creating a custom workspace app
- Creating a twenty standard app
- Refactored the seed core schema and workspace creation to be run
within a transaction in order to handle circular dependency foreignkey
requirements ( which is deferred for app toward workspace )
- Updated workspace entity to have a custom workspace relation (
nullable for the moment until we implem an upgrade command to handle
retro comp )
- Integration testing on user, workspace creation deletion and expected
default apps creation
- ~~Soft deleted user on `deleteUser`~~ Done by marie and rebased on it
## What's next
- Update seeder to propagate the `twenty-standard` workspace
`applicationId` to every standard synchronized entities ( cheap and fast
iteration through the about to be deprecated sync-metadata as an easy
way to synchronize standards metadata entities ).
- Update seeder to propagate the `custom-workspace-application`
workspace `applicationId` to anything custom ( `pets` and `rockets` )
- Prepend `custom-workspace-application` `applicationId` to every
metadata API operations ( create a specific cache etc )
- Upgrade command on all existing workspace to create a custom app and
associate its applicationId to any existing custom entities
- Make `universalIdentifier` and `applicationId` required for any
syncable entity
## Overview
This PR strengthens our permission system by introducing more granular
role-based access control across the platform.
## Changes
### New Permissions Added
- **Applications** - Control who can install and manage applications
- **Layouts** - Control who can customize page layouts and UI structure
- **AI** - Control access to AI features and agents
- **Upload File** - Separate permission for file uploads
- **Download File** - Separate permission for file downloads (frontend
visibility)
### Security Enhancements
- Implemented whitelist-based validation for workspace field updates
- Added explicit permission guards to core entity resolvers
- Enhanced ESLint rule to enforce permission checks on all mutations
- Created `CustomPermissionGuard` and `NoPermissionGuard` for better
code documentation
### Affected Components
- Core entity resolvers: webhooks, files, domains, applications,
layouts, postgres credentials
- Workspace update mutations now use whitelist validation
- Settings UI updated with new permission controls
### Developer Experience
- ESLint now catches missing permission guards during development
- Explicit guard markers make permission requirements clear in code
review
- Comprehensive test coverage for new permission logic
## Testing
- ✅ All TypeScript type checks pass
- ✅ ESLint validation passes
- ✅ New permission guards properly enforced
- ✅ Frontend UI displays new permissions correctly
## Migration Notes
Existing workspaces will need to assign the new permissions to roles as
needed. By default, all new permissions are set to `false` for non-admin
roles.
## 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
We were using a global ValidationPipe in main.ts. This is an issue as
@Controllers should return HttpExecption and @Resolvers should return
GraphqlErrors
Removing the global pipe and creating a ResolverValidationPipe able to
generate GraphqlError. We also need to handle the exception in a filter
to avoid nest to think it's unhandled and make it flow to logs
Next step:
- it would be nice to have both @UsePipes(ResolverValidationPipe) +
@UseFilters(GraphqlValidationExceptionFilter) come together. This should
be possible if we create a @GraphQLResolver annotation
# 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 !
Following a conversation with @etiennejouan and @martmull, we are adding
a permission gate on billing resolver's checkoutSession, which should
only be accessible to entitled users or at workspace creation (when
there are no roles yet), when the subscription is incomplete
In this PR:
- migrate WorkspaceActivationStatus to twenty-shared (and update case to
make FE and BE consistent)
- introduce isWorkspaceActiveOrSuspended in twenty-shared
- refactor the code to use it (when we fetch data on the FE, we want to
keep SUSPENDED workspace working + when we sync workspaces we want it
too)
### 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>
## Summary
- [x] Remove defaultWorkspace in user
- [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId
- [x] Improve activate workspace flow
- [x] Improve security on social login
- [x] Add `ImpersonateGuard`
- [x] Allow to use impersonation with couple `User/Workspace`
- [x] Prevent unexpected reload on activate workspace
- [x] Scope login token with workspaceId
Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042
Beforehand, the name of the branch is not representative of the work
that has been done in this PR
**TLDR:**
Solves https://github.com/twentyhq/private-issues/issues/192
Add 3 tables BillingCustomer, BillingProduct and BillingPrice and
BillingMeter to core, inspired by the Stripe implementation. Separates
migration, between common and billing on order to not populate the db of
the self-hosting instances with unused tables.
**In order to test:**
Run the command:
npx nx typeorm -- migration:run -d
src/database/typeorm/core/core.datasource.ts
**Considerations:**
I only put the information we should use right now in the Billing
module, for instance columns like meter or agreggation formula where
omitted in the creation of the tables.
These columns and other ones who fall on the same spectrum will be added
as we need them.
If you want to add more information to the table, I'll leave some
utility links down bellow:
- BillingPrices: https://docs.stripe.com/api/prices/object
- BillingCustomer: https://docs.stripe.com/api/customers/object
- BillingProduct: https://docs.stripe.com/api/products/object
**Next Steps**
Use the Stripe Webhook in order to update the tables accordingly
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
In this PR:
1. Refactor guards to avoid duplicated queries: WorkspaceAuthGuard and
UserAuthGuard only check for existence of workspace and user in the
request without querying the database
Closes#5748
- Create feature flag
- Add scope `https://www.googleapis.com/auth/profile.emails.read` when
connecting an account
- Get email aliases with google people API, store them in
connectedAccount and refresh them before each message-import
- Update the contact creation logic accordingly
- Refactor
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
- move front `onboardingStatus` computing to server side
- add logic to `useSetNextOnboardingStatus`
- update some missing redirections in
`usePageChangeEffectNavigateLocation`
- separate subscriptionStatus from onboardingStatus
This PR is replacing and removing all the raw queries and repositories
with the new `TwentyORM` and injection system using
`@InjectWorkspaceRepository`.
Some logic that was contained inside repositories has been moved to the
services.
In this PR we're only replacing repositories for calendar feature.
---------
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>