This PR implements SSE update events across the main components of the
application : tables, boards, calendars, show pages.
There is still work to do on other event type and on making sure
everything works fine, but this first implementation should be robust
enough to start with.
Some problems encountered along the way :
- Events are returning raw Postgres output, because they are not
normalized by the GraphQL layer, so we ended up with amountMicros as
string values, which the frontend does not like, so I implemented a
small util in our `formatResult` generic pipeline to turn amountMicros
to a number if it's a string value. We could implement other formatters
for composite fields if we see problems with events.
-`action` property was missing in SSE events, which is required by the
frontend to know what kind of event it is.
# QA
https://github.com/user-attachments/assets/393b45ac-59d2-48b0-855b-2ce9c4b8ae57https://github.com/user-attachments/assets/cb214e7a-1595-4b85-bdb6-87630993d2e2
## Summary
Moves the custom ESLint rules from `tools/eslint-rules` to
`packages/twenty-eslint-rules` for better organization within the
monorepo packages structure.
## Changes
- Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules`
- Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules
- Update all ESLint configs to use the `twenty/` rule prefix instead of
`@nx/workspace-`
- Update `project.json`, `jest.config.mjs` with new paths
- Update `package.json` workspaces and `nx.json` cache inputs
- Update Dockerfile reference
## Technical Details
The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules`
utility which:
- Handles TypeScript transpilation automatically
- Allows loading workspace rules from any directory
- Provides a cleaner approach than the previous `@nx/workspace-`
convention
## Testing
- Verified all 17 custom ESLint rules load correctly from the new
location
- Verified linting works on dependent packages (twenty-front,
twenty-server, etc.)
As part of the extensibility effort, we are introducing a new engine
entity called "Front Component". This represents a dynamic react
component that will be rendered in CommandMenu actions or in PageLayout
widgets
This PR introduce the entity and all the necessary boilerplate to make
it syncable and cachable in the engine
- add `forwardedRequestHeaders` `string[]` column in `core.routeTrigger`
- filter request headers and forward filtered headers to function
payload (avoid spreading unexpectedly token or cookie)
- add `forwardedRequestHeaders` option in twenty-sdk `defineFunction`
util
BREAKING for actual routeTrigger payload but only 16 to migrate in
production
## Context
Now that RLS predicates are applied, creating a record through the FE
(which is empty by default) is failing if your role has predicates and
your input does not respect them (which will always be true since, as
said above, input will be pretty much empty)
## Implementation
- Moved isMatching* filters to twenty-shared
- Implemented isMatchingRlsPredicates utils in the backend (ORM) to
check before insertion/update if the record is matching the current user
role Rls predicates, reusing the isMatching* filters utils moved to
twenty-shared
- Frontend now applies RLS predicates before creating a new record
(similarly to what we do with view filters)
Note:
It seems composite were not properly handled with view-filter insertion
logic, since I'm reusing the util for now, the issue remains for RLS and
will need to be addressed
This PR refactors object record operation dispatch through a browser
event instead of a state that was registering all operations.
This is a cleaner pattern as it is a synchronous event code path instead
of relying on a useEffect to watch state change, which is not ideal.
We introduce this change first to then rely on this new pattern to
dispatch SSE events in a following PR.
## Summary
Fixes a regression where the save button was not visible when creating a
new role.
## Root Cause
PR #17062 (RLS FE implementation) introduced a change to the `isDirty`
logic that added `isDefined(settingsPersistedRole)` as a condition:
```typescript
const isDirty =
isDefined(settingsPersistedRole) &&
!isDeeplyEqual(settingsDraftRole, settingsPersistedRole);
```
However, in create mode, `settingsPersistedRole` is intentionally set to
`undefined` (in `SettingsRoleCreateEffect.tsx`), causing `isDirty` to
always evaluate to `false` and hiding the save button.
## Fix
Added `isCreateMode` to the `isDirty` condition so the save button shows
when creating a new role:
```typescript
const isDirty =
isCreateMode ||
(isDefined(settingsPersistedRole) &&
!isDeeplyEqual(settingsDraftRole, settingsPersistedRole));
```
cc @Weiko
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Restores save button visibility when creating a role.
>
> - Simplifies `isDirty` to `!isDeeplyEqual(settingsDraftRole,
settingsPersistedRole)`, removing the `isDefined(settingsPersistedRole)`
check so create-mode is considered dirty.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
21593d54c373b15b09406502f8d931cb5e203e46. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Weiko <corentin@twenty.com>
This PR fixes two critical bugs with the DATE type, following up the
recent refactor around dates and time zones :
https://github.com/twentyhq/twenty/pull/16544
There are a lot of related bugs in Sentry, this PR should fix all bugs
that are of the form :
`Cannot parse: 2026-02-04T00:00:00.000Z`
# `node-postgres` date type
The package `node-postgres`, used by TypeORM, returns by default a Date
object for the date OID type.
But we can change this behavior without patching anything.
See the documentation for `pg-types` :
https://github.com/brianc/node-pg-types
Our fix is to call `setTypeParser` from `pg` to have the postgres "date"
type returned as a string always.
```ts
export const setPgDateTypeParser = () => {
types.setTypeParser(PG_DATE_TYPE_OID, (val: string) => val);
};
```
Then in server's main.ts :
```ts
const bootstrap = async () => {
setPgDateTypeParser();
```
## Are we safe with this very low-level fix ?
Since TypeORM bypasses a string value returned by `pg` we are safe with
this modification at a very low-level.
```ts
else if (columnMetadata.type === "date") {
value = DateUtils.mixedDateToDateString(value)
}
```
https://github.com/typeorm/typeorm/blob/0ec4079cd3760dc49247a02c54415f16a2a51766/src/driver/postgres/PostgresDriver.ts#L838
```ts
/**
* Converts given value into date string in a "YYYY-MM-DD" format.
*/
static mixedDateToDateString(value: string | Date): string {
if (value instanceof Date) {
return (
this.formatZerolessValue(value.getFullYear(), 4) +
"-" +
this.formatZerolessValue(value.getMonth() + 1) +
"-" +
this.formatZerolessValue(value.getDate())
)
}
return value
}
```
https://github.com/typeorm/typeorm/blob/6f3788b83730463e3b787b2a98bb41695e13caf8/src/util/DateUtils.ts#L28
For reference the original type parser seems to be configured in
node-pg-types, on the 1182 OID, which is an array of date / 1082 OID,
this type parser calls another small library `postgres-date` which
creates a JS Date. I couldn't find a type parser explicitly on 1082 in
the stack TypeORM -> node-postgres -> node-pg-types -> postgres-date
```ts
register(1182, parseStringArray) // date[]
```
https://github.com/brianc/node-pg-types/blob/26bfe645a8ddc0c73830b1b8c63f2c4f8265b24f/lib/textParsers.js#L168C19-L168C45
```ts
static parse (dateString) {
return new PGDateParser(dateString).getJSDate()
}
```
https://github.com/bendrucker/postgres-date/blob/b3060560ed62c250f7800d29e4b68a9d5a0622bd/index.js#L285
# Calendar view mixing DATE and DATE_TIME
At the time of the refactor, DATE type wasn't properly tested with
calendar view, thus the drag and drop of a card with a calendar view on
a DATE field and not a DATE_TIME field, was not working, this is now
fixed.
- new channel EVENT_STREAM_CHANNEL based on event stream id
- on event, perform the matching and publish only to the right streams
- store a list of active streams per workspace
- store the user id along with the queries for each stream
Bonus:
- remove onSubscriptionMatch
Fixes#17006
## Changes
Added `lastAuthenticateWorkspaceSsoMethodState` Recoil atom to track the
last used SSO method, persisted in localStorage
Updated `SignInUpWithGoogle` component to display a "Last" pill badge
when Google was the last auth method
Updated `SignInUpWithMicrosoft` component to display a "Last" pill badge
when Microsoft was the last auth method
Updated `SignInUpWithSSO` component to display a "Last" pill badge when
SSO was the last auth method.
The state is saved after successful authentication redirect, ensuring it
persists across sessions
## Implementation Details
Uses existing `localStorageEffect` for persistence across browser
sessions
Leverages the existing Pill component from twenty-ui with blue accent
color
The label is positioned on the right border of the button using absolute
positioning
State is saved in the `useAuth` hook during Google/Microsoft sign-in and
in the SSO login component
<img width="684" height="608" alt="image"
src="https://github.com/user-attachments/assets/629a1599-aab0-44e4-b684-ae91a8e725fd"
/>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Highlights the most recently used authentication method and preserves
it across sessions.
>
> - Introduces `AuthenticatedMethod` enum and
`lastAuthenticatedMethodState` (persisted via `localStorageEffect`) and
preserves it through `useAuth.clearSession`
> - Displays a "Last" pill via `LastUsedPill` on `SignInUpWithGoogle`,
`SignInUpWithMicrosoft`, `SignInUpWithSSO`, and
`SignInUpWithCredentials` when appropriate; adds
`StyledSSOButtonContainer` for badge positioning
> - Adds `useHasMultipleAuthMethods` to detect when to show the badge;
threads `isGlobalScope` to relevant components
> - Sets last-used method on click/submit in SSO and credentials flows
(`useSignInUp`, SSO button handlers)
> - Refactors `SignInUpGlobalScopeForm` to use `SignInUpWithCredentials`
and updates `SignInUp` title logic for global scope (e.g., "Welcome to
Twenty")
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
70d552760916e7d5a8c83a33437585ca376253fc. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Félix Malfait <felix@twenty.com>
Closes https://github.com/twentyhq/core-team-issues/issues/2058
We settled on a simpler solution by just hardcoding the objects order
for now. We will change this once it's possible to reorder the workspace
favorites with drag and drop.
Upgraded to Storybook 10. We still use `@storybook/test-runner` for
testing since it appears it'd require more work to move from Jest to
Vitest than I initially anticipated, but I completed this PR to fix
`storybook:serve:dev` - it takes time to load, but it works the way it
used to with Storybook 8.
https://github.com/user-attachments/assets/7afc32c6-4bcf-4b37-b83b-8d00d28dda15
This PR is a follow-up of https://github.com/twentyhq/twenty/pull/16966
and implements a new mechanism to handle SSE events.
It creates only one event stream per browser tab, then use mutations to
tell the backend which query to listen to, without re-mounting the event
stream connexion.
Then each event that comes from this unique subscription is then
dispatched in a new JavaScript CustomEvent, per queryId, on which
specific hooks add an event listener.
This PR introduces the generic tooling as well as the handling of update
events on table.
## Summary
- Updated Relations and Fields tables in the object detail settings page
to use flexible width (`1fr`) for the Name column instead of fixed
pixels
- Aligned column widths between both tables (`1fr 148px 148px 36px`) so
the App and Type/Data type columns line up
## Test plan
- [ ] Navigate to Settings > Data model > select any object (e.g.,
Companies)
- [ ] Verify the Relations table rows take the full available width
- [ ] Verify the Fields table rows take the full available width
- [ ] Verify the App and Type/Data type columns are aligned between both
tables
## Summary
- Fixed the billing credits progress bar background color in dark mode
- The background was hardcoded to `BACKGROUND_LIGHT.tertiary`, causing
it to always display a light color regardless of the current theme
- Changed to use `theme.background.tertiary` to properly respect
dark/light mode
## Test plan
- [x] Navigate to Settings > Billing in dark mode
- [x] Verify the credits progress bar background matches the tertiary
background color (dark in dark mode)
- [x] Verify it still looks correct in light mode
Figma reference:
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=64812-242606&m=dev
## Summary
This PR introduces row-level security (RLS) permissions for roles in the
frontend, allowing fine-grained access control at the record level.
Users can now define permission rules that determine which specific
records a role can access based on dynamic conditions and filters.
## What's Changed
Implemented UI for configuring record-level permissions on object
permissions screens
Added support for defining permission predicates using filter conditions
(similar to advanced filters)
Introduced variable picker for dynamic permission rules (e.g., "me"
context for user-specific access)
Built predicate conversion layer to sync UI state with backend
permission structure
Extended GraphQL schema with mutations for upserting row-level
permission predicates
Fixed handling of orphaned RLS groups to prevent data inconsistencies
Added enterprise key validation for RLS features
The implementation enables scenarios like "users can only see their own
records" or "users can access records associated with their team."
<img width="687" height="702" alt="Screenshot 2026-01-09 at 23 07 02"
src="https://github.com/user-attachments/assets/33fe736e-6cbf-40bd-b2eb-c8a90c8d21bc"
/>
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
This PR introduces a few changes:
- Add three actions: see deleted dashboards, destroy dashboard and
restore dashboard
- Remove the soft delete and restore on all the page layout entities
- Cascade the destruction of a dashboard to a page layout
Video QA:
https://github.com/user-attachments/assets/ab993b11-dd9c-4e88-880c-92691a521cc2
This PR refactors the table virtualization real index main state that
was creating performance problems.
The problem was that we kept track of all the real rows even if we only
print 200 at a time in the application.
This caused a crash when some users with 10k+ rows on a table tried to
iterate over this state 10.000 times or more.
The trade-off is that now we keep all real indices in a Map, and each
virtual row uses a selector to go to this Map.
This way if we want to reset the full Map, we just have to overwrite the
base state that contains the map, and the 200 selectors will recompute.
This happens for example when we delete a row. This now has a limited
and negligible performance impact.
## Summary
- Enable editing for custom calendar event fields while keeping standard
fields read-only
- Load calendar event fields dynamically so custom field values are
editable everywhere they appear
- Preserve calendar event participants rendering
## Testing
- npx nx run twenty-front:lint:diff-with-main --skip-nx-cache
--output-style=stream
- npx jest --config packages/twenty-front/jest.config.mjs
--runTestsByPath
packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx
## Notes
- Full nx lint/test are very slow locally after barrel generation; CI
should run the full suite
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
# Introduction
In this pull request we're introducing new standard page layout, tabs
and widget ( 1 page layout, 1 tab and 8 widgets ) and also a new
opportunity field
Also now prefilling new records, 6 opportunities and a dashboard.
## Standard declaration
### New workspace creation
Relies on existing standard declaration builder
### Backfill command
We've been hacking through the standard builder in order to extract only
the standard page layout entities, updated their entity dependencies to
match the workspace ids so the validation passes
## Remark
- Refactored the `PageLayoutWidget` configuration type to be dynamically
typed through a generic discriminated union
---------
Co-authored-by: prastoin <paul@twenty.com>
**Description**
Fixes the "Add new" button appearing in relation picker dropdowns for
workspace member relations (e.g., Account Owner on companies). Workspace
members cannot be created from relation pickers, so this button should
not appear.
**Changes Made**
- Modified RelationManyToOneFieldInput.tsx to conditionally pass the
onCreate prop to SingleRecordPicker only when
createNewRecordAndOpenRightDrawer is defined.
- Moved the conditional check to the JSX level for clarity.
**Technical Details**
The useAddNewRecordAndOpenRightDrawer hook already returns undefined for
workspace member relations, but the component was still passing a
handler function to SingleRecordPicker, causing the "Add new" button to
appear. The fix ensures that when createNewRecordAndOpenRightDrawer is
undefined, onCreate is also undefined, which prevents
SingleRecordPickerMenuItemsWithSearch from rendering the button.
Testing
- Verified that the "Add new" button no longer appears in the Account
Owner dropdown for companies.
- Confirmed that the "Add new" button still appears for other relation
fields where it's appropriate (e.g., People, Opportunities).
- Tested that existing functionality for selecting workspace members
from the dropdown still works correctly.
Fixes#17060
Upgraded from 8.6.15 to 9.1.17 in two steps:
- 8.6.15 -> 9.0.0
- 9.0.0 -> 9.1.17
I had to disable `storybook-addon-cookie` since it is not supported for
Storybook 9. However, I do intend to upgrade to Storybook 10 when this
is merged, so we can replace the aforementioned add-on with this fork
specifically created to support Storybook 10 and above:
https://www.npmjs.com/package/@storybook-community/storybook-addon-cookie.
Additionally, once we upgrade to Version 10 successfully, I will start
looking into integrating the official Vitest add-on.
- moved a few gql types to twenty shared to re-use in server
- added a new endpoint, onEventSubscription that expect a streamId to
create a connection
- two new endpoints to store queries in Redis
- updated the existing batch channel to directly use object record type
Closes#17032
Summary
When a sort is already applied to a column, clicking "Sort" from the
column header dropdown now toggles the sort direction (ASC → DESC or
DESC → ASC) instead of doing nothing.
Problem
Previously, clicking "Sort" on a column that already had a sort applied
would always try to create a new sort with ASC direction. Since the
upsertRecordSort function updates an existing sort with the same field,
this effectively did nothing when the sort was already ASC—which felt
unintuitive.
Solution
Modified useHandleToggleColumnSort to:
- Check if a sort already exists for the clicked field
- If it exists, toggle the direction
- If it doesn't exist, create a new sort with ASC direction (existing
behavior)
---
Contribution by Gittensor, see my contribution statistics at
https://gittensor.io/miners/details?githubId=132382032