Commit Graph

13789 Commits

Author SHA1 Message Date
Thomas Trompette de3889e04a fix: use aria-current instead of aria-selected on navigation drawer items (#23160)
## Fixes

Closes #23129

## Problem

Navigation drawer items render an `<a>` (or React Router `Link`) with
`aria-selected="true"`. `aria-selected` is only valid on roles such as
`option`, `tab`, `row`, or `gridcell`, not on links, so axe flags WCAG
4.1.2 (aria-allowed-attr): "ARIA attribute is not allowed:
aria-selected=true".

## Fix

Use `aria-current="page"` instead. It is the WAI-ARIA recommended way to
mark the current item in a navigation, it is a global attribute valid on
any role (link, button, or div), and it correctly communicates the
active page to screen readers.


`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx`:

    - aria-selected={active}
    + aria-current={active ? 'page' : undefined}

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23160?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. -->
2026-07-22 15:19:09 +02:00
Paul Rastoin 9043ab3091 Upgrade to 1.0.9 PDL app (#23172)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23172?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. -->
2026-07-22 15:02:42 +02:00
martmull e0debf87a7 fix(call-recorder): listen proper updated fileds event (#23135)
## Context

Around meeting-end peaks (~6pm), Recall/Svix delivers event bursts for
every recorded call across the 700+ workspaces the app is installed on.
Each delivery was processed synchronously in the API request path, and
internal failures surfaced to Svix as non-2xx, so it redelivered — a
self-feeding storm of 500s and latency that only stopped when the
webhook endpoint was disabled.

## What changed

With #23134, server-route dispatch defaults to **queued** server-side:
the API acks Svix with a 202 right after signature verification,
`process-recall-webhook` runs on the worker queue, and failed runs retry
there (resolver `retryLimit`, default 3). The resolver needs no change
at all — `recall-webhook.ts` is back to main, and no SDK update is
required.

Remaining app changes:
- `schedule-recall-bot-on-call-recording-update` declares
`updatedFields` (the pending-transition fields) on its
`callRecording.updated` trigger, so the server drops the app's own
scheduling-progress and artifact writes **before** spawning a full
execution instead of executing and returning "skipped". The in-handler
check stays as a fallback.
- Version bumped to 1.5.0.
- Code comments introduced by earlier revisions of this PR removed per
review.

## Tests

- New test pins the trigger's `updatedFields` declaration.
- `yarn test:unit` (507 tests), `yarn lint`, `yarn typecheck` all green.

## Notes

- The `call-recorder (dockerhub-latest)` CI leg fails because main
already requires `twenty >= 2.23.0` while the latest published image is
2.22.0 — pre-existing, clears when 2.23.0 images publish.
- The 250s `import-call-recording-artifacts` route still runs in an API
request slot behind the fire-and-forget own-route POST; moving it fully
off the request path is a follow-up.
2026-07-22 14:58:25 +02:00
martmull f59bda1dbf feat(server): queued-only server-route dispatch (#23134)
## Context

Follow-up to the incident where 500s and latency spiked around 6pm until
the Recall webhook was disabled. Server routes
(`/webhooks/server/:resolverUid`) ran the resolver **and** the target
logic function synchronously inside the API request, so any handler
throw became a 500 and any slowdown past Svix's delivery timeout marked
the delivery failed — Svix redelivered, feeding load back into the API
in a self-sustaining storm.

## What changed

- Every server-route request acks with **202 `{ queued: true }`** as
soon as the resolver returns; the target runs on `logicFunctionQueue`.
Signature verification stays synchronous in the resolver and still
rejects with a non-2xx. External senders never observe target latency or
failures.
- The resolver contract is unchanged from main: `{ workspaceId,
targetLogicFunctionUniversalIdentifier, payload? }`.
- The target lookup still happens synchronously before enqueueing,
scoped to the resolver's application registration, so unknown targets
404 as before.
- Endpoints whose caller must read the response body (challenge
handshakes, Slack commands) should use `httpRouteTriggerSettings`
routes.

Final diff is 4 files: the server-route service, its spec, the
integration spec, and the docs page. Trigger jobs, fan-out, message
queue, shared types, and SDK are all untouched.

## Tests

- `server-route-trigger.service.spec.ts`: 202 ack + enqueue,
unknown-target 404 without enqueue, resolver auth/contract/error
mapping.
- Integration: `server-route-trigger-authorization.integration-spec.ts`
asserts the 202 queued ack (run locally against a seeded DB, green).
- `typecheck` + `lint:diff-with-main` + `oxfmt` clean.

## Notes

- **Breaking for existing server-route resolvers**: responses are always
202; the target's return value no longer reaches the caller. Existing
resolvers returning response bodies must move those endpoints to
`httpRouteTriggerSettings`.
- A queued target's handler failure is recorded in execution logs but
not retried (same as other queue-executed functions today); retry
semantics are deliberately out of scope here.
- Follow-up candidates: retry-on-failure semantics for queued
executions, `addBulk` for single-round-trip fan-out, declarative
signature verification to take resolver code out of the request path,
moving the call-recorder 250s artifacts import off the API request path.
- Companion PR #23135 (call-recorder): no app change needed for dispatch
— queued dispatch applies by default.
2026-07-22 14:57:18 +02:00
github-actions[bot] 271f9f833b i18n - translations (#23171)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23171?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: github-actions <github-actions@twenty.com>
2026-07-22 14:55:44 +02:00
Raphaël Bosi 0c964aee10 Fix blank pages and add error recovery in onboarding (#23069)
Onboarding showed a blank page in two places: before the plan/payment
step, and briefly after the welcome animation.

`ChooseYourPlan` returned `null` while its `ListPlans` query loaded,
leaving the step empty during the crossfade. It now renders the step
loader, and the plans query is warmed from an earlier onboarding step so
the content is usually already there.

The post-completion redirect lands transiently on `/`, whose route
element was `<></>`, so the welcome animation could reveal an empty
page. It now renders a skeleton, and the `null` Suspense fallbacks on
payment-success and book-call are replaced too.

A failed `ListPlans` query was worse than a blank frame: `PLAN_REQUIRED`
redirects every route back to itself, so the user was locked out of the
product with no way to retry. That step and the billing settings page
now show a retryable error state.

One related fix found on the way: `BlankLayout` had no error boundary,
so a render-time throw anywhere in sign-in or onboarding took down the
whole app. `DefaultLayout` already had one.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23069?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. -->
2026-07-22 14:46:52 +02:00
Abdullah. 7e79b19f73 fix: bump vitest family 4.1.8 -> 4.1.10 (Dependabot) (#23166)
## Summary

Bumps the **vitest family 4.1.8 -> 4.1.10** to clear the **critical**
Dependabot alert
[1800](https://github.com/twentyhq/twenty/security/dependabot/1800):
GHSA-p63j-vcc4-9vmv, @vitest/browser Browser Mode provider commands
bypass the file-access permission gate (no CVE yet; fixed 4.1.10).

`@vitest/browser` is exact-pinned by `@vitest/browser-playwright`
(vitest family version-locks), so the fix is a recursive `yarn up` on
the caret parents (`vitest` and `@vitest/browser-playwright`, both
declared `^4.1.0` by twenty-ui, twenty-front-component-renderer,
twenty-client-sdk). The whole family (browser, expect, mocker,
pretty-format, etc.) moves together to 4.1.10. No resolution, no
`package.json` change.

## Verification

- `yarn install --immutable` passes.
- Diff is `yarn.lock` only; `@vitest/browser` resolves to 4.1.10, no
4.1.8 remains.
- 4.1.10 published 2026-07-06, clears the 3-day npm age gate.
- Dev-scope tooling (vitest test runner), patch-level bump within 4.1.x.
2026-07-22 17:44:39 +05:00
Paul Rastoin 16e7db8577 PDL react 19 (#23169)
Started to face in version 1.07 on staging 2.23.0
```
Failed to load front component: Cannot read properties of undefined (reading 'ReactCurrentBatchConfig')
```
2026-07-22 14:39:51 +02:00
github-actions[bot] ec859faa6a i18n - translations (#23170)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23170?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: github-actions <github-actions@twenty.com>
2026-07-22 14:37:48 +02:00
Thomas Trompette f5ab8a236c fix: add aria-label to record table selection checkboxes (#23147)
## Fixes

Closes #23130

## Problem

On record tables, the select-all header checkbox and each row's
selection checkbox render as `role="checkbox"` with no accessible name,
failing WCAG 4.1.2 (aria-toggle-field-name). Screen-reader users cannot
tell what the checkbox selects.

## Fix

The shared `Checkbox` component already accepts and forwards
`aria-label`, but the record-table callers weren't passing one. Added
translated labels:

- Row checkbox: `aria-label={t\`Select row\`}` in
`RecordTableCellCheckbox.tsx`
- Header checkbox: `aria-label={t\`Select all rows\`}` in
`RecordTableHeaderCheckboxColumn.tsx`

Both use `const { t } = useLingui()` from `@lingui/react/macro`,
following the existing i18n convention in this module.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23147?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. -->
2026-07-22 14:29:41 +02:00
Priyanshu Bartwal b4aa323889 Record Board & Record Calendar Drag Drop dnd-kit rewrite. (#23071)
Closes: #23070


Record Board:


https://github.com/user-attachments/assets/26df63e2-fbbd-4339-88b3-eed73af283b4

Record Calendar:


https://github.com/user-attachments/assets/cb209545-695e-44a1-8dfb-a92b3c8650a6

 ### Technical Inputs
- Each column is a `Droppable`. This differs from other implementations
(Record table header and Record board header), where only the gap
between headers is `Droppable`.
 - Individual cards are `Sortable`.
- Uses `DragOverlay` to display a cloned version of the dragged card
along with a `+N` chip when dragging multiple cards.
- Updated `DragDropColumnDropTarget` to handle `vertical` and
`horizontal` orientation of drop target.
 - File name changes:
    - `DragDropColumnDropTarget` → `DragDropItemDropTarget`
    - `DragDropColumnDroppableSlot` → `DragDropItemDroppableSlot`
    - `DragDropColumnSortableCell` → `DragDropItemSortableCell`
    - `DragDropColumnSortableHandle` → `DragDropItemSortableHandle`
    - `DragDropColumnDndContext` → `DragDropItemDndContext`
    - `DragDropColumnData` → `DragDropItemData`


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23071?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: Félix Malfait <felix.malfait@gmail.com>
2026-07-22 14:21:23 +02:00
Etienne 3223e3c58f fix - fix light overscroll flash in dark mode (#23153)
Before

https://github.com/user-attachments/assets/7dc35369-0c1e-4ca8-9d30-12738c7e7d5b
After

https://github.com/user-attachments/assets/b8dd49c2-9ad3-49d0-a107-6ddfdf1ba531



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23153?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. -->
2026-07-22 13:54:40 +02:00
neo773 0489d39f2b fix(messaging): stop group and bulk emails from creating contacts (#23137)
Group addresses were only checked against the message sender, so
anything arriving as reply-to or cc slipped through and became a
contact. Now checked per participant at contact creation.

The group word list can't keep up with real senders (posts-recap@,
showinfo@, follow-suggestions@), so bulk-mail headers back it up:
`List-Unsubscribe, List-Id, Precedence, Auto-Submitted` (Industry
standard headers)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23137?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. -->
2026-07-22 17:17:45 +05:30
Paul Rastoin 8d943e1f68 Bump to alpha 2 all published apps (#23165)
Related https://github.com/twentyhq/twenty/pull/23155

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23165?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. -->
2026-07-22 13:39:06 +02:00
Abdullah. 3e97b355b4 fix: bump tar 7.5.16 -> 7.5.20 (Dependabot) (#23159)
## Summary

Bumps **tar 7.5.16 -> 7.5.20** on the root `yarn.lock` to clear **4
Dependabot alerts** from this week's node-tar advisory chain, including
the critical one.

| Severity | Advisory | Vulnerable | Fixed | Alert |
|---|---|---|---|---|
| critical | GHSA-23hp-3jrh-7fpw | <= 7.5.18 | 7.5.19 |
[1772](https://github.com/twentyhq/twenty/security/dependabot/1772) |
| high | GHSA-8x88-c5mf-7j5w | <= 7.5.17 | 7.5.18 |
[1771](https://github.com/twentyhq/twenty/security/dependabot/1771) |
| medium | GHSA-w8wr-v893-vjvp | <= 7.5.17 | 7.5.18 |
[1773](https://github.com/twentyhq/twenty/security/dependabot/1773) |
| medium | GHSA-gvwx-54wh-qm9j | <= 7.5.16 | 7.5.17 |
[1770](https://github.com/twentyhq/twenty/security/dependabot/1770) |

The root lockfile has a single tar entry with all-caret consumer ranges
(`^7.4.0` ... `^7.5.16`), so a recursive `yarn up -R tar` lifts it to
the latest patch (7.5.20, published 2026-07-12, clears the 3-day age
gate). The existing scoped tar resolutions (`npm:^7.5.16` for the
@electron/rebuild toolchain and @mintlify/previewing) are caret ranges
and permit it unchanged, so no `package.json` edit.

Note: the remaining 52 tar alerts live in the twenty-apps and
seed-dependencies mini-lockfiles and will be handled separately.

## Verification

- `yarn install --immutable` passes.
- Diff is `yarn.lock` only; single tar entry now 7.5.20, nothing below
remains.
2026-07-22 13:25:55 +02:00
github-actions[bot] 41c32a04e4 i18n - docs translations (#23162)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-22 13:17:44 +02:00
Thomas Trompette 25f28ee299 Fix TWENTY-SERVER-60Y: register permissions exception filter globally (#23104)
## Context

Sentry
[TWENTY-SERVER-60Y](https://twenty-v7.sentry.io/issues/6633503406)
("Permission Denied: Entity performing the request does not have
permission") has ~12.9k occurrences / 151 users. It groups by the shared
throw site `settings-permission.guard.ts:57`, so it's a **catch-all
bucket** for settings-permission denials across many resolvers, not a
single operation. Sampled events include `findOneApplication` (app
runtimes reading their own `applicationVariables`),
`uploadFilesFieldFileByUniversalIdentifier`,
`UpdatePageLayoutWithTabsAndWidgets`, `CreateFileUpload`, etc.

## Root cause

Settings-permission denials are only converted to a client-appropriate
`FORBIDDEN` when a resolver manually attaches
`PermissionsGraphqlApiExceptionFilter`. **28** GraphQL resolvers do;
**~35** guarded resolvers do not. On those, the guard-thrown
`PermissionsException` (a `CustomException`, not a `BaseGraphQLError`)
falls through to the Yoga error hook, is serialized as
`INTERNAL_SERVER_ERROR`, and `shouldCaptureException` reports it to
Sentry as a 500-class error.

REST is not affected: every `SettingsPermissionGuard` controller already
carries `PermissionsRestApiExceptionFilter` (15/15).

## Fix

Register `PermissionsGraphqlApiExceptionFilter` globally via
`APP_FILTER` in the core and metadata engine modules, mirroring the
existing global GraphQL filters `BillingGraphqlApiExceptionFilter` and
`FlatEntityMapsGraphqlApiExceptionFilter`. The filter is guarded on the
GraphQL context (`host.getType()`), so REST keeps its existing
per-controller filter untouched and no request-scoped dependency is
pulled into a global provider.

Every settings-guarded resolver now returns `FORBIDDEN` for denials,
which is both the correct client error code and excluded from Sentry.
Existing per-resolver
`@UseFilters(PermissionsGraphqlApiExceptionFilter)` entries remain valid
(handler-scoped takes precedence, identical result); collapsing them
into the global registration is a possible follow-up.

## Test

Added a case to `granular-settings-permissions.integration-spec.ts`: a
member without the `APPLICATIONS` flag calling
`findOneApplication{applicationVariables{key value}}` (the exact Sentry
query, and a resolver with **no** resolver-scoped filter) must receive
`FORBIDDEN`, exercising the global filter.

Verified against the local test DB:
- With the global filter: passes (`FORBIDDEN`).
- Without it: fails with `INTERNAL_SERVER_ERROR`, reproducing the leak.
- The existing roles / workspace-members / api-keys denial tests
(per-resolver filters) still pass, confirming no precedence conflict and
no boot issue from dual `APP_FILTER` registration.

Fixes TWENTY-SERVER-60Y
2026-07-22 12:10:49 +02:00
Thomas Trompette 623aac5a56 Show agent names on real estate dashboard charts (#23146)
Follow-up to the real estate demo app: the Agency Overview dashboard's
"Listings by agent" and "Showings by agent" bar charts grouped by the
agent relation, which rendered the agent's UUID on the axis.

Adding `primaryAxisGroupBySubFieldName: 'name.firstName'` groups by the
agent's first name instead, so the charts show agent names (Emma, Lucas,
Chloe, Louis).


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23146?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. -->
2026-07-22 12:04:23 +02:00
Paul Rastoin 3ee8b72aa3 twenty-sdk env var to disable prov check (#23155)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23155?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. -->
2026-07-22 11:51:06 +02:00
github-actions[bot] 9d4a564af4 i18n - translations (#23157)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23157?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: github-actions <github-actions@twenty.com>
2026-07-22 11:27:59 +02:00
Abdul Rahman 4c4a154d31 key-value storage for applications (#23089)
## What

Key-value storage for applications, as proposed in
twentyhq/core-team-issues#2391 — built on the existing `keyValuePair`
entity:

- Nullable `applicationId` relation on `keyValuePair` + a new
`APPLICATION_VARIABLE` type (fast instance command included)
- GraphQL CRUD on the metadata schema (`appKeyValue`, `setAppKeyValue`,
`deleteAppKeyValue`), requiring an `APPLICATION_ACCESS` token —
`applicationId` always comes from the token, never from arguments, so
apps can't touch each other's entries
- `kv.get` / `kv.set` / `kv.delete` helpers in
`twenty-sdk/logic-function`

## Scopes

- **`INSTALL`** (default): entries are private to one workspace install;
arbitrary JSON values
- **`GLOBAL`**: entries are shared across every install of the app, with
claim semantics — the value is always the claiming `workspaceId` and
only that workspace can overwrite or delete the key (guarded writes,
race-safe via insert-if-absent)

Since `applicationId` identifies an install (one row per workspace),
GLOBAL entries are stored under the registration owner workspace's
install so all installs of the same app share one namespace.

The GLOBAL scope is what enables cross-workspace webhook routing: e.g.
the Slack app's `serverRoute` resolver (running in the owner workspace)
can resolve `kv.get('slack:team:' + team_id, { scope: 'GLOBAL' })` to
find the workspace that connected that Slack team — without a workspace
being able to hijack another's mapping.

## Follow-ups

- Wire the Slack assistant PR (#22984) to write the claim at connect
time and read it in the events resolver
- `kv.*` access from front components

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23089?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. -->
2026-07-22 11:19:44 +02:00
Abdullah. 8b0cd00357 fix: bump websocket-driver 0.7.4 -> 0.7.5 (Dependabot) (#23142)
## Summary

Bumps **websocket-driver 0.7.4 -> 0.7.5** to clear **2 Dependabot
alerts** on the root `yarn.lock`, including the critical one from this
week's advisory wave.

| Severity | Advisory | CVE | Alert |
|---|---|---|---|
| critical | GHSA-xv26-6w52-cph6 | CVE-2026-54466 |
[1577](https://github.com/twentyhq/twenty/security/dependabot/1577) -
message corruption via abuse of protocol length headers |
| medium | GHSA-mp7j-qc5w-4988 | CVE-2026-54490 |
[1576](https://github.com/twentyhq/twenty/security/dependabot/1576) -
resource limit bypass via message compression |

websocket-driver is transitive via **faye-websocket** (`>=0.5.1`) and
**sockjs** (`^0.7.4`); both ranges already permit 0.7.5, so a recursive
`yarn up -R websocket-driver` lifts it with no resolution and no
`package.json` change.

## Verification

- `yarn install --immutable` passes.
- Diff is `yarn.lock` only; websocket-driver resolves to 0.7.5, no 0.7.4
remains.
- 0.7.5 published 2026-06-04, clears the 3-day npm age gate.
2026-07-22 14:05:01 +05:00
twenty-pr[bot] 10a313dc7f chore: bump version to 2.24.0 (#23152)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version
- Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same
version

## Checklist

- [ ] Verify version constants are correct
- [ ] Verify npm package versions match

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23152?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: Github Action Deploy <github-action-deploy@twenty.com>
2026-07-22 10:48:10 +02:00
Paul Rastoin 893db7558e Fix instance fast migration, fallback index creation (#23149)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23149?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. -->
2026-07-22 10:22:41 +02:00
github-actions[bot] 7fa3c5c77b chore: sync AI model catalog from models.dev (#23143)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-22 08:48:42 +02:00
Weiko 984944e9bc Fix workspace cache flush versioned metadata (#23121)
## Problem

`WorkspaceCacheStorageService.flushVersionedMetadata` never deleted
several of the keys it was supposed to flush:

- When called without a `metadataVersion` (the dev seeder path), it
built keys ending in `:*` and passed them to `cacheStorageService.del`.
`del` is an exact-key delete with no glob support, so this branch
deleted nothing at all.
- `setGraphQLTypeDefs` and `setGraphQLUsedScalarNames` can write
applicationId-suffixed keys
(`{key}:{workspaceId}:{metadataVersion}:{applicationId}`), which the
flush loop never matched. On workspace deletion these SDL and scalar
entries survived in Redis until the 1-week TTL expired.
- The `MetadataVersion` key is stored without a version suffix
(`metadata:workspace-metadata-version:{workspaceId}`), but the flush
loop appended `:{metadataVersion}` to every key, so it never matched
either.

## Fix

`flushVersionedMetadata` now targets the key shapes that are actually
written:

- The `MetadataVersion` key is deleted by its exact, unversioned shape.
- With a known `metadataVersion`, each versioned key gets an exact `del`
on `{key}:{workspaceId}:{metadataVersion}` plus a `flushByPattern` on
`{key}:{workspaceId}:{metadataVersion}:*` to catch
applicationId-suffixed entries. The pattern requires the trailing colon
so flushing version 1 cannot match version 12.
- Without a `metadataVersion`, each versioned key is flushed with
`flushByPattern` on `{key}:{workspaceId}:*`.

`flushByPattern` is Redis-only, which is safe here: the cache module
factory hardcodes the Redis store, and `flushGraphQLOperation` in the
same service already relies on it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23121?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. -->
2026-07-22 01:28:19 +02:00
github-actions[bot] c503d4c4aa i18n - docs translations (#23136)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-21 20:59:03 +02:00
Etienne 5add5ea695 fix(billing) - expand billing sub uniqueness constraint (#23123)
fixes :
https://discord.com/channels/1130383047699738754/1528956737363771497

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23123?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. -->
2026-07-21 17:04:10 +00:00
github-actions[bot] af6f3c0c06 i18n - translations (#23126)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-21 19:00:53 +02:00
Paul Rastoin c59df4cddd Upgrade remaining official apps to 2.23 alpha (#23124)
The sdk does not provide system field anymore at all ( including
relation ) if you don't upgrade you'll get a deterministic universal
identifier collision from previously provision and now side effect
resulting ones

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23124?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. -->
2026-07-21 18:59:38 +02:00
github-actions[bot] 9742c21a99 i18n - docs translations (#23125)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23125?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: github-actions <github-actions@twenty.com>
2026-07-21 18:58:59 +02:00
Thomas Trompette 9a374c4b6d Add real estate demo app (#23111)
## What

A new app under `packages/twenty-apps/internal/real-estate` that seeds a
real-estate demo workspace: buyers, sellers, agents, property listings,
showings, and an opportunity pipeline, with role-based access.

## Data model

- **Property** (custom object): address, price (currency), status
(coming soon / active / under offer / sold), type, beds/baths/surface,
photos, `listingAgent` and `sellerContact` relations to Person.
- **Showing** (custom object): scheduledAt, status, feedback, interest
rating, and `property` / `buyer` / `agent` / `opportunity` relations.
- **Person** (standard, extended): `personType` (Buyer / Seller /
Agent), budget min/max, pre-approved, desired area.
- **Opportunity** (standard, extended): `buyerStage` pipeline
(completing profile → showing → offer made → closing → won → lost), and
`buyer` / `seller` / `property` / `showings` relations.

## Views

- **Buyer Pipeline** — kanban on Opportunity grouped by buyer stage, one
card per buyer, scoped to real-estate deals.
- **Available (by price)** — properties sorted by price desc, excluding
sold.
- **Agents** / **Buyers** — filtered Person views.
- Record-page layouts for Opportunity and Showing so the relations
render on the detail pages.

## Roles

- **Broker** — full access (default).
- **Agent** — Property / Showing / Person / Note / Task, no Opportunity
access.
- **Seller** — read-only on their listing and its showings.

## Seeding

A synchronous post-install logic function seeds 4 agents, 12 sellers, 12
buyers, 30 properties across 5 cities, 24 showings, and 12 opportunities
(one per buyer), all wired through the relations.

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-07-21 18:53:23 +02:00
Weiko 28adcbffb9 Remove dead code from legacy metadataVersion cache mechanism (#23122)
- Drop unused setORMEntitySchema/getORMEntitySchema from
WorkspaceCacheStorageService
- Drop MetadataObjectMetadataMaps cache key, only referenced by the
flush loop
- Drop never-populated workspaceMetadataVersion field from workspace
auth context type and builders
- Drop unthrown TwentyORMExceptionCode.METADATA_VERSION_MISMATCH
- Drop unused WorkspaceMetadataVersionModule imports in field-metadata
and object-metadata modules

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23122?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. -->
2026-07-21 18:52:51 +02:00
github-actions[bot] cf5e6b7bad i18n - website translations (#23096)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23096?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: github-actions <github-actions@twenty.com>
2026-07-21 17:45:30 +02:00
Raphaël Bosi 578d69e4fb Fix update events reporting untouched columns as changed (#22797)
## Before


https://github.com/user-attachments/assets/b02dbd50-87c9-4699-a6a9-254a4c8b9182

The image uploaded during the onboarding is deleted and doesn't appear
in the animation

## After


https://github.com/user-attachments/assets/9443b837-044e-47c6-b2df-c392c238e935

The Image appears in the animation

## Description

TwentyORM's `.save()` emits UPDATE events whose `after` record carries
default (empty) values for columns that weren't written: TypeORM
null-injects untouched nullable columns on the entity in place, and
`formatResult` turns those into empty strings. So a partial update (e.g.
renaming a workspace member) reports untouched fields like `avatarUrl`
and `userEmail` as changed, which is what made the avatar-file-deletion
listener delete the picture uploaded during onboarding. The same stale
data also reaches webhooks, workflow/logic-function triggers and record
subscriptions.

Fix: `save()` was the only write path building its event `after` from
the in-memory payload instead of re-reading the row. `update()`,
`upsert()` and `softDelete()` all re-SELECT after the write, so `save()`
now does the same.

`withDeleted` is on both the before and the after find, since `save({
id, deletedAt })` and `save({ id, deletedAt: null })` are valid
soft-delete and restore, and an asymmetric find would leave a restored
row with no matching before-record.

Worth noting: a genuinely no-op save now emits no update event, where it
previously emitted one with a bogus diff.
2026-07-21 17:44:18 +02:00
BOHEUS 00e5917d4d Documentation update (#23091)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23091?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. -->
2026-07-21 17:39:39 +02:00
Weiko 23cf85f745 Fix invalid UUID insert in workflow core-links backfill (#23118)
## Fix invalid UUID insert in workflow core-links backfill

### Problem
The `2-23:backfill-workflow-core-links` workspace upgrade command failed
with:

```
QueryFailedError: invalid input syntax for type uuid: "" (22P02)
```

The `core."workflow"."lastPublishedVersionId"` column is a `uuid`, but
some workspace workflows store an empty string `""` (not `NULL`) for
that field. The code used `workflow.lastPublishedVersionId ?? null`, and
`??` only falls back on `null`/`undefined` — so `""` was passed straight
through and Postgres rejected it.

### Fix
Use `|| null` instead of `?? null` so empty strings are normalized to
`null` before insertion into the `uuid` column.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23118?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. -->
2026-07-21 17:22:53 +02:00
github-actions[bot] ec8d9c38c4 i18n - docs translations (#23117)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-21 17:17:37 +02:00
github-actions[bot] 0ce90d5c82 i18n - translations (#23116)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23116?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: github-actions <github-actions@twenty.com>
2026-07-21 17:13:07 +02:00
Paul Rastoin 71a1ff7ac8 Cache twenty-client-sdk modules host-side via content-addressed URLs (#22981)
## Context

Front component sources are fetched host-side and integrity-verified by
the SHA-256 checksum embedded in their URL, cached in Cache Storage — a
layer that exists specifically because their download URLs are presigned
and rotate. The `twenty-client-sdk` modules (`core` and `metadata`) were
re-fetched on every render and could not be cached safely: their URLs
carried no checksum and the server exposed no freshness signal.

This PR makes the SDK module URLs **content-addressed** and relies on
the **browser HTTP cache** for immutability, and it keys the checksums
on their real owners: the **application** for `core`, the **instance**
for `metadata`. The checksum does double duty: cache invalidation
(regeneration changes the checksum → the URL changes → guaranteed cache
miss) and a server-side cacheability guard (the server only grants
`immutable` when the checksum in the URL matches the authoritative
checksum it knows for that module — persisted at generation time for
`core`, hashed once at bootstrap for `metadata` — so no per-request
hashing of the served bytes). Note this is **not** an end-to-end
integrity guarantee: there is no client-side hash verification, and on a
fingerprint mismatch the server still serves the current bytes with
`no-store` (self-healing for stale URLs) rather than failing.

<img width="2412" height="926" alt="image"
src="https://github.com/user-attachments/assets/d97935d2-0fdb-4c44-89ac-596b7ca8ca64"
/>

Closes twentyhq/core-team-issues#2688.

## Routes

| Module | URL | Scope |
| --- | --- | --- |
| `core` | `/rest/sdk-client/{applicationId}/core[/{checksum}]` | Per
application (generated bundle) |
| `metadata` | `/rest/sdk-client/metadata[/{checksum}]` |
**Instance-wide**: no application segment, so every application
converges on one URL and the browser downloads the module once per
release instead of once per application |

The previous application-scoped metadata path
(`/rest/sdk-client/{applicationId}/metadata[/{checksum}]`) is **kept for
backward compatibility**, new clients just stop generating those URLs.
The instance-wide route is declared before the parameterized route so
`metadata/{checksum}` is not swallowed as `:applicationId/:moduleName`.

## Caching model

| Request | `Cache-Control` | Effect |
| --- | --- | --- |
| Fingerprinted URL, checksum matches the known module checksum |
`immutable` | Cached indefinitely by the browser HTTP cache; a new
checksum is a new URL |
| Fingerprinted URL, checksum does not match | `no-store` | Current
bytes served uncached (self-healing for stale URLs) |
| Bare URL (pre-generation fallback, `core` only in practice) |
`no-store` | Never cached |

- Both responses also set `X-Content-Type-Options: nosniff` and
`Content-Type: application/javascript`.
- SDK modules are intentionally **not** placed in Cache Storage. That
layer stays reserved for the presigned/rotating component-source URLs;
SDK modules are served directly and authenticated, so the browser HTTP
cache (keyed by the content-addressed URL) is their single cache layer.

## Checksum provenance

- **core** — per **application**, persisted on
`application.sdkClientCoreChecksum` at generation time and read back
from `flatApplicationMaps` (never re-hashed per request).
- **metadata** — **instance-wide**, hashed once from the installed
`twenty-client-sdk/dist/metadata.mjs` package (warmed at bootstrap,
memoized per process) and served straight from that package, so it is
fresh from the first request after a release with no archive dependency.

## Server (twenty-server)

- Hash `dist/core.mjs` at SDK generation and persist
`sdkClientCoreChecksum` via `applicationRepository.update`. Adds the
nullable text column to `application.entity.ts` (mirroring
`packageJsonChecksum`) plus a fast instance command with up/down;
`FlatApplication` picks it up automatically.
- New **application-scoped** query
`applicationSdkClientChecksums(applicationId: UUID!):
SdkClientChecksums` on `ApplicationResolver` (metadata schema,
`WorkspaceAuthGuard` + `NoPermissionGuard`). `SdkClientChecksums.core`
is **nullable** and stays `null` until the SDK has been generated at
least once; `metadata` is **always present** (bootstrap-warmed), so the
metadata module is cacheable from the very first render of any app. The
query itself returns `null` only for unknown applications.
- `SdkClientChecksumsDTO` now lives in the shared
`core-modules/sdk-client/dtos/`. `FrontComponentDTO` and the
`frontComponent` resolver no longer carry checksums (decoupled from the
front-component row).
- `sdk-client` controller: instance-wide `metadata[/:checksum]` route
(no workspace-cache or application lookup, serves the memoized installed
module) + application-scoped `:applicationId/:moduleName[/:checksum]`
route (serves `core` from the per-application archive, `metadata` kept
for back-compat). Cacheability compares the URL checksum against the
**known** checksum — persisted `sdkClientCoreChecksum` for `core`,
memoized package hash for `metadata` — instead of hashing the served
bytes on every request: `immutable` on match, `no-store` otherwise (bare
URL or stale fingerprint), plus `nosniff`. A persisted checksum out of
sync with the archive only downgrades to `no-store` until the next
regeneration.

## Front (twenty-front)

- New metadata query `GetApplicationSdkClientChecksums`, keyed by
`applicationId`; removed the `sdkClientChecksums` selection from
`FindOneFrontComponent`.
- `getSdkClientUrls` builds the two module URLs independently:
`/sdk-client/{applicationId}/core/{checksum}` and the **instance-wide**
`/sdk-client/metadata/{checksum}` (no application segment → one shared
browser cache entry per release across all applications). Each falls
back to its bare URL when its checksum is absent — since `core` is
nullable, a never-generated app still gets a content-addressed metadata
URL and only `core` falls back. The checksum type is sourced from the
codegen `SdkClientChecksums` type rather than a hand-maintained
duplicate.
- `FrontComponentRenderer` is split into a gating outer component (runs
`FindOneFrontComponent`, renders nothing while loading) and a content
component that receives a guaranteed-non-null `frontComponent`.
Following project conventions, the side effects live in dedicated effect
components: `FrontComponentLoadErrorSnackBarEffect` (query error →
snackbar) and `FrontComponentApplicationTokenPairEffect` (mirrors the
query-derived token pair into component state unconditionally, `null`
included, so revoked credentials can never be retained or refreshed).
The content component fetches checksums via the application-keyed query
and **gates the mount of SDK-using components on that query**, so the
very first module fetch is always the content-addressed (`immutable`)
URL instead of the bare `no-store` one. Non-SDK components skip the
query and are never blocked.
- **Live invalidation without reload:** SDK regeneration updates the
application row, and the server broadcasts an `application` metadata
event carrying the new core checksum.
`useOnApplicationSdkClientChecksumsUpdated` /
`useUpdateSdkClientChecksumsApolloCache` patch the application-keyed
checksum query cache (core only; the instance-wide metadata is
preserved), so every mounted component of that application picks up the
new URL at once. This replaces the previous frontComponent-derived field
and closes the earlier "known gap" (a mounted component staying on a
session-old checksum until a full reload). The cache-patching callback
is memoized (`useCallback`) so the window listener is registered once
per application, and the listener is **skipped entirely** for non-SDK
components (`useListenToMetadataOperationBrowserEvent` gained a `skip`
option) — they register no listener and never refetch a query they don't
consume.

## Renderer (twenty-front-component-renderer)

- SDK sources are fetched through a dedicated plain authenticated fetch,
`fetchJavaScriptModuleSourceText` (Bearer header, `credentials:
'omit'`), instead of the Cache Storage `fetchComponentSource` path;
`fetchSdkClientSources` uses it. Execution stays exclusively in the
opaque-origin worker via blob URLs; the host only fetches and forwards
source strings (no hashing host-side). Staleness self-resolves through
the checksum: new checksum → new URL → cache miss.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22981?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. -->
2026-07-21 15:05:00 +00:00
Etienne 8f704e87e3 fix(ai-chat) - fix AI record references when display names contain markdown characters (#23113)
<img width="516" height="86" alt="Screenshot 2026-07-21 at 16 41 16"
src="https://github.com/user-attachments/assets/6e14b933-b48e-49ab-856b-400efdeba53a"
/>




## Summary
- Switch record references from `[[record:object:id:label]]` to
`[[record:object:id:label[[/record]]` so labels can include `]`,
backticks, brackets, and other markdown-significant characters
- Parse references with an explicit close tag (still accepting legacy
`]]`), escape labels before markdown lexing, and serialize mentions
through a shared formatter
- Update the AI chat system prompt so the model emits the new format

## Test plan
- [ ] Ask AI about a record whose name contains `` ` ``, `[`, `]`, or
`]]` and confirm it renders as a chip, not broken markdown
- [ ] Confirm legacy `[[record:...]]` references still chip correctly
- [ ] Mention a record in the chat editor and verify serialized text
uses `[[/record]]`
- [ ] Run:
- `npx jest src/modules/ai/utils/__tests__/findRecordReferences.test.ts
src/modules/ai/utils/__tests__/formatRecordReference.test.ts
src/modules/ai/utils/__tests__/protectRecordReferencesForMarkdown.test.ts
src/modules/ai/components/__tests__/TextWithRecordLinks.test.tsx
--config=packages/twenty-front/jest.config.mjs`
  - mention extension tests for `MentionTag` / `MentionSuggestion`

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23113?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. -->
2026-07-21 17:00:29 +02:00
martmull f5b06d20e4 Add per-application API rate limiter (#23100)
## Context

Application-token API requests are not rate limited today:
`throttleQueryExecution` in the common query runner only throttles
API-key requests, per workspace. An application installed on hundreds of
workspaces (Call Recorder is on 700+) can therefore hit the API with
large synchronized bursts, as seen with the recovery crons impacting
production.

## What changed

- New rate limiter in `CommonBaseQueryRunnerService`, applied when the
auth context is an application context, using the existing
`ThrottlerService.tokenBucketThrottleOrThrow` like the per-workspace
API-key throttle. Both REST and GraphQL record operations go through
this path.
- The limiter key is `api:throttler:application:{universalIdentifier}`
with no workspace component: the budget is shared by every installation
of the application on the instance, which is what protects production
from install-count-proportional load. `universalIdentifier` was chosen
over `applicationRegistrationId` because the latter is null for
unpublished/local applications.
- Two new config variables in the `RATE_LIMITING` group:
`APPLICATION_API_RATE_LIMITING_LIMIT` (default 500) and
`APPLICATION_API_RATE_LIMITING_TTL_IN_MS` (default 60000), i.e. 500
requests/min per application across all workspaces.
- Rejections raise the existing `ThrottlerException`, already mapped by
both the REST and GraphQL exception handlers, and increment a new
`common-api-query/application-rate-limited` metric (only for throttler
rejections, so cache infrastructure failures are not reported as rate
limiting).
- Cron trigger dispatch `retryLimit` raised from 3 to 10 so throttled
logic function executions eventually run once the budget refills.

The existing per-workspace API-key throttle is unchanged (extracted to
its own method).

## Notes

- The limit is tunable per environment without a deploy pipeline change.

## Test

- `npx nx lint:diff-with-main twenty-server` clean.
- `npx nx typecheck twenty-server` clean.
- Throttler spec passes (5 tests).

---------

Co-authored-by: martmull <martin@twenty.com>
2026-07-21 14:40:04 +00:00
github-actions[bot] d1087d5fc7 i18n - translations (#23114)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23114?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: github-actions <github-actions@twenty.com>
2026-07-21 16:30:12 +02:00
martmull e14c32015f Make upgrade applications batch size a job parameter defaulting to 5 (#23101)
Makes the batch size used when upgrading applications a parameter
instead of a hardcoded constant, defaulting to 5, and lets admins set it
from the upgrade confirmation modal.

Backend:
- `UpgradeApplicationsJobData` gains an optional `batchSize` field,
passed through by `UpgradeApplicationsJob` to the service.
- `ApplicationUpgradeService.upgradeAllApplications` accepts an optional
`batchSize` parameter, defaulting to
`UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5` (previously a fixed batch
size of 20). The value is sanitized to a positive integer to avoid an
infinite batching loop.
- The `upgradeRegistrationApplications` admin mutation accepts an
optional `batchSize: Int` argument and forwards it to the job.

Frontend (admin panel):
- The "Upgrade existing installations" confirmation modal now includes a
"Batch size" number input, defaulting to 5, sent with the mutation.
- Updated the admin GraphQL document and generated types.

## Screenshots

Upgrade section on the admin app registration page:

![Upgrade
section](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-section.png)

Confirmation modal with the new batch size input (defaults to 5):

![Upgrade confirmation modal with batch size
input](https://raw.githubusercontent.com/twentyhq/twenty/8f6f7b5b87b8218e10f9f8ed8a8b705cf25f22f6/upgrade-modal-batch-size.png)

---------

Co-authored-by: Martin <martin@twenty.com>
2026-07-21 14:22:19 +00:00
Raphaël Bosi 07be5e0892 Forward editing and clipboard events to front components (#22630)
Adds the text-editing events input-heavy front components need:
`beforeinput`, `compositionstart/update/end` and `copy/paste/cut`,
allowed on `input` and `textarea` only.

These events carry payload: `beforeinput` forwards `inputType`/`data`
through a native host listener (React synthesizes `onBeforeInput`
without them), composition events forward `data`, and paste forwards
`clipboardData.getData('text')` capped at 100k chars. Clipboard text is
read only on an explicit paste into the component's own input, never on
copy/cut, and the worker synthesizes a minimal `clipboardData` so
`onPaste` handlers work. `beforeinput` is observe-only: `preventDefault`
cannot cross the async worker boundary.

Allow-listing these events makes the host bind them, so the
`buildHostReactPropsFromRemoteProps` test that pinned them as rejected
now pins events that are still unmapped.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22630?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. -->
2026-07-21 14:21:53 +00:00
Madan kumar 8b0e7a93a4 fix(shared): multi-select "contains any" filter matcher should use OR semantics (#23010)
The in-memory `isMatchingMultiSelectFilter` evaluated the `containsAny`
operand with `Array.every`, which requires a record to hold **all**
selected options. But `containsAny` means "any overlap": the server
evaluates it as a Postgres array-overlap (`field::text[] &&
ARRAY[...]`), and the "Contains" UI operand for a MULTI_SELECT field
builds exactly this operand — both match on **at least one** shared
option.

So the matcher disagreed with the server. In a "Tags contains any of [A,
B]" view, an optimistic create/update of a record whose tags are just
`[A]` was treated as not matching, so it failed to appear (or was
wrongly dropped) until a refetch; `DOES_NOT_CONTAIN` (built as `not {
containsAny }`) inverted the same way. The same helper backs the
row-level-permission predicate matcher.

Switched to `Array.some` to match the OR semantics, and updated the
tests (partial-overlap, single-overlap, no-overlap, empty-array). The
sibling `isMatching*Filter` helpers were checked — this is the only
affected one.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23010?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. -->
2026-07-21 16:19:06 +02:00
Weiko 024b379e1a Instrument Node runtime and workspace cache metrics (#23107)
## Context

High API tail latency can come from either downstream work or the
Node.js process itself being unable to schedule work. Existing traces
expose database and HTTP spans, but they do not provide a continuous
event-loop signal or identify time spent rebuilding individual workspace
metadata cache entries.

## What changed

- Enable Sentry's built-in Node runtime integration with a 30-second
collection interval.
- Collect only event-loop delay p99, event-loop delay max, and
event-loop utilization. CPU, memory, p50, and uptime metrics remain
disabled because existing infrastructure telemetry already covers those
areas.
- Add a parent span around workspace metadata cache invalidation and
recomputation.
- Add child spans around cache-provider computation, including the cache
key, recomputation strategy, and whether the provider uses local data
only.

## Telemetry scope

- Cache hits do not create spans.
- Cache spans use `onlyIfParent`, so they are recorded only inside an
already-sampled trace.
- Runtime metrics are three low-cardinality values every 30 seconds per
server process.
- This does not add Prometheus histograms, per-cache-key metric labels,
database pool gauges, or a custom runtime collector.
- No cache behavior or invalidation semantics change.

This should let us distinguish event-loop stalls from downstream
latency, then identify which cache provider contributes to a slow cache
rebuild without materially increasing telemetry volume.
2026-07-21 14:02:58 +00:00
github-actions[bot] 64891c561d i18n - translations (#23108)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23108?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: github-actions <github-actions@twenty.com>
2026-07-21 15:48:30 +02:00
Félix Malfait 3ad3e8bd1a feat: kanban, calendar and group-by table layouts for dashboard view widgets (#22963)
## Context

Dashboard view widgets previously only rendered flat tables. This PR
ships the full feature: **Table with group-by**, **Kanban**, and
**Calendar** layouts for dashboard view widgets — server API + frontend,
end-to-end. (Originally staged as a 4-PR stack — #22966, #22967, #22968
— consolidated here per review.)

## Server / API

- **View typing.** Adds `KANBAN_WIDGET` and `CALENDAR_WIDGET` to
`ViewType` (following the `TABLE_WIDGET` precedent) so widget-backing
views keep their layout in `view.type` while staying excluded from
record-index pickers. Shared `getViewLayoutFromViewType()` maps widget
types to their base layout; `isWidgetViewType()` centralizes the
exclusions that were previously hardcoded per-site.
- **Migrations.** Two fast instance commands (**2.23**): `ALTER TYPE
core.view_type_enum ADD VALUE` for both values, and a widened
`CHK_VIEW_CALENDAR_INTEGRITY` constraint covering `CALENDAR_WIDGET`
(entity `@Check` updated for fresh installs).
- **Validation.** `FlatViewValidatorService` keys kanban/calendar
validation on the mapped layout, so widget views get the same invariants
as index views (kanban needs a groupable group-by field; calendar needs
a date field + layout). Calendar widget views default to month; a
non-month (DAY/WEEK) layout is rejected at the API level **unless** the
`IS_CALENDAR_WEEK_VIEW_ENABLED` feature flag is enabled for the
workspace — the same flag that gates day/week on index calendars.
- **API.** `upsertViewWidget` (LAYOUTS permission) accepts a nested
`view` settings input (`type`, `mainGroupByFieldMetadataId`,
`shouldHideEmptyGroups`, kanban aggregate/column-width, calendar
layout/fields). Routes through the standard update path, so `viewGroups`
auto-generate from SELECT options exactly like index views. Only widget
view types accepted; only `RECORD_TABLE` widgets can change view
settings.
- **AI tools.** `create-complete-dashboard` + `create_view` now
use/allow the `*_WIDGET` types (previously they created plain `TABLE`
views that leak into index pickers).

## Frontend

**Settings panel.** The **Source** (object) row comes first, since which
layouts are available depends on it. The **Layout** row below is a
working dropdown (Table / Kanban / Calendar); layouts the source object
can't support are **disabled with a hint** ("Needs a Select field" /
"Needs a Date field") rather than hidden. Group-by row (select fields;
searchable) with a **Hide empty groups** toggle while grouped; **Date
field** row replaces Group by while Calendar is active, and — when the
`IS_CALENDAR_WEEK_VIEW_ENABLED` flag is on — a **Calendar view** row
(Day / Week / Month) appears beside it; **Limit** row hidden while
grouped (only the flat virtualized loader enforces it). Kanban keeps its
group-by locked (no `None` option).

**Instant edit-mode preview.** Draft snapshots carry `viewGroups`;
picking a group-by synthesizes them client-side
(`buildDraftViewGroupsForFieldMetadataItem`, mirroring the server&#39;s
generation), so grouped tables/boards preview immediately before
dashboard save. On save, `upsertViewWidget` responses hand back the
server-generated groups, which replace the client-generated ones in the
persisted snapshot.

**Renderers.** `RecordTableWidgetRendererContent` branches on the
backing view&#39;s layout: `RecordBoardWidget` (wraps the standard
`RecordBoardContainer`) and `RecordCalendarWidget` (mounts the existing
`RecordCalendar`, which renders month / day / week) inside the same
per-widget provider sandbox the table uses.

**Read-only semantics.** Two flags with distinct scopes, each documented
on its state:
- `isRecordBoardViewSettingsReadOnlyComponentState` — locks the board
chrome that edits view settings (add group, column reorder/resize/menu,
aggregates); **card drag still updates records** under object
permissions.
- `isRecordCalendarReadOnlyComponentState` — widget calendars are
read-only by default (no drag, no add-new, no in-calendar layout
switch); cards open the side panel. The one exception, behind
`IS_CALENDAR_WEEK_VIEW_ENABLED`: a **live (non edit-mode) day/week**
widget calendar allows drag-to-reschedule and record creation under
object permissions. Month calendars and edit-mode previews stay
read-only.

**Calendar state componentization.** The calendar module&#39;s three
settings move from global atoms to component states keyed on
`RecordCalendarComponentInstanceContext` (same pattern as record-board),
so several calendar widgets and an index-page calendar can coexist
without leaking state. All readers resolve the ambient instance;
calendar unit tests updated.

**Multi-instance fixes that also fix index pages:** record drag states
were written against a different instance than every reader resolves
(now use the ambient instance); the board sticky-header DOM id is
namespaced per board; dragged board cards portal to `document.body`
while dragging so react-grid-layout&#39;s transforms can&#39;t offset
the clone from the pointer.

## Scope (v1)

- Widget calendars are month-only and read-only by default. With
`IS_CALENDAR_WEEK_VIEW_ENABLED` enabled, day/week layouts become
selectable (UI + API) and live day/week widget calendars support
drag-to-reschedule and record creation under object permissions.
- Widget group-by offers SELECT fields only (server auto-generates
groups from options; widgets have no per-record add-group flow).

## Tests

- Integration: `upsert-view-widget-view-settings.integration-spec.ts` (9
tests — group auto-creation, invalid type/field rejections, non-month
calendar widget rejected while the week/day flag is off and accepted
once it&#39;s enabled, combined settings+fields call); pre-existing
`upsert-view-widget` suite (20) green.
- Front: new suites for draft view-group generation and snapshot
clone/build utils; calendar suites componentized; full `twenty-front`
jest, typecheck, oxlint green; `twenty-server` typecheck + lint green.
- Browser-verified end-to-end (real dev server + seeded workspace):
configure → live edit-mode preview → save → reload for all three
layouts; measured drag with pointer inside the card; index-page calendar
re-verified (with the week/day flag enabled).

https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf
2026-07-21 15:41:08 +02:00
Paul Rastoin 2ef7b7824e Upgrade call-recorder, people-data-labs, last-contact and partners apps to twenty-sdk 2.23.0-alpha.1 (#23098)
## What

Upgrades the two breaking-change-prone apps to `twenty-sdk` /
`twenty-client-sdk` `2.23.0-alpha.1`, and adds the server-side hook that
lets the 2.23 upgrade install them:

- **people-data-labs**
- **partners**

Follows up on #22882 (System side effect relations), which re-derived
the system relation field universal identifiers name-free and shipped
`getSystemRelationFieldUniversalIdentifier` in the SDK.

## How

- **people-data-labs**: bump the SDK to `2.23.0-alpha.1`. The enriched
views temporarily hardcoded the new system relation identifiers with a
TODO because the SDK still embedded the old values; now that the
name-free identifiers ship in `2.23`, derive them from
`STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.{company,person}.fields.{noteTargets,taskTargets,attachments,timelineActivities}.universalIdentifier`
(identical to the previously pinned values, verified). Engine already
pinned `twenty >=2.23.0`; app stays `1.0.7` (manifest unchanged).
- **partners**: bump the SDK to `2.23.0-alpha.1`. The partner role
references
`opportunity.fields.{taskTargets,noteTargets,attachments,timelineActivities}`
universal identifiers, which the SDK now resolves to the `2.23`
name-free values. Pin `engines.twenty >=2.23.0` and bump the app to
`1.3.1`.
- **server**: add an opt-in `skipWorkspaceCompatibilityCheck` to the
install/upgrade path. The `upgrade-people-data-labs-application` 2.23
command runs mid-upgrade, before the workspace is marked as having
completed 2.23, so the workspace-compatibility check would otherwise
reject installing `1.0.7` (`engines >=2.23.0`). The server is already on
2.23, so the command passes the flag to install `1.0.7` and close the
desync window. Version-progression (downgrade/same-version) checks still
run.
- **call-recorder** and **last-contact** are intentionally left
unchanged (reverted): they don't define custom objects and don't
reference the system relation identifiers, so they aren't
breaking-change-prone and need no SDK bump.

## Breaking change constraints

- **people-data-labs** and **partners** reference system relation
identifiers that only exist on a `2.23` server, so both pin
`engines.twenty >=2.23.0`. Their `dockerhub-latest` integration leg is
red by design until a >=2.23 server image is published (same accepted
state as #22882); the `local` leg is green.

## Validation

- Regenerated the app lockfiles against the published `2.23.0-alpha.1`.
- `people-data-labs` typechecks cleanly against the real `2.23` SDK
types.
- CI: people-data-labs and partners green on `local`, red on
`dockerhub-latest` by design; server/SDK/all other checks green.
- Rebased onto latest `main`.
2026-07-21 15:40:14 +02:00