Commit Graph

13597 Commits

Author SHA1 Message Date
Priyanshu Bartwal 7babc049f5 [Twenty-Front]: Record board column drag and drop functionality (#22323)
Closes #22321 

- Used `@dnd-kit` library for core drag and drop logic.
- Tried to keep as much similar to #21304 as possible.


https://github.com/user-attachments/assets/bff100cf-d727-4281-a5fa-b010a373f189



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22323?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: Charles Bochet <charles@twenty.com>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
2026-07-10 13:47:59 +00:00
twenty-pr[bot] ab9e6f30b8 chore: bump version to 2.21.0 (#22820)
## 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/22820?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-10 15:46:26 +02:00
martmull 7f8a1da27a Fix Cloudflare rate limiting during last-contact backfill (#22811)
## Context

Upgrading `twenty-last-contact` on a production workspace failed with a
Cloudflare error 1015 ("You are being rate limited"). The
`backfill-last-contact` post-install function runs on every version
upgrade and fired 20 concurrent update mutations per batch with no pause
between batches, on top of paginated full-collection reads. On a
workspace with real email/calendar history that burst trips Cloudflare's
rate limit, and since the client SDK throws on any non-2xx response, a
single 429 killed the whole install/upgrade hook mid-backfill.

## Changes

- New `executeWithRetry` util: retries rate-limit (429 / Cloudflare
1015) and transient gateway/network errors (502/503/504, timeouts,
connection resets) with exponential backoff and jitter, capped at 5
attempts. Honors a `retry_after` hint when present in the response body.
Non-retryable errors still throw immediately.
- All backfill queries and mutations are wrapped with it.
- Update batch concurrency reduced from 20 to 10 to keep bursts under
the rate limit in the first place.
- Bumped app version to 1.1.1 with a changelog entry.

## Test

- Added unit tests for `executeWithRetry` (success passthrough,
retry-then-succeed, non-retryable passthrough, retry exhaustion,
`retry_after` handling).
- `yarn test:unit` (28 passed), `yarn typecheck`, `yarn lint` all green
in the app package.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01AtnkEfbpFhLp5qCJbSmZpE)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22811?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-10 15:26:07 +02:00
github-actions[bot] 7861ada589 i18n - translations (#22815)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22815?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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 15:23:27 +02:00
github-actions[bot] 5f70beb2d2 i18n - docs translations (#22816)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22816?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-10 15:18:53 +02:00
Charles Bochet 4fc3650b2d fix(emails): deterministic i18n catalog order to stop recurring i18n PR conflicts (#22803)
## Problem

The automated `i18n - translations` PR (branch `i18n`) conflicts on
`twenty-emails` `.po` files on **every** cycle. Each time, the msgid
*set* is identical to main — only the entry **order** differs.

## Root cause

`twenty-emails` uses explicit message ids (`js-lingui-explicit-id`).
With lingui's default ordering, **`lingui extract` is non-idempotent for
this catalog** — two consecutive runs on identical source produce
different `.po` orderings:

```
# no source change between runs
lingui extract  # run 1
lingui extract  # run 2  -> ~170 lines reordered vs run 1
```

So the order produced by the `i18n-push` extract on main, the order
stored in Crowdin, and the order the `i18n-pull` bot downloads never
agree, and the translation PR re-conflicts perpetually.
`twenty-front`/`twenty-server` use hashed ids and are already idempotent
— this is isolated to emails.

## Fix

Set `orderBy: 'messageId'` in `twenty-emails/lingui.config.ts`. Verified
this makes extraction idempotent — two consecutive extracts now produce
byte-identical output.

This commit includes the one-time reorder of the existing catalogs into
the stable order. Generated `.ts` output is unchanged (already
order-independent). After merge, one push cycle syncs Crowdin to the
stable order, after which the recurring conflicts stop.

## Test plan

- `nx run twenty-emails:lingui:extract` twice → no diff on the second
run.
- `nx run twenty-emails:lingui:compile` → succeeds, generated `.ts`
unchanged.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22803?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-10 15:16:33 +02:00
martmull 945724e016 accepts any valid expression, not just an identifier (#22778)
Fixing and issue with front component definition

look at the unit test to see the actual fix
2026-07-10 13:15:26 +00:00
Weiko 666ceb41d5 Fix SDK plan on non installed apps (#22805)
# Context

`yarn twenty plan` fails when the app has never been installed in the
target workspace:

```
Sync failed with error: Application "f5ce204f-..." is not installed in workspace "10f39a9d-...". Install it first.
Hint: run `yarn twenty dev --once` to register the app in this workspace, then retry.
```

This forces developers to apply before they can plan, which defeats the
purpose of `plan`. Planning a not-yet-installed app is well defined: the
from-state is empty, so the plan is simply "create everything".

## Why it failed

The dry-run sync required the application row to exist in two places:

1. `ApplicationSyncService.synchronizeFromManifest` threw
`APP_NOT_INSTALLED` when the app row was missing, because the dry-run
needs an owner `FlatApplication` to anchor the from → to metadata diff.
2.
`WorkspaceMigrationFlatEntityMapsService.computeAllInvolvedApplicationIds`
threw when the owner app id was absent from `flatApplicationMaps`, even
though the only hard dependency of a build is the twenty standard
application.

`apply` never hit this because it registers the app row as a side effect
before syncing (and even swallows this exact error on its pre-apply
plan).

# What this PR does

Keeps `plan` strictly read-only, no registration or app row is created:

- **`application-sync.service.ts`**: on dry-run, resolve the owner to
the installed application when it exists (unchanged behavior), otherwise
build a virtual, non-persisted `FlatApplication` from the manifest. Its
freshly generated id matches no existing metadata, so the from-state
slice resolves to empty and every manifest entity shows up as a create.
- **`workspace-migration-flat-entity-maps.service.ts`**: relax the guard
so only the twenty standard application is required. A missing owner app
just contributes an empty from-slice instead of throwing. Installed apps
take the exact same path as before (`applicationId` defined → identical
behavior).
2026-07-10 15:11:46 +02:00
Raphaël Bosi 60f5964c64 Run front components in a sandboxed opaque-origin iframe (#22588)
Front components run untrusted third-party React in a Web Worker. That
worker previously shared the host origin, so it could reach
origin-scoped storage (the metadata-store IndexedDB, the
`twenty-sign-out` BroadcastChannel), cookies, and same-origin resources.

This runs the worker inside a `sandbox="allow-scripts"` (no
`allow-same-origin`) iframe, giving it an opaque origin where the
browser denies localStorage, cookies, IndexedDB, and BroadcastChannel
outright. The worker is kept inside the iframe (rather than a bare
iframe) so untrusted code always runs off the main thread; the
remote-dom render path is unchanged.

- **Transport:** host ↔ iframe ↔ worker over a re-transferred
`MessagePort` (`ThreadMessagePort`); a small bootstrap script is inlined
into the iframe via `srcdoc` (bundled at build time by a prebuild step)
and relays the port to the worker it spawns. Messages across the
boundary use a typed discriminated union with a single parse/guard.
- **Network:** under the opaque origin, direct fetches to the Twenty API
would be `Origin: null`, so the component source and SDK modules are
fetched through an allowlisted, credential-omitting `hostFetch` bridge
and blobbed inside the worker. The allowlist is single-sourced on the
host (http(s) origins only) and carried in the render context. The
bridge is mandatory (rendering fails closed if it is missing), refuses
redirects except for GET/HEAD to the known file-storage URLs, and caps
response body size.
- **SDK loading:** SDK client modules now load inside the worker through
the bridge, replacing the host-side SDK-blob state/effect/provider with
a pure `getSdkClientUrls` URL builder.
- **Isolation tests:** a unit test locks the sandbox attribute
(`allow-scripts`, never `allow-same-origin`); a browser test asserts the
worker actually gets an opaque origin with storage denied, probing
cookies by writing one rather than reading an empty jar.

Also adds a "List Companies" seed front component that queries workspace
data via the SDK client (exercising the bridge end-to-end),
single-sources the command-menu confirmation-modal result event name and
detail type in `twenty-shared` (previously a hand-synced duplicate), and
decomposes the renderer (bridge, sandbox, worker orchestration) into
small single-purpose utils with unit tests.

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant Host as Host window (twenty-front · host origin)
    participant Frame as Sandboxed iframe (allow-scripts · opaque origin)
    participant Worker as Worker (untrusted component · opaque origin)
    participant API as Twenty API (host origin)

    rect rgb(238,242,248)
    Note over Host,Worker: 1 — Boot handshake
    Host->>Frame: create iframe sandbox="allow-scripts", srcdoc = inlined bootstrap script
    Host->>Host: MessageChannel + ThreadMessagePort(port1)<br/>exports = host API + hostFetch
    Frame-->>Host: READY
    Host->>Frame: INIT + transfer port2
    Frame->>Worker: spawn inlined Worker + re-transfer port2
    Worker->>Worker: ThreadMessagePort(port)<br/>exports = render / updateContext
    Note over Host,Worker: Port now entangles Host ↔ Worker directly
    end

    rect rgb(246,240,248)
    Note over Host,Worker: 2 — Render
    Host->>Worker: render(connection, { componentUrl, sdkClientUrls, hostFetchOrigins, token })
    Worker->>Worker: override globalThis.fetch<br/>(Twenty origins → hostFetch)
    end

    rect rgb(248,244,238)
    Note over Worker,API: 3 — Network via hostFetch bridge (opaque Origin:null cannot reach the API directly)
    Worker->>Host: hostFetch(componentUrl, Bearer)
    Host->>Host: origin allowlist + credentials:'omit'
    Host->>API: fetch(componentUrl)
    API-->>Host: source
    Host-->>Worker: { status, headers, body }
    Worker->>Host: hostFetch(sdkClientUrls.core / .metadata)
    Host-->>Worker: SDK module sources
    Worker->>Worker: blob each source in its own opaque origin → import() → run untrusted React
    end

    rect rgb(238,248,242)
    Note over Worker,Host: 4 — Render mirror
    Worker->>Host: remote-dom mutations (RemoteConnection)
    Host->>Host: RemoteReceiver → RemoteRootRenderer → host DOM
    end

    Note over Worker: Opaque origin ⇒ browser denies localStorage,<br/>cookies, IndexedDB, BroadcastChannel
```
2026-07-10 13:10:30 +00:00
github-actions[bot] 95b672e61e i18n - translations (#22814)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22814?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-10 15:09:01 +02:00
github-actions[bot] caed92b1d4 i18n - translations (#22813)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 15:02:17 +02:00
Marie f667ba500c fix(front): clean stale morph relations from metadata store on object deletion (#22681)
## Problem

After deleting a custom object (e.g. `meeting`), the app crashes with
"Sorry, something went wrong" on pages that load records referencing
that object through a morph relation. The console shows:

```
Target object metadata item not found for target (morph target meeting)
```

It reproduces on the machine that used the object before deletion but
not on a fresh machine, which points at a stale client metadata store
rather than a server issue.

## Root cause

Every field carries its own server-provided `morphRelations` array; a
morph relation field (note/task/timeline targets, etc.) lists every
object it can point to, including the deleted one. When an object is
deleted, `useDeleteOneObjectMetadataItem` and the SSE `delete` handler
only remove the deleted **object** and its own fields from the metadata
store. The sibling morph fields on other objects keep their now-dangling
`morphRelations` entry pointing at the deleted object.

Those stale entries were only meant to be cleaned up later by a
collection-hash-triggered `network-only` refetch. When that
reconciliation does not win, `generateDepthRecordGqlFieldsFromFields`
can't resolve the deleted morph target in `objectMetadataItems` and
throws, crashing the page.

## Fix

Clean `morphRelations` entries referencing the deleted object from the
field metadata store at deletion time, so the store stays
self-consistent immediately instead of relying on an async refetch.
Applied in both paths that handle object deletion:

- `useDeleteOneObjectMetadataItem` (the client performing the deletion)
- `MetadataStoreSSEEffect` delete handler (other tabs/clients receiving
the event)

The throw in `generateDepthRecordGqlFieldsFromFields` is intentionally
left in place so any genuine future metadata inconsistency still
surfaces rather than being silently swallowed.

## Test

Added a unit test for the cleaning util covering: morph relations
targeting the deleted object are removed, only changed fields are
returned, non-morph fields are untouched, and nothing is returned when
no relation targets the deleted object.
2026-07-10 14:55:25 +02:00
github-actions[bot] 797acff5fa i18n - translations (#22812)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 14:54:18 +02:00
Thomas Trompette c1b62334b7 fix(workflow): scope one-active-per-workflow index to workspace (#22795)
## Problem

The Phase 0 core index \`IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW\`
is on \`(workflowId) WHERE status='ACTIVE'\`, with **no
\`workspaceId\`**. But \`core.workflowVersion\` is a shared multi-tenant
table, so this enforces "one active version per workflowId **globally
across all workspaces**" instead of per workspace.

The version backfill fails on staging with:
\`\`\`
duplicate key value violates unique constraint
"IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW"
Detail: Key ("workflowId")=(8b213cac-...) already exists.
\`\`\`
across several different workspaces that share the same workflowId
(seeded/cloned data): workspace A's active version claims the
workflowId, and every other workspace's insert collides. Every other
index on this table includes \`workspaceId\`; this one dropped it when
copied from the per-tenant workspace entity.

## Fix

Index becomes \`(workspaceId, workflowId) WHERE status='ACTIVE'\` — one
active version per workflow **per workspace**, matching the table's
multi-tenant design and the intended invariant. New 2-20 fast instance
command drops and recreates the index (Phase 0's command is
merged/append-only).

## Test

Reset + reproduce the exact scenario against the fixed index:
- two workspaces with the same workflowId, both ACTIVE → **insert
succeeds** (previously collided)
- a second ACTIVE version for the same workflow within one workspace →
**still blocked** (invariant preserved)

Zero \`migrate:generate\` drift, typecheck + lint clean. After this
deploys, re-run \`upgrade:2-20:backfill-workflow-version-to-core\`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22795?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-10 12:48:33 +00:00
github-actions[bot] dcb67fbbe1 i18n - translations (#22810)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 14:46:54 +02:00
Rashad Karanouh bf1220883f docs: partner CTAs on high-traffic pages (workflows, data-model, docker-compose) (#22808)
## Summary

Follow-up to #22719 (merged), which added partner-marketplace CTAs to
four **high-intent, low-traffic** docs pages (SSO, both migration
guides, implementation services).

Reviewing the docs' **top-visited pages** showed none of those four rank
in the top ~20 — they're the high-intent tail, which is correct, but
small reach. This PR extends the same pattern to three **high-traffic
pages that also carry buying intent**, without touching the pure
top-of-funnel intros/quickstarts (volume without intent → a CTA there is
just noise).

Same conventions as #22719: Mintlify-native `<Tip>` callouts,
partner-first with `contact@twenty.com` secondary, directory deep-linked
via `?categories=<scope>` and tagged with `?ref=docs-*`. No new snippet;
no `docs.json`, navigation, or translation (`l/`) changes.

## Pages changed — screenshots (one per page)

> Preview locally with `npx mintlify dev` from `packages/twenty-docs`,
or use the Mintlify PR preview once it posts. Paths below are under
`docs.twenty.com`.

### 1. `/user-guide/workflows/overview` (~593 views)
New `## Need Help?` two-bullet `<Tip>` → **Done for you** (Solutioning
partner) / **Onboarding pack** (Workflow Creation). Maps 1:1 to the
named onboarding service.

_screenshot:_
<img width="1440" height="818" alt="Screenshot 2026-07-10 at 14 34 32"
src="https://github.com/user-attachments/assets/231f681d-b5be-4b1e-9b6a-a4947a9fca37"
/>


### 2. `/user-guide/data-model/overview` (~954 views)
Replaced the plain "Need Help?" line with a two-bullet `<Tip>` → **Done
for you** (Solutioning partner) / **Onboarding pack** (Data Model
Design). Keeps the existing Implementation Services link.

_screenshot:_
<img width="1436" height="817" alt="Screenshot 2026-07-10 at 14 34 14"
src="https://github.com/user-attachments/assets/c0fe1064-1030-4062-91c7-24644ac31654"
/>


### 3. `/developers/self-host/capabilities/docker-compose` (~2421 views)
New `## Managed Hosting` single-line `<Tip>` → *find a certified Twenty
hosting partner* (Hosting), contact fallback. Framed as a lighter
"prefer not to run it yourself?" alternative — deliberately low-pressure
for the DIY self-host audience.

_screenshot:_
<img width="1437" height="815" alt="Screenshot 2026-07-10 at 14 33 39"
src="https://github.com/user-attachments/assets/a37207cd-2aaa-4aba-848d-cbf06a1e1321"
/>

## Notes for reviewers

- Page-selection rationale: intent × volume. Kept the four intent-tail
pages from #22719; added the highest-traffic pages that also carry a
natural partner-buying moment (self-host → Hosting; workflows /
data-model → Solutioning). Intros/quickstarts/contribute pages
intentionally left untouched.
- **Attribution caveat (unchanged from #22719):** twenty.com's analytics
(Cloudflare Web Analytics) is path-based, so `?ref=` is not measurable
yet. Per-page measurement via a `/go/*` redirect Worker remains a
planned, separate follow-up (out of scope here).
- `mintlify validate` passes.

Opened as a draft.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22808?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-10 14:39:31 +02:00
github-actions[bot] 55768cfe81 i18n - translations (#22809)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22809?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-10 14:36:06 +02:00
github-actions[bot] e9dc4d8e89 i18n - translations (#22807)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22807?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-10 14:29:33 +02:00
github-actions[bot] 07c88aed45 i18n - translations (#22806)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22806?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-10 14:22:05 +02:00
github-actions[bot] 39847c8574 i18n - translations (#22804)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 14:14:54 +02:00
github-actions[bot] a8a7356a42 i18n - translations (#22802)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22802?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-10 14:07:53 +02:00
github-actions[bot] 75ea4fb027 i18n - translations (#22799)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22799?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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 14:00:49 +02:00
Abdul Rahman ffdda50afc remove nestjs-query auto-resolver from index-metadata (#22775)
## Summary

Removes the `NestjsQueryGraphQLModule.forFeature` block from
`IndexMetadataModule`, continuing the incremental migration off
`@ptc-org/nestjs-query`.

- Drops the dead auto-generated read surface: `index` and
`indexMetadatas` queries, the `IndexConnection` /
`IndexObjectMetadataConnection` types, and the `Index.objectMetadata`
field. No client consumes these — the frontend reads indexes via
`ObjectMetadata.indexMetadatas`.
- Keeps `IndexMetadataDTO` nestjs-query-compatible (`@Authorize`,
`@FilterableField`, `@QueryOptions`, `@IDField`) because
`ObjectMetadataDTO` still references it via
`@CursorConnection('indexMetadatas')` until object-metadata is migrated.
- Hand-written `createOneIndex` / `deleteOneIndex` mutations and the
`indexFieldMetadataList` resolve-field are unchanged.
- Deletes the now-obsolete `index-metadatas` integration test and
regenerates the GraphQL schema artifacts (frontend + client-sdk).

## Breaking change

This is an intentional GraphQL schema breaking change
(`api-breaking-changes` CI will flag it)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22775?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-10 14:00:23 +02:00
github-actions[bot] 04c195a54e i18n - translations (#22798)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22798?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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 13:44:11 +02:00
Paul Rastoin 108e1654e7 Catch Cursor's "Made with" attribution footer in blocked contributors check (#22791)
## Summary

PR #22785 merged with a Cursor attribution (05afc49e) even though the
`check-blocked-contributors` job ran and passed. The PR's commits were
clean, but the description ended with Cursor's attribution footer, which
starts with "Made with" followed by a markdown link to cursor.com, while
the script only matched the "Generated with" wording. Since the repo
squash-merges, the PR description became the merged commit message,
carrying the footer (plus the co-author trailer GitHub appends at squash
time) into main's history.

This broadens the signature pattern in
`scripts/check-blocked-contributors.ts` to also cover the "Made"/"Built"
wordings.

Note: this description intentionally avoids quoting the banned strings
verbatim, since the check scans it too (and it would become the squash
commit message).

## Test plan

Verified the new pattern against:
- The exact footer from the PR #22785 description - now caught
- The co-author trailer from the merged squash commit - caught by
existing patterns
- The "Generated with" and "Built with" wordings, with and without the
"Agent" suffix in the link label - caught
- Negative cases: prose mentioning the word cursor and a plain markdown
link to cursor.com - not flagged
2026-07-10 11:42:59 +00:00
github-actions[bot] 27e101599a i18n - translations (#22796)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 13:30:12 +02:00
Abdul Rahman d23511ea5e remove nestjs-query from user and workspace resolvers (#22766)
## What

Migrates the `user` and `workspace` core modules off
`@ptc-org/nestjs-query`. Both used `NestjsQueryGraphQLModule` only as
scaffolding — all CRUD was disabled and the real GraphQL API is already
served by the hand-written `UserResolver` / `WorkspaceResolver`.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22766?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: Weiko <corentin@twenty.com>
2026-07-10 16:59:58 +05:30
Paul Rastoin ace16add6c fix(twenty-partners): align createdAt view field with 2.19 deterministic ids and bump to 1.2.10 (#22782)
## What

- Bump `twenty-partners` from `1.2.0` to `1.2.10`.
- Fix the red integration tests by pointing the "Partner Applications"
view `createdAt` column at the real field metadata id.

## Why the integration tests were red

The `twenty-partners` CI job spins up `twentycrm/twenty-app-dev:latest`
and runs the integration suite. The suite's global setup does a dev sync
of the app, which failed:

```
Dev sync failed: viewField: INVALID_VIEW_DATA: Field metadata not found
(universalIdentifier: 835c9a7e-72ec-46c5-8d90-39a02998f561)
```

The `partner-applications.view.ts` `createdAt` column referenced
`PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = 421cbcea-...`, an
invented id. `createdAt` is a reserved system field auto-created on the
custom `partner` object, and since 2.19 its universal identifier is
derived deterministically by the server from the application id, the
object id and the field name. The invented id matched nothing, so the
sync rejected the dangling view field and the app never registered,
failing every integration test.

## Fix

Set `PARTNER_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER` to the
deterministically derived value `746e2944-28d0-545e-9832-a46516e1d9a0`
(application id `e662fc1f-...` + partner object id `39101b39-...` +
field name `createdAt`). This matches the same derivation the server
uses for standard object system fields, verified against the
`twenty-shared` opportunity `createdAt` snapshot.
2026-07-10 13:27:21 +02:00
github-actions[bot] 48252d35e9 i18n - docs translations (#22794)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22794?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-10 13:26:42 +02:00
github-actions[bot] 5499688861 i18n - translations (#22793)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22793?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-10 13:23:31 +02:00
Thomas Trompette b682162f31 fix(front): render +N button for right-edge-clipped chips in expandable list (#22748)
## Problem

Fixes #22383. Overflowing relation & multi-select chip cells didn't show
the "+N" overflow button, so hidden records/values were unreachable.
There were several distinct causes behind this, addressed below.

Before
<img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 43
13"
src="https://github.com/user-attachments/assets/1001d47b-0f54-4fbd-a5cb-83a5c32c35ac"
/>

After
<img width="263" height="34" alt="Capture d’écran 2026-07-10 à 11 41
54"
src="https://github.com/user-attachments/assets/d4f5cf2f-fda4-422e-b72d-2df3fc81a06f"
/>

## Changes

**1. Overflow detection missed right-edge-clipped chips**
`isFirstOverflowingChildElement` used `childElement.offsetLeft >
containerElement.clientWidth` (left edge past the container), which is
false when a chip is only right-edge clipped. Switched to a right-edge
check: `childElement.offsetLeft + childElement.offsetWidth >
containerElement.clientWidth`.

**2. Multi-select never used the overflow list when unfocused**
`MultiSelectFieldDisplay` rendered a plain clipped `MultiSelectDisplay`
when not focused, and only used `ExpandableList` on focus. That
non-focused fallback painted over the focused "+N" and hid it. It now
always renders through `ExpandableList` with
`isChipCountDisplayed={isFocused}`, matching
`RelationFromManyFieldDisplay`, so the count shows on focus only (not
idle).

**3. The hover portal was never actually focused**
`FieldFocusContextProvider` silently ignored its `isFocused` prop (`({
children }: any)` + hard-coded `useState(false)`), so
`RecordInlineCellAnchoredPortal`'s `<FieldFocusContextProvider
isFocused={true}>` had no effect and the hovered cell's display always
saw `isFocused=false`. That's why "+N" never appeared on hover for any
multi-value field. The portal now uses the existing
`FieldFocusStaticFocusedProvider`, fulfilling the intent — so relations,
multi-select, emails and phones all surface their "+N" on hover.

**4. "+N" was hard to read on multi-select**
The "+N" chip has a transparent background (shared component), and the
hover portal let the base layer's colored option chips bleed through it.
Gave the hover portal content an opaque `background.primary` so nothing
bleeds through; the "+N" component itself is untouched, so
relations/emails/phones keep their existing look.

## Test plan

- Hover a relation or multi-select field whose values overflow the cell:
the "+N" button appears (and is readable), and clicking it lists the
hidden records. When not hovered, no "+N" shows.
- Verify fully-overflowing rows still show the correct "+N" count.
2026-07-10 13:22:03 +02:00
github-actions[bot] f58683e33c i18n - translations (#22789)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-10 13:16:47 +02:00
github-actions[bot] c0b404b3d1 i18n - translations (#22788)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22788?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-10 13:09:39 +02:00
github-actions[bot] a487ed043b i18n - translations (#22787)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22787?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-10 13:01:38 +02:00
github-actions[bot] 6baa5a6257 i18n - translations (#22784)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22784?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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 12:54:58 +02:00
Paul Rastoin 05afc49ea6 Fix front-component cache read rejection bypassing network fallback (#22785)
## Summary

Quick follow-up to #22672.

`frontComponentCacheStorageService.read` returned
`cachedResponse.text()` without awaiting it, so the promise escaped the
surrounding `try/catch`. A cache entry with an unreadable body (corrupt
or partially-evicted `CacheStorage` entry) would reject in
`fetchComponentSource` and break component rendering entirely, instead
of being treated as a cache miss with a network fallback.

- `await` the body read inside the `try/catch` so decoding failures
degrade to a network fetch
- Add a regression test: cached body read rejects → source is still
served from the network

## Test plan

- `fetchComponentSource.spec.ts` — new test `falls back to the network
when the cached response body is unreadable`; full renderer suite passes
(14 tests)

Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22785?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: Cursor <cursoragent@cursor.com>
2026-07-10 10:50:29 +00:00
Abdul Rahman 4ab36a630b remove nestjs-query from app-token and drop unused createOneAppToken mutation (#22765)
## What

Migrates `app-token` off `@ptc-org/nestjs-query` and removes the
auto-generated `createOneAppToken` mutation, which was unused dead API
surface.

## Why

The `createOneAppToken` mutation was reachable only from the schema — no
frontend query, SDK caller, or test used it. It was also non-functional
(its input couldn't set the token `value`), a leftover from
nestjs-query's default `create.one` being left enabled. Real app tokens
(refresh, password-reset, email-verification, invitation, OAuth,
enterprise) are all created directly via the repository in ~14 services,
none of which touched this mutation.

## Notes

- ⚠️ This removes `AppToken`, `createOneAppToken`,
`CreateAppTokenInput`, and `CreateOneAppTokenInput` from the `/metadata`
schema, so the **api-breaking-changes check will flag it**


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22765?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: Weiko <corentin@twenty.com>
2026-07-10 16:14:13 +05:30
Paul Rastoin 9e20e2222a Fix front-component serving on Safari, kill stale presigned caching, and cache built bundles client-side (#22672)
## Context

Built front-component bundles are served via `GET
/rest/front-components/:id/:cacheKey`. On S3-backed storage (Twenty
Cloud) the endpoint used to 302-redirect the worker's authenticated
fetch to a presigned S3 URL. That redirect caused two bugs, and fixing
it removed the caching the redirect was accidentally providing — so this
PR also adds a proper client-side cache.

Closes twentyhq/core-team-issues#2653.

### Bug 1 — Safari 403 (Authorization header forwarded across redirect)

The renderer worker fetches the bundle with `Authorization: Bearer`. The
controller answered with a 302 to a presigned S3 URL. Per the Fetch
spec, browsers must strip `Authorization` on a cross-origin redirect.
Chrome/Firefox do, but Safari/WebKit forwards it, so S3 receives both a
query-string signature and an `Authorization` header and rejects with
`InvalidArgument: Only one auth mechanism allowed`. Result: front
components never load in Safari on S3-backed storage.

### Bug 2 — 302 cached publicly (browser-independent)

The redirect branch set no `Cache-Control`, so a CDN could cache it far
beyond the presigned URL's TTL (`STORAGE_S3_PRESIGNED_URL_EXPIRES_IN`,
900s). Consequences: any client re-served the cached 302 after 15 min
hits an expired signature (403, also affects Chrome), and the cached
redirect containing a live presigned URL is served to unauthenticated
requests (short-lived auth bypass).

### Regression this introduces — warm-load caching lost

Marking the handoff `no-store` (Bug 2 fix) is correct, but it means the
built bundle is no longer cached anywhere on the S3 path. The browser
HTTP cache cannot compensate: the presigned URL that actually returns
the bytes carries a fresh `X-Amz-Date`/`X-Amz-Signature` on every
request, so each download is a brand-new cache key and never hits. Net
effect without mitigation: every worker mount re-downloads the full
bundle.

## What changed

- **Front components return a 200 JSON body instead of a 302.** The
controller now responds `200 { url }` with `Cache-Control: private,
no-store`. The worker parses the JSON and issues a separate header-less
`fetch(url)` to S3. No redirect means the `Authorization` header is
never forwarded, making it browser-independent, and the handoff carrying
the presigned URL is never cached. The stream path (local storage) is
unchanged.
- **Client-side bundle cache in the renderer (restores warm loads).**
`fetchComponentSource` wraps the fetch chain in a `CacheStorage` layer
keyed by the **content-addressed** `/front-components/:id/:checksum.js`
URL. A hit returns the stored bundle and skips **both** the `no-store`
handoff to Twenty and the S3 download — restoring cross-session warm
loads without ever persisting a presigned credential. Because
`CacheStorage` is writable by any same-origin code (including the
untrusted component code this cache feeds), cached content is verified
against the sha-256 checksum embedded in the URL on every read, and
evicted on mismatch. Caching degrades to a plain fetch where
`CacheStorage` or WebCrypto is unavailable.
- **sha-256 checksums for built front components.** The SDK build and
workspace prefill now fingerprint built front-component bundles with
sha-256 (WebCrypto has no md5), enabling the integrity check above.
Other file folders keep md5. Legacy md5-fingerprinted URLs (32-hex)
simply bypass the cache — already-synced components keep working and
start benefiting from caching on their next build/sync.
- **WebKit e2e coverage.** Added a `webkit` project to the postcard
example's Playwright config mirroring `chrome` (shared setup +
storageState), plus iframe/worker diagnostics logging so front-component
failures surface in the test log. `TZ` is pinned to `Europe/Paris`
because WebKit on Linux ignores Playwright's `timezoneId` emulation and
rejects the runner's legacy `CET` alias, which crashed the record page
before the component could render.

### Why we hand off to S3 instead of streaming through Twenty

On S3-backed storage we deliberately **do not** proxy/stream the bundle
bytes through the API. The controller returns the presigned URL and the
worker fetches the content directly from S3, for two reasons:

- **Server CPU/bandwidth.** Streaming every bundle on every cold load
would put the API server on the hot path for all front-component
content. Handing off to S3 keeps that load off the server.
- **Domain isolation.** Front-component content is fetched from the
object-storage domain (e.g. `s3.domain.com`), a different origin than
the API and the front app. Serving untrusted/app-authored bundle content
from a separate domain than `twenty.com` keeps it off the app's origin.

The stream path is kept only as the local-storage fallback (no
S3/presign available), where these concerns don't apply.

## Examples

### The JSON handoff (S3 path)

```http
GET /rest/front-components/d3b07384-.../a1b2c3d4.js HTTP/1.1
Host: twenty.com
Authorization: Bearer <worker-token>
```

```http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store

{"url":"https://s3.domain.com/bucket/.../checkout-widget.mjs?X-Amz-Date=20260709T091500Z&X-Amz-Expires=900&...&X-Amz-Signature=AAAA1111..."}
```

The worker then fetches that presigned URL **without** headers (the
Safari fix) and gets the bundle bytes.

### Why the browser HTTP cache can't reuse it

| | Load 1 (09:15) | Load 2 (09:30) | Same key? |
|---|---|---|---|
| Twenty handoff URL | `.../a1b2c3d4.js` | `.../a1b2c3d4.js` |  but
response is `no-store` |
| Presigned `X-Amz-Signature` | `AAAA1111...` | `ZZZZ9999...` |  |
| Effective S3 URL (the HTTP cache key) |
`...&X-Amz-Signature=AAAA1111...` | `...&X-Amz-Signature=ZZZZ9999...` |
 new key → miss |

### What the CacheStorage layer stores

```
key   = https://twenty.com/rest/front-components/d3b07384-.../a1b2c3d4.js   (stable, chosen by us)
value = <bundle JS bytes>                                                   (NOT the presigned URL)
```

Keying by the stable logical URL (not the volatile URL the bytes arrived
from) is the one thing the native HTTP cache can't express. The
presigned URL is used once and discarded.

### Invalidation

No TTL and no explicit delete — invalidation is by key change. A rebuild
changes the checksum → changes the URL → guaranteed miss on the new key.
The old entry is orphaned and reclaimed by normal browser eviction
(quota/LRU; Safari ITP after 7 idle days). Global invalidation lever:
bump the cache name suffix (`front-component-source-v1`).

## Deploy note — front/server release window

Old frontend bundles (already-open tabs) hitting the new server receive
the JSON handoff where they expect raw JS and fail to render until the
tab is reloaded. The other direction is safe: the new worker against an
old server follows the 302 transparently (the content-type check falls
through to `response.text()`). Accepted as a short deploy-window
trade-off.

## Follow-ups (not in this PR)

- The client-side cache is a bridge for the `no-store` presigned
handoff. If built components are later served from a stable, non-signed,
public-by-URL path (they are already content-addressed by checksum, so
`immutable` is safe), the browser + CDN cache natively and this custom
layer can be removed.
- `GET /file/:fileFolder/:id` presigned 302s still carry no
`Cache-Control`. An explicit policy there (bounded `private, max-age`
below the presigned TTL) was prototyped in this PR and deliberately
dropped to keep the scope on front components — the file path
authenticates via a query-param token (part of any cache key), so its
exposure differs and deserves its own PR.

## Non-goals

Per the issue, file serving keeps its query-param token + 302 model.
Native browser loads (`<img>`, downloads) cannot do a two-step fetch and
already work on Safari. The public-asset redirect is left untouched
since its caching is intentional.

## Test plan

- Renderer: `fetchComponentSource.spec.ts` covers cache miss + write,
verified cache hit (no network), poisoned-entry eviction,
checksum-mismatch (never cached), non-fingerprinted and legacy-md5 URL
bypass, and the no-`CacheStorage` / no-WebCrypto fallbacks.
`fetchComponentSourceFromNetwork.spec.ts` covers the direct JS response,
the JSON handoff follow-through (header-less presigned fetch), and error
mapping.
- e2e: the postcard front-component spec now runs on both Chromium and
WebKit against prod-parity storage (S3 + Lambda).
- `oxlint` + `oxfmt` clean; typecheck passes on changed packages.

### Reproduction proof — Safari was always broken (e2e probe)

We ran the prod-parity postcard e2e suite (S3 storage + Lambda) with
WebKit against **`main` without this fix**, via a throwaway probe PR:
twentyhq/twenty#22717.

Result — [ci-privileged run
29015624468](https://github.com/twentyhq/ci-privileged/actions/runs/29015624468):

```
1 failed
  [webkit] › card-front-component.spec.ts:61 › renders the postcard name and status badge in the record preview
2 passed (1.4m)
```

`[webkit]` times out waiting for `getByTestId('postcard-card')` to
become visible (*element(s) not found*) while the Chromium run of the
same spec passes. This confirms the front component **never rendered in
Safari** on S3-backed storage prior to this PR — it is a genuine,
browser-specific bug, not a flake. The fix in this PR is expected to
turn that same `[webkit]` assertion green.

Note: running the WebKit tests in CI requires the WebKit browser binary
and its system dependencies in the e2e job (now installed via `npx
playwright install --with-deps chromium webkit`).
2026-07-10 10:12:58 +00:00
github-actions[bot] d87e762bf9 i18n - translations (#22781)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22781?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-10 12:11:58 +02:00
martmull 1c8b8970fd Allow CLI dev mode on catalog-synced apps without mutating the shared registration (#22756) 2026-07-10 12:10:00 +02:00
github-actions[bot] f8d3555fe7 i18n - translations (#22779)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22779?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-10 12:05:07 +02:00
github-actions[bot] 09496a0f98 i18n - translations (#22745)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22745?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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-07-10 11:56:49 +02:00
github-actions[bot] bd349b1544 i18n - docs translations (#22777)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22777?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-10 11:41:55 +02:00
neo773 f547da4ee9 Gate connected-account webhook subscriptions behind config (#22761)
Now gated behind IS_CONNECTED_ACCOUNT_WEBHOOK_SUBSCRIPTION_ENABLED
(default false).

Merge + deploy twentyhq/twenty-infra#780 first.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22761?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: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-07-10 15:03:53 +05:30
Abdul Rahman a360f9bdec migrate 10 modules off NestjsQueryTypeOrmModule wiring (#22763)
## Summary
Continues the incremental removal of `@ptc-org/nestjs-query`. Migrates
ten modules from `NestjsQueryTypeOrmModule.forFeature` to the standard
`TypeOrmModule.forFeature`. These modules only used `nestjs-query` for
repository registration — none register `NestjsQueryGraphQLModule` /
auto-generated
resolvers — so this is a pure module-wiring swap with no behavior or
schema change.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22763?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: Weiko <corentin@twenty.com>
2026-07-10 09:21:52 +00:00
martmull 23cae2040a Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.

This makes assets always bundled files:

- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.

Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.

Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-07-10 09:18:52 +00:00
neo773 4cce9b1544 Fix integration test hang on unmocked requests + de-flake object metadata suite (#22758) 2026-07-10 14:27:25 +05:30
Thomas Trompette b327ab09a7 feat(workflow): add universalIdentifier + applicationId to core workflowVersion (#22747)
## workflowVersion core: syncable columns

Adds `universalIdentifier` + `applicationId` (nullable) to
`core.workflowVersion`, plus the FK to `core.application` and the
`(workspaceId, universalIdentifier)` unique index, via a 2.20
add-columns fast command gated with `@WasIntroducedInUpgrade`.

Nullable for now: the already-merged Phase A backfill (#22663) inserts
version rows without these columns, so `applicationId` can't be NOT NULL
yet. Flipping to NOT NULL + `extends SyncableEntity` comes once they're
populated (backfill + dual-write follow-ups).

Schema captured and verified via `migrate:generate` (zero drift).

Independent of the core-workflow PR, but both add 2.20 upgrade commands,
so this one (ts `…480`) must merge **after** the core-workflow PR (ts
`…479`), or it gets re-timestamped on rebase.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22747?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-10 10:26:52 +02:00
martmull 0786f9e793 Seed CHANGELOG.md and SETUP.md in create-twenty-app scaffold (#22769)
Projects scaffolded with `create-twenty-app` now include two additional
seed files:

- `CHANGELOG.md` with an initial `0.1.0` entry matching the template's
package version
- `SETUP.md` with step-by-step local setup instructions (prerequisites,
install, local server, dev sync, verification commands)

Both files live in `src/constants/template/`, so they flow through the
existing `fs.copy` scaffolding and the vite `copy-assets` build step
with no code changes. Verified `dist/constants/template/` contains both
files after `nx build create-twenty-app`.

The scaffolded `README.md` was also simplified into a marketable front
page for the app being built: a pitch placeholder, a features section,
and links to `SETUP.md` for setup instructions and `CHANGELOG.md` for
history, instead of duplicating dev commands.

Also:

- Adds a regression test asserting the template directory contains both
seed files
- Updates `project-structure.mdx` docs to list the new files in the
scaffold directory tree

---------

Co-authored-by: Martin <martin@twenty.com>
2026-07-10 10:02:43 +02:00
martmull 1fe6156cf5 fix(twenty-sdk): restore require/__filename/__dirname in rolldown ESM CLI bundle (#22711)
## Problem

Fixes #22708.

`twenty-sdk` 2.19.0 CLI commands run through the ESM entrypoint (`node
dist/cli.mjs dev --once`, `app:uninstall`, ...) crash on startup:

```
Error: Calling `require` for "fs" in an environment that doesn't expose the `require` function.
```

## Root cause

The 2.19.0 release switched bundling from esbuild to **rolldown** (Vite
7 → 8). Rolldown **inlines CommonJS dependencies** into the ESM output
(`dist/cli.mjs` grows from ~6k to ~136k lines). Those third-party CJS
modules — e.g. `typescript`, pulled in via `ts-morph` — call
`require(...)` and read `__filename` / `__dirname` at load time. None of
those exist in an ES module:

- `require(...)` is routed through rolldown's interop shim, which
**throws** when `require` is absent (i.e. in a `.mjs` file).
- `__filename` / `__dirname` are simply `ReferenceError: … is not
defined in ES module scope`.

esbuild (2.18.0) injected these CJS globals for node-targeted ESM
output; rolldown does not. Because the offending usage lives in
**bundled third-party CJS**, prefixing our own imports could not fix it.

## Fix

Add a banner to the **ESM output only** in `vite.config.node.ts` that
recreates the CJS globals from `import.meta.url`:

```js
import { createRequire as __twentyCreateRequire } from 'node:module';
import { fileURLToPath as __twentyFileURLToPath } from 'node:url';
import { dirname as __twentyDirname } from 'node:path';
const require = __twentyCreateRequire(import.meta.url);
const __filename = __twentyFileURLToPath(import.meta.url);
const __dirname = __twentyDirname(__filename);
```

This is the same `createRequire` pattern the repo already uses for the
`twenty-oxlint-rules` ESM build. The CJS output already provides all
three, so the banner is not applied there.

This PR also prefixes the SDK CLI's own Node-builtin imports with
`node:` (using native ESM imports instead of the interop shim for our
own code) — good hygiene and guarded by a unit test, but note the
**banner is the actual bug fix**.

## Verification (built and run locally)

- Built the node bundle and reproduced the crash on the pre-fix build
(`Calling require for "fs"`), then a follow-on `__filename is not
defined` once `require` was restored.
- With the banner, ran the previously-crashing commands against the
built `dist/cli.mjs`:
  - `--help` → prints usage, exit 0
- `dev --once` → reaches "Checking server… Cannot reach Twenty server"
(normal, no local server)
  - `app:uninstall` → reaches the interactive confirmation prompt
- CJS bin (`dist/cli.cjs`) still works.

## Tests

- `cli-esm-bundle-startup.integration.spec.ts` — runs the **built**
`dist/cli.mjs --help` and asserts no require/ESM-scope crash.
Demonstrated **red without the banner, green with it**. It runs in the
`sdk-test` job (which builds the SDK before tests); it fails loudly in
CI if the artifact is missing and skips locally when unbuilt, so it is
never silently green in CI.
- `node-builtin-import-protocol.test.ts` — guards the `node:`-prefix
hygiene across the CLI source.

Note: the existing `sdk-e2e-test` never caught this because it runs the
CLI via `tsx` on the TypeScript **source**, which has no rolldown shim —
only the bundled `.mjs` reproduces the crash.
2026-07-10 09:59:48 +02:00