f58683e33cba53cbc38ed17d52827aaaef00823a
13566 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f58683e33c |
i18n - translations (#22789)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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`). |
||
|
|
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> |
||
|
|
1c8b8970fd | Allow CLI dev mode on catalog-synced apps without mutating the shared registration (#22756) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
|
||
|
|
4cce9b1544 | Fix integration test hang on unmocked requests + de-flake object metadata suite (#22758) | ||
|
|
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. --> |
||
|
|
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> |
||
|
|
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. |
||
|
|
d430e38a9a |
Fix shard 12 integration test failure: regenerate stale object metadata snapshot from #22594 (#22768)
# Context Since #22594 merged (July 9, 16:59 CET), `server-integration-test (12)` fails deterministically on every main-based branch. The failure is easy to misread as flakiness because nx truncates the failed task's replayed output before jest's `FAIL`/summary lines reach the CI log; the visible `timelineActivity` FK-violation errors are pre-existing noise also present in green runs. Note: the flakiness PRs merged yesterday morning (#22699, #22701, #22702) are not the cause. The failure window starts with #22594. # Root cause #22594 moved `searchVector` provisioning out of the reserved-system-fields path into its own side-effect handler, registered after `ObjectSystemFieldsOnCreateSideEffectHandlerService` in `metadata-side-effect-handlers.module.ts`. The `searchVector` field entry therefore now appears after `updatedAt`/`updatedBy` in the create-object validation report. The snapshot for `failing-create-one-object-metadata-v2.integration-spec.ts` was rewritten in #22594 but kept the old `position -> searchVector -> updatedAt` ordering, so 15 of the 19 tests fail on a snapshot mismatch. Reproduced locally on latest main: 15/19 fail, diff is exactly the three reordered name lines. # Fix Regenerated the snapshot with `jest -u` against a freshly reset database. Suite is 19/19 green afterwards. The sibling snapshot suites in the same directory (`failing-update-one-object-metadata`, `failing-update-one-standard-object-metadata`, `successful-update-one-standard-object-metadata`) were re-run and pass unchanged. Pure block move: only the `searchVector`/`updatedAt`/`updatedBy` name lines relocate, 15 occurrences each (45+/45-), one file, no product code. # Related - #22757 is an earlier draft with the same snapshot regeneration. - #22758 fixes the same suite by replacing the snapshot with per-case contract assertions (plus an msw catch-all). If that one merges first, this PR becomes unnecessary. --- _Generated by [Claude Code](https://claude.ai/code/session_01NPSkHDBHNQw4c1iJzFTxoA)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22768?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: Martin <martin@twenty.com> |
||
|
|
25d7758049 |
chore: sync AI model catalog from models.dev (#22770)
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. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22770?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
a35aa6ce7f |
i18n - docs translations (#22764)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22764?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> |
||
|
|
09a48f56c5 |
i18n - docs translations (#22762)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22762?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> |
||
|
|
751f18ee9d |
i18n - docs translations (#22759)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22759?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> |
||
|
|
d8082d86ca |
Add Last contact on Companies and Opportunities to last-contact app (#22720)
## After Example with google company, 2 contacts, 2 opportunities: <img width="1512" height="332" alt="image" src="https://github.com/user-attachments/assets/9dac3ac1-bbbb-41a4-b6d6-5f2180e33c13"/> <img width="1512" height="313" alt="image" src="https://github.com/user-attachments/assets/e6e326db-942f-45c2-a696-05fa4c32619b"/> <img width="1309" height="313" alt="image" src="https://github.com/user-attachments/assets/64ed84ab-f7f0-47e5-b48c-42572c7d2534"/> ## What Extends the `twenty-last-contact` app so **Last contact** is also surfaced on **Companies** and **Opportunities**, not just People. Feedback: Companies and Opportunities already show emails and meetings from their related Person records on their timeline, so they should expose the most recent touch as fields too. Terminology and mechanism intentionally mirror what the app already does on People (no email-specific fields). ## Changes - **New fields**, identical in name/label/semantics to the People headline columns: - `Company.lastContactAt` and `Opportunity.lastContactAt` (datetime, "Last contact") - `Company.lastContactItemMessage` / `lastContactItemCalendarEvent` and the same pair on Opportunity (morph relation, "Last contact item"), with inverse `lastContactForCompanies` / `lastContactForOpportunities` relations on Message and Calendar event - All app fields are read-only in the UI (`isUIEditable: false`) - **View fields** on the All Companies and All Opportunities views (Last contact + Last contact item, visible). - **Live updates**: the new `updateRelatedLastContact` util propagates a person's interaction to their company and their point-of-contact opportunities, guarded so an older interaction never overwrites a newer one. It is called from both the email handler (`on-email-interaction`) and the shared calendar path (`updatePersonLastContactFromCalendar`, used by `on-calendar-interaction` and `on-calendar-event-started`), so emails and meetings both count. - **Backfill** (`backfill-last-contact`): aggregates each person's last contact up to their company, and each opportunity's from its point of contact. Related-record scope: a company's last contact comes from its people; an opportunity's from its point of contact. ## Not included (deliberately) The directional fields (`lastInboundAt`, `lastOutboundAt`, `lastContactBy`) and the `lastEmail`/`lastMeeting` shortcuts are not mirrored: aggregated across many people they get semantically fuzzy, and they would double the write amplification on every synced email for little added signal. ## Tests - Unit tests for `updateRelatedLastContact` (email and meeting propagation, recency guard, no-company case). - Integration tests: a related person's email sets company + opportunity last contact; a later meeting supersedes an email; an older interaction does not overwrite a newer one. - Existing unit + integration tests updated and passing; typecheck and lint clean. |
||
|
|
6210389221 |
feat(workflow): add core workflow entity (syncable) + create-table (#22746)
## Core `workflow` entity (syncable) Part of the app-workflows work. Adds a core `WorkflowEntity extends SyncableEntity` (`name`, `lastPublishedVersionId`, plus `universalIdentifier`/`applicationId`/workspace from the base class) and its 2.20 create-table fast command, gated with `@WasIntroducedInUpgrade`. Schema was captured and verified via `migrate:generate` (zero drift, FK/index hashes correct), then run against a live DB. Backfill (populate from workspace `workflow` records) and the dual-write listener land in follow-ups. Independent of the version-syncable-columns PR, but note: both add 2.20 upgrade commands, so this one (ts `…479`) must merge **before** the version PR (ts `…480`) to satisfy the append-only guard. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22746?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. --> |
||
|
|
9c405384f3 |
Only propose configured apps during onboarding install step (#22712)
## What - Onboarding "Install your first apps" now proposes only apps that are actually installable: it intersects the onboarding list with `findManyMarketplaceApps`, which the backend already filters to listed + configured apps (all required server variables set). - If none are available, the step auto-skips. If the marketplace query fails, it shows an intentional fallback (heading + Skip) instead of silently skipping or rendering an empty install card. - `findManyMarketplaceApps` now accepts `universalIdentifiers`, so onboarding fetches and configuration-checks only its own apps instead of the entire catalog. ## Why Previously the step rendered all hardcoded apps regardless of configuration, only borrowing logos from the marketplace, so a user could be offered an app the admin never configured. This centralizes onboarding availability on the marketplace's existing logic and keeps the query bounded as the marketplace grows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22712?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. --> |
||
|
|
b9cd62f396 |
i18n - docs translations (#22752)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22752?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> |
||
|
|
e9a030f762 |
fix(server): allow relabelling onto a field introduced in the same manifest sync (#22727)
## Context Closes twentyhq/core-team-issues#2655. `computeOrderedMigrationActions` runs `objectMetadata.update` **before** `fieldMetadata.create`. In a single manifest sync that both introduces a new field and relabels the object's `labelIdentifierFieldMetadataUniversalIdentifier` onto that field, the object update handler resolved the label identifier's universal identifier against the persisted `flatFieldMetadataMaps` only. Since the field's `fieldMetadata.create` runs later in the same migration, the field isn't in the maps yet and the sync failed with `ENTITY_NOT_FOUND`. The API metadata path is unaffected because create-field and update-object are separate requests (separate transactions), so the field is already persisted by the time the object update resolves. ## What this does `update-object-action-handler.service.ts` now resolves `labelIdentifierFieldMetadataId` and `imageIdentifierFieldMetadataId` against the deterministically preallocated field ids first, then falls back to the persisted flat maps for fields that already exist. The preallocated ids (`preallocatedIdByUniversalIdentifierByMetadataName`) are built from every create action before the migration loop starts (`buildPreallocatedIdByUniversalIdentifierFromActions`) and are the same ids `create-field-action-handler` persists the fields with. This is the same "preallocated-first, then flat maps" resolution that `resolveUniversalRelationIdentifiersToIds` already uses for modeled many-to-one relations, so no ordering change or new machinery is needed, and the single sync stays one atomic transaction. This does not touch the underlying action ordering or the hand-rolled label/image identifier handling flagged by the `#2172` TODO; generalizing those into the relation config remains the follow-up. ## Test Adds `relabel-onto-new-field-manifest-sync.integration-spec.ts` (used as the TDD reproduction, now green): - introducing a field and relabelling onto it in a single sync succeeds, and the object's `labelIdentifierFieldMetadataId` points at the new field; - the split path (introduce in one sync, relabel in the next) still succeeds and exposes the enriched `labelIdentifierFieldMetadataId` through the metadata API. Both cases pass against the fix. `oxlint`, `oxfmt`, and `typecheck` are clean. --- _Generated by [Claude Code](https://claude.ai/code/session_01KJewXMuWYUyrX3YJh2JBDE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22727?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. --> |
||
|
|
60fd322b49 |
Centralize system field side effects + search field metadata (#22594)
## Introduction Closes twentyhq/core-team-issues#2635 and twentyhq/core-team-issues#2642 and twentyhq/core-team-issues#2589 Object system fields (`searchVector` + its GIN index + `searchFieldMetadata`, the reserved system fields, default relations) were provisioned through several scattered, path-specific code paths. As a result the **app-manifest sync path** authored objects with an empty/`NULL` `searchVector` and **zero `searchFieldMetadata`**, so app-owned objects shipped a broken generated search column (see #22657). The generation logic also lived partly in imperative services rather than in the metadata side-effect engine, and relied on non-deterministic (`v4`) universal identifiers that `twenty apply` could not converge, destroying manually backfilled rows. This PR centralizes every object-creation system side effect into the **metadata side-effect engine**, extends the engine to keep search metadata consistent on field delete and object relabel, makes the standard app's search identifiers deterministic, and ships upgrade commands to reconcile existing workspaces. ## What changed ### Side effects moved into the metadata side-effect engine New dedicated, self-contained handlers — so every write path (API and app manifest) gets identical results, and side effects never trigger other side effects. **Object create / delete** (`handlers/object-metadata`) * **`objectSystemFieldsOnCreate`** — generates the 7 reserved system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`). * **`objectSearchVectorOnCreate`** — provisions the full-text search surface as one unit: the `searchVector` `TS_VECTOR` field, its backing GIN index, and the `searchFieldMetadata` row (for searchable objects whose label identifier is a searchable field) that keeps `searchVector` populated instead of `NULL`. * **`objectSystemSideEffectsOnDelete`** — tears the above down on object deletion. **Search-metadata consistency on relabel / field delete** (new — these are what close the manifest-path gaps) * **`objectSearchVectorOnUpdate`** (`handlers/object-metadata`) — when a searchable object is relabeled onto a new searchable field, provisions the `searchFieldMetadata` row that indexes it. Relabeling is **additive**: existing rows (e.g. the provisioned `name` row) are preserved, so the previous label identifier stays searchable. Mirrors the API update path so a manifest re-sync that changes the label identifier reaches search parity. No-ops for junction objects (`id` label identifier) and non-searchable field types. * **`fieldSearchFieldMetadataOnDelete`** (`handlers/field-metadata`) — when a field is deleted, cascade-deletes every `searchFieldMetadata` row that indexes it. `searchFieldMetadata` is excluded from manifest deletion inference, so this explicit cascade is what covers **both the API and manifest paths** (the object-scoped DB cascade only fires on object deletion). Uses the `searchFieldMetadataUniversalIdentifiers` aggregator on the flat field for an O(k) lookup instead of scanning all rows. The **default `name` field and default relations are now caller-provided default fields** (SDK autocomplete on the manifest path, input transpiler on the API path) rather than system side effects — removing duplicate name generation, the imperative `build-default-*-for-custom-object` utilities, and the ad-hoc system-field integrity validator. ### Deterministic identifiers for the standard app The twenty-standard search GIN index and `searchFieldMetadata` now derive deterministic universal identifiers (`getIndexUniversalIdentifier` / `getSearchFieldUniversalIdentifier`) instead of `v4`, so `twenty apply` converges instead of recreating. ### Upgrade commands (`2-20`) to reconcile existing workspaces **Instance commands** (run once per instance; ordered fast → slow → workspace): 1. **`AddIsSystemSideEffectToSearchFieldMetadata`** (fast) — adds the `isSystemSideEffect` column to `core.searchFieldMetadata`. Defaults to `true`, which also correctly backfills every existing row since `searchFieldMetadata` is always system-derived (never user-authored). 2. **`BackfillNameFieldIsSystemSideEffect`** (slow) — re-flags existing `name` fields from `isSystemSideEffect: true` → `false`, since the default `name` field is now a caller-provided default like any other user-owned field (it was provisioned as `true` in 2.15 → 2.19). This is a pure data backfill, so the bulk `UPDATE` lives in `runDataMigration()` rather than `up()` — keeping it out of the fast schema transaction avoids holding an `ACCESS EXCLUSIVE` lock that could stall reads during the deploy. Slow instance commands still run before every workspace command of the version, so the fresh value is in place before the search-reconcile workspace commands recompute the `fieldMetadata` flat-entity cache. Scoping by name alone is safe (no engine-owned field is named `name`); `down()` is best-effort (pre-2.15 `false` rows are indistinguishable from flipped ones). **Workspace commands** (idempotent, dry-run supported): 1. **`reconcile-search-vector-gin-index-universal-identifier`** — re-owns every searchVector GIN index UID to its deterministic value (all applications), then backfills the missing GIN index for installed-app objects. 2. **`reconcile-search-field-metadata`** — re-owns every `searchFieldMetadata` UID (all applications), then backfills the missing rows for installed-app searchable objects. 3. **`rebuild-installed-app-search-vectors`** — rebuilds the `searchVector` column of every installed-app `TS_VECTOR` field, once the index and rows exist. Design notes: * **Re-own is global** (twenty-standard, workspace-custom, installed) — a UID convergence keyed on each row's own application. * **Backfill is installed-app only** — standard/custom objects already have these rows via the manifest funnel. * Re-own runs **before** backfill and is transaction-guarded; a failure aborts that workspace to avoid a unique-identifier collision. ## Tests * Integration: app manifest sync now asserts system fields + searchable objects (searchVector, GIN index, searchFieldMetadata) are created; a new relabel suite drives three manifest syncs and asserts records stay searchable through the old + new label identifiers and lose searchability when a field is removed; removed the obsolete system-fields-integrity suite/snapshots. * Unit: per-handler side-effect specs (including the new `objectSearchVectorOnUpdate` and `fieldSearchFieldMetadataOnDelete` handlers), and per-util specs for the re-own / backfill operation builders and the GIN-index classifier. ## Upgrade / migration notes * Existing workspaces converge on the next upgrade run via the `2-20` instance + workspace commands (idempotent, dry-run supported). * Backfill and rebuild go through the workspace-migration runner (automatic cache invalidation); the re-own step invalidates only the affected flat-entity maps directly. * The cross-version upgrade CI now flushes the cache before running the upgrade, so the new version recomputes every flat-entity map from the database instead of reading blobs the old version serialized in an older shape. ## Follow-up * `object-metadata.service.ts` still carries a `TODO: remove once default view fields move to the metadata side effect engine` — default view fields are the next candidate to move into the engine. * A single manifest sync cannot yet both create a field and relabel the object onto it, because `objectMetadata.update` is ordered before `fieldMetadata.create` in the migration runner. Tracked in twentyhq/core-team-issues#2655; to be fixed in a follow-up. |
||
|
|
b47e64c9e3 |
docs: surface the self-serve partner marketplace on high-intent pages (#22719)
## Summary Adds partner-marketplace CTAs to four high-intent docs pages (enterprise + migration), routing readers to the **self-serve** partner directory (`twenty.com/partners/list`) while keeping `contact@twenty.com` as a secondary option. This upgrades pages that previously only offered the old "email us to be matched" path (or no partner path at all). Rendered with Mintlify-native components — `<Tip>` callouts, plus a `<CardGroup>` on the Implementation Services page. No new snippet; no `docs.json`, navigation, or translation (`l/`) changes. ## Pages changed — screenshots (one per page) > Preview locally with `npx nx run twenty-docs:dev` (localhost:3000), or use the Mintlify PR preview once it posts on this PR. Paths below are under `docs.twenty.com`. ### 1. `/user-guide/permissions-access/capabilities/sso-configuration` `<Tip>` callout → *Find a certified Twenty partner* (Solutioning), `contact@twenty.com` as a secondary aside. _screenshot:_ <img width="712" height="616" alt="Screenshot 2026-07-09 at 12 14 05" src="https://github.com/user-attachments/assets/e6521634-7783-466f-bfae-a4a49f64d941" /> ### 2. `/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud` `<Tip>` callout → *Get a certified Twenty partner* (Hosting), contact fallback. _screenshot:_ <img width="665" height="193" alt="Screenshot 2026-07-09 at 12 14 23" src="https://github.com/user-attachments/assets/30ecb4d2-fb7e-4e0f-9355-491713290f90" /> ### 3. `/user-guide/data-migration/how-tos/migrating-from-other-crms` `<Tip>` callout → **Done for you** (partner) vs **Onboarding pack** (Twenty team). _screenshot:_ <img width="659" height="273" alt="Screenshot 2026-07-09 at 12 14 35" src="https://github.com/user-attachments/assets/9cc5bb3e-c8be-4734-9e03-dc5b536eeeb7" /> ### 4. `/user-guide/getting-started/capabilities/implementation-services` `<CardGroup>` → **Browse certified partners** / **Get matched by Twenty**. _screenshot:_ <img width="706" height="766" alt="Screenshot 2026-07-09 at 12 14 47" src="https://github.com/user-attachments/assets/01b69197-0f47-46fe-a40e-92c11286827e" /> ## Notes for reviewers - Links deep-link the directory via `?categories=<scope>` (verified live) and carry a `?ref=docs-*` tag. - **Attribution caveat:** twenty.com's analytics (Cloudflare Web Analytics) is path-based, so `?ref=` is not measurable yet. - `contact@twenty.com` intentionally kept as a secondary option. - `mintlify validate` passes. Opened as a draft. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22719?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. --> |
||
|
|
f06ba08b16 |
Fix onboarding locale reverting to English after signup (#22675)
During onboarding the UI reverted to English after email verification. The chosen locale was never stored on the user, so the workspace member seeded from it inherited `en`, and after workspace activation `loadCurrentUser` reactivated `en` and flipped the whole UI to English regardless of the browser language. Two changes: **Backend (the actual fix):** the `signUp` resolver received `signUpInput.locale` but only used it for the verification email, creating the user without it (so it defaulted to `en`). It now passes the locale into `signUpWithoutWorkspace`, and the SSO create-a-workspace path forwards `locale` as well. The user, and the workspace member created from it at activation, now keep the chosen locale, so the post-activation reactivation no longer forces English. **Frontend:** carry the active locale across the workspace subdomain redirect (in `useBuildSearchParamsFromUrlSyncedStates`) so the freshly loaded subdomain renders `/verify` in the right language before the current user loads, instead of briefly falling back to the browser default. Minor, only affects the case where the chosen locale differs from the browser language. |
||
|
|
a22fdf1d8e |
fix: prevent duplicate junction rows from double-fired checkbox clicks in multi-select menu items (#22737)
## What Fixes a bug where selecting a relation from the record picker created **two** rows in a junction object instead of one (issue #22698). ## Root cause Base UI's `Checkbox` renders a styled `<span role="checkbox">` plus a hidden `<input>`. On click of the span it re-dispatches a second, *bubbling* click on the hidden input (to keep the native input in sync) without stopping propagation. Both the original span click and the re-dispatched input click bubble up to the menu-item row, whose `onClick` drives selection — so one physical click on the checkbox fired the row's handler **twice**. This was invisible until now because every consumer's handler was idempotent: - multi-select toggle: both calls pass the same `!selected`, net one toggle - relation attach: setting a foreign key twice is the same result The junction relation feature is the first non-idempotent consumer: each call creates a new junction record with a fresh UUID, so two calls produced two rows. ## Fix Extract a shared `MenuItemMultiSelectCheckbox` part that: - drives selection through the checkbox's own `onCheckedChange` (events up) - wraps the checkbox so its click cannot propagate to the row's `onClick` The row stays clickable for the rest of the item; the checkbox click and the row click are now two clean, single-fire event sources. Applied to all three affected components (`MenuItemMultiSelect`, `MenuItemMultiSelectAvatar`, `MenuItemMultiSelectTag`) so the whole class of bug is fixed once, not patched per component. No change to the shared `Checkbox` API. ## Notes for reviewer - The `oxlint-disable` for the stopPropagation wrapper's `onClick` now lives in exactly one place (the shared part), following the existing precedent in `OverflowingTextWithTooltip`. - Added a regression interaction test on the `MenuItemMultiSelectAvatar` story (one checkbox click = one `onSelectChange`); it exercises the shared part. - `typecheck`, `oxlint`, and `oxfmt` pass. The Storybook vitest-browser runner is currently broken locally for all stories, so the interaction test was not run locally — it runs in CI. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22737?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. --> |
||
|
|
7475b5f16f |
perf(upsert) - tighten createMany upsert candidate lookup to avoid broad OR scans (#22721)
## Summary
Fixes a performance-correctness bug in the createMany upsert path where
the
existing-record lookup built an overly broad WHERE clause, causing full
table
scans and multi-second latency on bulk upserts.
Reported via Sentry: a 100-record create*(upsert: true) request on
_sdWorkspace
completed with HTTP 200 but took ~14s. The candidate-lookup SELECT alone
took
~6.7s because it fetched a huge superset of rows before matching in
memory.
## Root cause
buildWhereConditions created one IN(...) per conflicting column across
the whole
batch, and findExistingRecords OR-ed them together:
WHERE "cbCustomerId" IN ($1..$100) OR "environment" IN ($101..$200)
For a composite unique index (cbCustomerId, environment), this is
semantically
wrong: it OR's the columns instead of matching them as a tuple. Because
environment is low-cardinality (prod/staging/dev/test), the second IN
alone
matched almost the entire table, forcing a Seq Scan and shipping the
whole
result set to the app for in-memory filtering.
## Fix
buildWhereConditions now generates targeted lookup conditions:
- Single-column unique keys collapse into one `column IN
(distinctValues)`
condition (instead of N OR-ed equalities).
- Composite unique keys produce one `(colA = ? AND colB = ?)` condition
per
input record — the columns are ANDed as a tuple, and separate unique
indexes
are still OR-ed together.
- Conditions are deduplicated (robust JSON-based key, no separator
collisions)
to avoid redundant OR branches.
- Values are now typed as string | number | boolean instead of being
implicitly
coerced to string.
## Benchmark
Reproduced the incident with a table mirroring _sdWorkspace:
high-cardinality
cbCustomerId + low-cardinality environment (4 values), composite unique
index on
(cbCustomerId, environment), 100-record upsert batch. EXPLAIN (ANALYZE,
BUFFERS)
on a warm 500k-row / 392 MB table:
Metric | OLD (colA IN OR colB IN) | NEW (targeted) | Improvement
----------------------|--------------------------|----------------|------------
Scan type | Seq Scan (full table) | Index Scan | index vs full scan
Rows returned to app | 500,000 | 100 | ~5,000x fewer
Buffers touched | ~45,455 (~355 MB) | 400 (~3.2 MB) | ~110x fewer
Execution time | 224 ms | 11.9 ms | ~19x faster
Data shipped to app | ~322 MB | ~64 KB | ~5,000x less
The cache-independent facts are the proof: the old query returned the
entire
table for a 100-record batch (the "overly broad record set" from the
report),
while the new query returns exactly the matching rows via the composite
index.
## Test plan
- [x] Unit tests for buildWhereConditions (single-column IN batching,
nested
paths, composite AND tuples, dedup incl. separator-collision safety,
numeric values, mixed single-column + composite OR) — 22 passing across
build-where-conditions, get-matching-record-id, get-value-from-path.
- [x] Upsert integration suites pass (upsert +
composite-unique-index-upsert,
10 tests).
- [x] EXPLAIN ANALYZE benchmark confirms Index Scan and bounded row
counts.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22721?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. -->
|
||
|
|
b2c2c2341c |
i18n - translations (#22744)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22744?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> |
||
|
|
bdcdaaa3d8 |
Fail App docs drift check ci if doc drift detected (#22713)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22713?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. --> |
||
|
|
c8c38b6cd2 |
Fix onboarding entry animations not triggering on first load (#22724)
The staggered entry animations on the onboarding welcome/sign-in and
sync-emails screens sometimes didn't play on first load, though a
refresh reliably fixed them.
Root cause: `OnboardingTransitionOutlet` wrapped pages in
`<AnimatePresence initial={false}>`. framer-motion propagates that
`initial: false` through `PresenceContext` to every descendant motion
component on the outlet's first render, so any
`OnboardingStepAnimatedItem` that mounts during that first commit snaps
straight to its final state instead of animating. Because onboarding
pages are preloaded (`lazyWithPreload`), a warm in-app navigation
renders the page synchronously in that first commit and the cascade is
suppressed; a cold load/refresh mounts it a commit later via Suspense,
so it animates.
Fix: drop `initial={false}`. Descendants no longer inherit a blocked
initial state, so the cascade animates regardless of preload timing. The
page-level fade now also runs on first entry, matching the treatment
already used on every step-to-step transition and cold load.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22724?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. -->
|
||
|
|
9ffd5866c9 |
i18n - translations (#22743)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
1ee4065f41 |
i18n - translations (#22741)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22741?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> |
||
|
|
012c1b3bfe |
i18n - translations (#22738)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22738?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> |
||
|
|
195eeec5da |
i18n - translations (#22735)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22735?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> |
||
|
|
ff692f3687 |
i18n - translations (#22733)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22733?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> |
||
|
|
c00ca37ff5 |
i18n - translations (#22732)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22732?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> |
||
|
|
3083f6f947 |
i18n - translations (#22731)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22731?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> |
||
|
|
7d71b5af58 |
i18n - translations (#22730)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
56bd467825 |
i18n - translations (#22729)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22729?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> |
||
|
|
80cf187ab3 |
i18n - translations (#22728)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22728?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> |
||
|
|
7130aae772 |
i18n - translations (#22726)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22726?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> |
||
|
|
a3d9efd94a |
i18n - translations (#22725)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22725?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> |
||
|
|
c0cb0676a3 |
i18n - docs translations (#22723)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22723?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> |