Compare commits

..
Author SHA1 Message Date
Charles Bochet 4a74a030ba test: bump banner min-height to verify argos visual regression 2026-06-04 02:31:19 +02:00
Abdullah.andGitHub ccffc4a1ea Fix axios related dependabot alerts generated against root yarn.lock (#21187)
Fixes the following Dependabot alerts:
https://github.com/twentyhq/twenty/security/dependabot?q=is%3Aopen+package%3Aaxios+manifest%3Ayarn.lock+has%3Apatch

Upgraded the referenced version in root yarn.lock. Creating a separate
PR for the nested ones to keep the updates isolated (e.g.
/seed-dependencies/yarn.lock).
2026-06-03 23:19:42 +00:00
685c0ddd12 i18n - docs translations (#21195)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 21:43:27 +02:00
EtienneandGitHub 15eaabdbc1 fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
  - `find_many(_companies)`: **7 158 → 2 700 tokens**
  - `find_one(_company)`: **280 → 126 tokens**
  -  ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.

- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
2026-06-03 17:57:40 +00:00
Abdullah.andGitHub 50c9b68e81 Update axios in package.json of seed-dependencies (#21190)
Fixes the following Dependabot alerts:
https://github.com/twentyhq/twenty/security/dependabot?q=is%3Aopen+package%3Aaxios+manifest%3Apackages%2Ftwenty-server%2Fsrc%2Fengine%2Fcore-modules%2Fapplication%2Fapplication-package%2Fconstants%2Fseed-dependencies%2Fyarn.lock+has%3Apatch

Follow up to the alerts not addressed in
[this](https://github.com/twentyhq/twenty/pull/21187) PR.
2026-06-03 17:49:02 +00:00
34d893f9fd i18n - translations (#21192)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 19:57:05 +02:00
Priyanshu BartwalGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Etienne
d3a7ea0790 Fix: Not able to add multiple handles to Blocklist in settings/accounts (#21049)
Fixes: #21031

### Root cause:
This feature was never fully implemented even though the placeholder
text suggested it was supported. It could only update one handle at a
time.

### Fix
Fixed the zod validation to validate each handle separately.
Used `useCreateManyRecords` to update multiple handles at the same time.

### Before:
<img width="2032" height="1162" alt="Screenshot 2026-05-29 at 3 25
12 PM"
src="https://github.com/user-attachments/assets/ae6b6ae3-ed38-4410-801e-11f514773681"
/>

### After:


https://github.com/user-attachments/assets/69354129-422a-41de-baf7-fa5a28f01f3f

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-03 17:34:51 +00:00
49828d9379 i18n - translations (#21191)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 19:17:44 +02:00
EtienneandGitHub dd0039ca1c feat(mcp) - optimize instruction prompt and hide get_tool_catalog (#21183)
Workspace-aware initialize.instructions

- Deleted the static mcp-server-instructions.const.ts
- Created build-mcp-server-instructions.util.ts — a comprehensive system
prompt with identity, object list, tool grammar, routing decision tree,
intent mapping, skills vs tools, safety constraints, and data efficiency
guidelines
- Created McpInstructionBuilderService — fetches workspace-specific
object names + skill names and injects them into the instructions


Hide/deprecate get_tool_catalog

Benefit : skip first MCP call (tools are included in instruction)
2026-06-03 16:58:32 +00:00
MarieandGitHub 4b15b949f3 Provide additional logsobservability to workflow runs (per node) (#21142)
Surfaces per-step "Logs" tabs in the workflow run side panel so users
can see what each step actually did (model + tokens + tool calls for AI,
console output for serverless functions, request/response for HTTP,
recipients/body for Email).

<img width="546" height="501" alt="ai_agent_without_websearch"
src="https://github.com/user-attachments/assets/c6ca3518-9489-4484-a570-3d0569ff3b03"
/>

## Storage

- New `stepLogs` JSONB column on the `workflowRun` workspace entity,
typed as `Record<string, WorkflowRunStepLog>` (keyed by step id).
- Schema lives in `twenty-shared`: `workflowRunStepLogSchema` with a
discriminated `details.type` union for `AI_AGENT | CODE | HTTP_REQUEST |
EMAIL` — frontends and backends consume the same Zod-inferred type.
- Field is added to existing workspaces via a workspace upgrade command
(`2-9 add-workflow-run-step-logs-field`); the standard-object metadata
declares it for new workspaces.
- Writes happen atomically per step in
`WorkflowRunStepLogWorkspaceService.setStepLog` using `jsonb_set`. That
lets concurrent steps in the same run write their own keys without
contending with the existing lock around `workflowRun.state`.
- Per-step payload is hard-capped at 256 KB; anything larger is dropped
with a `logger.warn`, so a pathological tool call can never bloat a row.
See below for more information.

## How logs are produced

**Aalmost everything was already being collected; this PR mostly
persists and renders it.**

- **AI agent** — `AgentAsyncExecutorService` already tracked token
usage, model id, native web-search count, and the AI SDK's `steps[]`. We
map those into the log via `mapAiStepsToToolCallLogs` (`searchVector`
stripped from record outputs, per-call input/output capped at 32/64 KB,
max 200 tool calls per step). The only new measurement is a wall-clock
`durationMs` taken around `executeAgent`, and we now fold native
web-search cost into the displayed `totalCostInDollars` (it was already
billed, just not shown).
- **Code / serverless function** — reuses the `console.log` output the
function runner already returns (`logsByLevel`);
`build-code-step-log.util` only repackages it.
- **HTTP request** — built from the action's existing input/output via
`build-http-request-step-log.util`. No new signals collected.
- **Email (send / draft)** — added `sanitizedHtmlBody` + `plainTextBody`
to the existing tool outputs (a small additive change), then
`build-email-step-log.util` consumes them.

No additional AI inference or external calls are made for logging — the
cost is a small CPU overhead per step plus the JSONB write.

## Security

The log surface intentionally shows whatever the workflow touched, which
made redaction and sanitization the main design concern.

- **HTTP — secrets in headers**: existing `SENSITIVE_HEADER_NAMES` set
(Authorization, Cookie, …) replaced with `[redacted]` in both request
and response.
- **HTTP — secrets in URLs**: `SENSITIVE_URL_PARAM_NAMES` (e.g.
`api_key`, `token`, `access_token`) replaced in the query string via
`URL`-based parsing.
- **HTTP — secrets in bodies**: `SENSITIVE_BODY_KEY_REGEX` deep-walks
JSON request/response bodies (object input or stringified JSON) and
redacts matching keys. Applied to the `error` field too, since
transport-layer errors sometimes embed structured payloads.
- **Email — XSS risk in body preview**: tool outputs now expose a
server-side `sanitizedHtmlBody`; the log builder prefers it over the raw
user-authored `input.body`, with `plainTextBody` as a second fallback.
The original raw body is only used if sanitization didn't happen (e.g.
tool failed before composing).
- **AI — internal/noisy data**: `searchVector` (Postgres tsvector
strings) is stripped from record outputs returned by Twenty tools to
avoid leaking internal full-text-search payloads.
- **DB bloat / runaway agents**: 256 KB per-step cap + 32 KB / 64 KB
per-tool-call input/output cap + 200 tool calls per step.

<img width="547" height="307" alt="logic_function"
src="https://github.com/user-attachments/assets/dd4a3d16-67f2-434b-95b3-bdcaf9ed053d"
/>

## More details on Log size & truncation

Logs are stored in `workflowRun.stepLogs` (JSONB), keyed by `stepId`.

### Per-step cap

Each step's log is hard-capped at **256 KB** (`MAX_STEP_LOG_BYTES` in
`WorkflowRunStepLogWorkspaceService.setStepLog`).

For ~99% of workflows this is roomy — typical real-world sizes:
- Code / serverless function: 1–20 KB
- HTTP request: 5–70 KB
- Email: 5–30 KB
- AI agent (a handful of tool calls): 5–50 KB

### Two layers of bounding

1. **Per-field truncation** in each builder (before writing):
   - **Code**: ≤ 500 entries, ≤ 4 KB per message, ≤ 8 KB stack trace
   - **HTTP**: ≤ 32 KB per body (request + response), UTF-8 byte-aware
   - **Email**: ≤ 8 KB body preview, UTF-8 byte-aware
- **AI agent**: ≤ 32 KB tool input, ≤ 64 KB tool output, ≤ 200 tool
calls/step

2. **Global per-step safety net** at write time: if the assembled
`stepLog` still exceeds 256 KB, the write is **dropped entirely** with a
`logger.warn`. The workflow itself keeps running unaffected.

### What this means in practice

- **Safe**: workflow execution, step results, downstream steps — never
blocked by log size.
- **Safe**: iterators (each iteration overwrites the previous log for
that `stepId`, so they can't accumulate).
- **Safe**: step retries (same `stepId` is overwritten, not appended).
- **Possible**: an AI agent step with many large tool outputs (e.g., 50+
heavy `web_search` calls) can exceed 256 KB → the **entire** step's log
is dropped, side panel shows "No logs were recorded for this step". The
user has no explicit signal that the log was dropped due to size (only
server-side warn).
- **Possible** (theoretical): a workflow with hundreds of distinct steps
could push the row toward Postgres's internal ~256 MB jsonb limit.
Beyond that, individual `jsonb_set` writes would error and be swallowed
by the action's try/catch — workflow still completes.

### Possible future hardening (not in this PR)

- Replace "drop entire log" with a stub that preserves the summary card
(cost, duration, status) and marks `truncated.reason = 'size_cap'`.
- Surface size-drops in the UI (similar to the existing
`<StyledTruncatedNotice>`).
- Emit a metric so dropped logs are observable in dashboards.
2026-06-03 16:53:47 +00:00
martmullandGitHub 0671ff3de5 Fix lambda error (#21179)
- move twenty-sdk from dependencies to devDependencies
- add documentation about breaking
- add warning about moving the package to dev dependencies
2026-06-03 16:45:51 +00:00
EtienneandGitHub 7fcbc45017 perf(ai) - per-tool schema resolution in resolveSchemas (#21188)
learn_tools(["find_many_companies"]) goes from ~170ms → ~2ms (85x
faster, measured with 40 objects / empty fields — real workspaces would
see even bigger savings). Previously it generated schemas for all ~250
tools and discarded 249;

now it generates exactly the requested one(s).
2026-06-03 16:29:26 +00:00
c7365b09e2 i18n - translations (#21189)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 17:49:40 +02:00
Thomas TrompetteandGitHub ac0368e876 fix: eliminate workflow editing flicker on active-to-draft transitions (#21176)
## Before

https://github.com/user-attachments/assets/5108a9d8-2017-41d5-855c-98714cbd4237

## After

https://github.com/user-attachments/assets/0a78d1e1-354f-4f3f-8ec4-6f46517619e4

## Summary
- Fixes visual flickering/glitching in the workflow show page header and
canvas when editing an active workflow or discarding a draft
- Root cause: SSE events re-added discarded drafts to Apollo cache, and
multiple hook instances had independent state causing version
oscillation between DRAFT and ACTIVE
- Rewrites `useWorkflowWithCurrentVersion` with a module-level
`discardedDraftId` variable shared across all instances, Apollo cache
seeding in mutation callbacks, and `lastValidResult` caching to prevent
null renders

## Test plan
- [x] Open a workflow show page with an ACTIVE workflow
- [x] Drag a node to change position → verify no flicker, status shows
DRAFT smoothly
- [x] Discard the draft → verify header does NOT flicker between
DRAFT/ACTIVE, position resets cleanly
- [x] Click on manual trigger and edit settings → verify the edit works
(draft created, settings saved)
- [x] Repeat discard + edit cycle multiple times to confirm stability
2026-06-03 15:38:38 +00:00
WeikoandGitHub cc76b7bc50 Fix fields widget new field visibility (#21111)
Fixes https://github.com/twentyhq/twenty/issues/21043

## Context
Newly created fields were never added to FIELDS widgets, regardless of
the "Set fields created in the future as visible" toggle. The widget's
newFieldDefaultVisibility was null on widgets that never explicitly set
it (it was never populated at creation), so the backend skipped them and
no view field was created.

## Implementation
Keep newFieldDefaultVisibility nullable with false (not visible) as the
behavior when not provided.
The FE now reflects that properly and shows "un-toggled" when it's null
(iso with BE behavior).

The fix also ensures the value is explicitly set to true wherever it
should be:
- Set newFieldDefaultVisibility: true at every FIELDS widget creation
path (backend default record-page layout, frontend
createDefaultFieldsWidget + useTemporaryFieldsConfiguration);
- Added a 2-9 workspace upgrade command that backfills true onto
existing standard FIELDS widgets where the value is null.
2026-06-03 15:29:57 +00:00
e64e5662e5 fix(ai-chat): refresh JWT token on SSE reconnect to prevent login red… (#20176)
Closes #18928

## Problem

When a JWT access token expires while the AI chat is streaming a
response, the SSE connection drops and `graphql-sse` calls the retry
callback. The previous implementation would wait, then destroy the SSE
client but never refreshed the token. On the next connection attempt the
client reused the same expired token, eventually triggering an
`UNAUTHENTICATED` error that redirected the user to the login screen.

## Solution

Add proactive token renewal inside `useHandleSseClientConnectionRetry`
before each reconnect attempt:

- Uses a module-level `let renewalPromise` variable to deduplicate
concurrent renewal requests , the exactpattern used in
`ApolloFactory.ts`
- Calls `renewToken` via `retryWithBackoff` against the `/metadata`
endpoint
- Writes the fresh token pair into the Jotai store ,the SSE client's
`headers()` callback picks it up automatically on reconnect
- If renewal fails -> falls back to destroying the SSE client as before

## Files changed

-
`packages/twenty-front/src/modules/sse-db-event/hooks/useHandleSseClientConnectionRetry.ts`

## Notes

This addresses the two issues from the previous review:
- No `useRef`  using module-level variable instead
- CI passing  removed the `CombinedGraphQLErrors` import

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-03 15:27:30 +00:00
Paul RastoinandGitHub 164a5b1e8d Refactor email composer (#21177)
# Introduction
Gate what connected account can be ingested in case of ai mcp user
workspace agnostic funnel to only the workspace shared connected account

Added a quick win intregration tests on seeded connected accounts ( that
wasn't covered but already protected fix impacts only the mcp )

Refactored the API slightly too

## Notice
This mean there's a breaking change in the product behavior
Whereas before a non user workspace related mcp interaction would might
have fallback on any private user connected account it will now only
search for workspace visible listed ones
2026-06-03 15:15:51 +00:00
Rashad KaranouhandGitHub 8c3a93871e fix: update yarn.lock after removing dotenv and zod from twenty-sdk (#21174)
## Summary

- Commit b330105470 removed `dotenv` and `zod` from
`packages/twenty-sdk/package.json` dependencies but did not run `yarn
install`, leaving the lockfile out of sync.
- `yarn install --immutable` in CI was failing with `YN0028: The
lockfile would have been modified by this install` on those two
packages.
- This PR just runs `yarn install` to sync the lockfile — no logic
changes.

## Root cause

[Failing CI
run](https://github.com/twentyhq/twenty-infra/actions/runs/26883193920/job/79288298420):
```
YN0028: -    dotenv: "npm:^16.4.0"
YN0028: -    zod: "npm:^4.1.11"
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
```

## Changes

`yarn.lock` only — removes the `dotenv@^16.4.0` range and the
`twenty-sdk` metadata entries for `dotenv` and `zod`.
2026-06-03 12:23:48 +00:00
0a86a6c6db i18n - website translations (#21165)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-03 14:10:35 +02:00
martmullandGitHub b330105470 Fix Unzipped size must be smaller than 250Mb error (#21172)
move dev dependencies into devDependencies
2026-06-03 11:39:11 +00:00
Rashad KaranouhandGitHub c2ad3f3614 (partners): bump app version 0.3.3 -> 0.3.4 (#21167)
Forgot to bump the version when merging #21162 from `0.3.3` to `0.3.4`
2026-06-03 07:34:28 +00:00
0d9f7673c9 chore: sync AI model catalog from models.dev (#21170)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-06-03 09:34:21 +02:00
Rashad KaranouhandGitHub 6b5a956c85 feat(website): partner marketplace UI fixes & CTA improvements (#21163)
## Summary

Partner marketplace UI fixes and CTA improvements on the website
(`twenty-website`).

## Changes

- **Find a partner buttons** → link to `/partners/list` (hero + signoff
sections) instead of opening the contact modal.
- **Chip row layout fix** → align partner chip rows to the first chip's
baseline. In the narrow 3-column grid, category chips wrap to multiple
lines; centering floated the label to the middle and visually lifted the
first chip into the row above (e.g. "Custom Development" appeared under
Languages). Baseline keeps the label pinned beside the first chip.
- **Card CTA** → replace the conditional "Book a call" calendar CTA with
a "View profile" link to the partner profile page, shown on every card
regardless of whether a calendar link exists. Keeps card heights
consistent across the grid.
- **Card avatar** → show the partner's real profile picture when a safe
`http(s)` URL is present, keeping the initials block as the fallback.
- **Profile "Contact <partner>" CTA** → when a partner has no booking
(calendar) link, show a "Contact <partner>" button (alongside LinkedIn
if present) that opens a `mailto:rashad@twenty.com` with a partner-named
subject and a pre-filled body prompt.

## Tests

- New: `PartnerAvatar`, `PartnerProfileCtas` unit tests.
- Updated: `PartnerCard` tests for the View-profile CTA.
- All `Partner*` website tests pass.

## Screenshots 
<img width="1440" height="816" alt="Screenshot 2026-06-02 at 23 40 55"
src="https://github.com/user-attachments/assets/bd88a9be-3297-4d7f-891c-c9d403d2b4d9"
/>
<img width="1441" height="818" alt="Screenshot 2026-06-02 at 23 40 47"
src="https://github.com/user-attachments/assets/b315424f-48d1-4d29-9e97-1fcf4d8c47f2"
/>
<img width="458" height="505" alt="Screenshot 2026-06-02 at 23 40 18"
src="https://github.com/user-attachments/assets/2a4e0ec2-8e0a-4220-84ec-177c788aa580"
/>
2026-06-02 20:22:41 +00:00
Rashad KaranouhandGitHub ff5d082e7c feat(partners): remove Project Budget Typical field, rework partner views & nav order (#21162)
## Summary

Partners-app changes spanning the Partner object, its data scripts,
table views, and sidebar navigation.

### Remove the "Project Budget Typical" field
Dropped the `projectBudgetTypical` currency field from the Partner
object and every reference to it:
- `get-partner-by-slug` and `list-available-partners` logic-function
selections
- the seed script (type, write mapping, and per-partner data)
- the `import-from-tft` mapping (also dropping the now-unused
`partnerBudgetAverage` TFT source selection)

`projectBudgetMin` is intentionally kept.

### Rework partner views
- **Partners** (all-partners) view: replaced the **Deployment
Expertise** column with **Categories** (the `partnerScope` field).
- **Validated partners** view: added a **Languages Spoken** column.
- Set view `position`s so the in-object view switcher orders **Validated
→ Applications → Partners**.

### Navigation order
Reordered the "Partners" folder navigation items so the sidebar reads
**Validated partners → Partner applications → Partners** (Partner
content stays last).

### Also included
The previously-pushed fix that excludes partners with an empty slug from
the available-partners list.

## Notes
- No deploy/sync performed. The view-column and navigation-ordering
changes take effect once the app manifest is synced (`yarn twenty dev
--once` locally).
- The `deploymentExpertise` field itself is unchanged — only its column
was removed from the all-partners view.
2026-06-02 20:19:07 +00:00
Thomas TrompetteandGitHub 015b5b6919 fix(twenty-server): fix AI tools description for SELECT view filter values (#21156)
Fixes https://github.com/twentyhq/twenty/issues/20840

## Summary
- Fix the AI tools factory (`view-filter-tools.factory.ts`) description
that incorrectly described SELECT filter values as plain strings,
causing agents to write values like `"CLOSED_LOST"` instead of the
required array format `["CLOSED_LOST"]`

This led to production crashes when later trying to update/delete select
options on fields that had view filters created by AI agents with
invalid format.

## Test plan
- No behavior change in existing code — only the tool description is
updated to guide AI agents correctly
- Manual: confirm AI agents now create SELECT filters with array values
2026-06-02 15:59:26 +00:00
Charles BochetandGitHub f6a17bc5f7 refactor(server): split LambdaDriver and LocalDriver into focused sub-services (#21153)
## Why

`LambdaDriver` (1.4k lines) and `LocalDriver` (709 lines) each mixed
many unrelated concerns in a single class.

## What changed

**This PR only moves code** — every line is byte-for-byte the same as
`main` (same logic, same comments, same constants, same lock keys/TTLs,
same error mapping). The drivers are now thin orchestrators wiring
co-located sub-services + pure utils (each with unit tests).

| File | Before | After |
|---|---|---|
| `lambda.driver.ts` | 1425 | 252 |
| `local.driver.ts`  | 709  | 231 |
| Largest sub-service | — | 371 (`lambda-executor-manager`) |
| Unit tests on these paths | 0 | 15 |

Lambda split: `lambda-aws-client` / `lambda-tool-functions` /
`lambda-layer-manager` / `lambda-executor-manager` + `constants` +
`types` + 5 pure utils.

Local split: `local-layer-manager` / `local-child-process-runner` /
`local-prebuilt-bundle` + `constants` + `types` + 3 pure utils.

No behavior change, no signature change, no public-API change.
2026-06-02 15:43:11 +00:00
Raphaël BosiandGitHub 1833fa84a5 Fix front component pointer/mouse event coordinates (#21117)
Fixes https://github.com/twentyhq/twenty/issues/21000

Front-component event handlers read standard event fields
(event.clientX, event.offsetX, …), but these were always undefined. On
the remote side, serialized event data was passed only as the
CustomEvent's detail — and CustomEvent ignores every constructor option
except detail, so the values lived at event.detail.clientX and never on
the event object itself.

- Added `applySerializedEventProperties`, to copy a curated allowlist of
event-level keys onto the event. Element/target state (value, checked,
files, scroll, media props) stays in
`applySerializedEventTargetProperties`, applied to this (the dispatch
element = event.target).
- Added x/y to `SerializedEventData` and to host-side serialization in
`createHtmlHostWrapper`.
- Added an `svg-pointer `story + `createHtmlTagPointerStory`

Note: Also pinned @types/react to v18 so the renderer stops dragging in
React 19 types and breaking typecheck.
2026-06-02 15:04:11 +00:00
1642be86f5 Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
2026-06-02 14:39:14 +00:00
Charles BochetandGitHub cb744b2eeb refactor(twenty-server): collapse workspace migration build orchestrator (#21144)
## Why

`WorkspaceMigrationBuildOrchestratorService.buildWorkspaceMigration` was
1117 lines of 30 near-identical copy-pasted blocks (one per metadata
entity) plus a 165-line ordered `actions: [...]` literal. Adding a new
entity meant copying ~30 lines and hoping the boilerplate stayed in
sync; the actual per-entity execution order was buried in the noise.


## Gain

- Orchestrator: **1117 → 388 lines**
- Net diff: **-558 lines**
- Adding a new entity is now a single line in the registry
- Execution order is visible at a glance
2026-06-02 14:16:10 +00:00
Anish PaudelandGitHub b422550fcc refactor(server): merge duplicate TypeOrmModule.forFeature calls in MessagingMessageCleanerModule (#21150)
Combined two separate `TypeOrmModule.forFeature()` calls into one. Both
registered entities on the default data source, so no behavioral change.
Repositories for WorkspaceEntity and MessageChannelEntity remain
injectable as before.
Cleaner imports, one less redundant call.
2026-06-02 14:10:36 +00:00
3aae4330b6 i18n - website translations (#21137)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 16:10:15 +02:00
Félix MalfaitandGitHub 1e336dbad1 feat: allow many-to-one relations as advanced filter leaves (#21147)
## What

Lets a many-to-one relation be selected as the **leaf** of an advanced
(nested) filter. Previously the nested-field submenu excluded relations,
so you could filter `Opportunities WHERE company.Name contains X` but
not `Opportunities WHERE company.accountOwner = me`.

## How it works

Selecting a relation leaf filters by its **foreign key** —
`company.accountOwnerId = X` — a single hop the backend already resolves
on the joined table (`{ company: { accountOwnerId: { in: [...] } } }`).
It is **not** a multi-hop traversal: filtering on a *scalar field of*
the related record (e.g. `company.accountOwner.name`) stays excluded,
since that needs a second join the backend caps at one hop.

Two changes:
- **`AdvancedFilterRelationTargetFieldSelectMenu`** — stop excluding
many-to-one relations from the nested-field submenu.
- **`ObjectFilterDropdownRecordSelect`** — resolve the record picker's
object from the *leaf* relation's target (e.g. WorkspaceMember,
including the "Me" pin) rather than the source relation's object. The
source-field fallback applies only when there is no leaf.

## Testing
- Added `turnRecordFilterIntoRecordGqlOperationFilter` unit cases
asserting a relation leaf (and `= me`) compiles to the FK form — 59/59.
- typecheck + lint green (twenty-front, twenty-shared).

Seeding an onboarding view that uses this filter will follow in a
separate PR.
2026-06-02 15:52:08 +02:00
c14422473b i18n - translations (#21154)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 15:36:10 +02:00
120793f69f fix: block self-impersonation in admin panel (#21130)
## Issue
- From Settings -> Admin Panel -> Workspace -> Members, impersonating
the currently logged-in user still issued an impersonation login token.
Token exchange produced invalid impersonation JWTs
(`impersonatorUserWorkspaceId === impersonatedUserWorkspaceId`). JWT
validation then failed with `User cannot impersonate themselves`,
leaving the app in an endless loading state until cookies were cleared.
- Closes #21086

## Approach
I was first thinking of to only hide the impersonate button for the
logged-in user in the admin, since they can not click what isn’t shown
(as I thought it was just a frontend issue).

But that was not enough:
- The `impersonate` mutation can still be called directly (GraphQL
client, scripts, devtools).
- Before this fix, the mutation could succeed and only fail later at JWT
validation, which led to invalid tokens and a broken session.

So the PR does both:
- Frontend: hide/disable self-impersonation in the UI and avoid
reloading on failed token exchange (UX).
- Backend: reject self-impersonation in `ImpersonationService` and at
token exchange (enforcement, fail fast before bad tokens).

Hiding the button is the right product behavior; the backend change is
what makes the rule real and safe.

## How to test
Manual:
- Log in as a user with admin impersonation.
- Go to Settings -> Admin panel -> Workspace -> open your workspace ->
members.
- Confirm your row has no Impersonate button; other members still do.
- Open Admin Panel -> User for yourself -> confirm no impersonate
button.
- Open Settings -> Members -> your own member profile -> confirm no
Impersonate action.
- Impersonate another member -> should work as before

Automated: `npx jest impersonation.service.spec`

### Before:
<img width="830" height="413" alt="Screenshot 2026-06-02 122224"
src="https://github.com/user-attachments/assets/46f38a74-8bd6-4ffa-b749-500ce18314f1"
/>

### After:
<img width="795" height="369" alt="Screenshot 2026-06-02 122333"
src="https://github.com/user-attachments/assets/62ece4a8-d38b-4f91-817f-792ff49b146b"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 13:15:44 +00:00
Raphaël BosiandGitHub 4d520a312f Allow functional iframes in front components while blocking sandbox escapes (#21145)
Fixes https://github.com/twentyhq/twenty/issues/19899

Front component iframes were previously forced to `sandbox=""`, which
fully locks them down: no scripts, no forms, no popups. That broke any
legitimate embedded content (maps, widgets, embeds) developers tried to
render.

But we can't just trust the app-provided sandbox value either: tokens
like allow-same-origin or allow-top-navigation would let a malicious
embed escape the sandbox and hijack the host Twenty tab.

- Add `sanitizeIframeSandbox`, which keeps the iframe useful while
enforcing security:
applies a safe default (allow-scripts allow-forms allow-popups) when no
sandbox is set
always forces allow-scripts so embeds work
- strips dangerous tokens (`allow-same-origin`, all
`allow-top-navigation`*, `allow-popups-to-escape-sandbox`),
case-insensitively
- Wire it into `createHtmlHostWrapper` so every `iframe` rendered by a
front component is sanitized.
- Add unit tests for the sanitizer and Storybook interaction tests
asserting dangerous sandboxes are stripped.
2026-06-02 13:01:18 +00:00
Rashad KaranouhandGitHub ea84aabe4c chore(twenty-partners): refine design-doc skill doctrine (#21151)
## Summary

Iterative refinements to the partner design-doc doctrine after running
it on a second lead (TADA) and reviewing output side by side. Touches
only the `twenty-partner-design-doc` skill files (doctrine + Claude Code
wrapper); no runtime / app code.

**What changed**

- **Flag system:** emoji + short text label pairs only (`🔮 inf.`, **
open**, **⚠️ heavy**, **🛑 blocker**). Replaces the prior text-tag-only
system; scannable, unambiguous.
- **Section structure:** split into **Required** (always present) and
**Conditional** (Views, Automations, Integrations, Reporting). Include
conditional sections only when the client grounded them in the source.
Number sequentially, no gaps.
- **No filler placeholders:** banned `X was not named` / `left out on
purpose` lists in body sections. Unknowns belong in Open questions, not
as their own section or bullet.
- **Functional cross-refs:** every `§N` reference is now a markdown
anchor link `[§N](#n-section-slug)`, so a partner skimming the doc can
navigate. Bare `§N` is banned.
- **Bullets and tables over paragraphs**, with **Open questions** kept
as a numbered list (so the partner can read items 1, 2, 3 with the
client).
- **Views & navigation** rendered as a tight `Surface | Shows |
Audience` table. No view-type column — table / kanban / page layout is
the partner's call, not a scoping decision.
- **Data-model table** gains a `Source` column (`client` / `inf.`) for
at-a-glance fact-vs-inference visibility.
- **Business decisions over technical mechanics:** cut SDK / runtime
internals that don't move the quote (Docker version, OAuth flavour,
auto-system relations, env-var names, CI/CD workflow detail).
- **Common-mistakes table** updated with rows for the new rules.
- **SKILL.md self-check** expanded so the wrapper enforces all of the
above before saving.

## Test plan

- [ ] Re-read doctrine end-to-end for internal consistency
- [ ] Verify the four canonical emoji + text pairs appear and no stray
emoji flags remain
- [ ] Confirm Required vs Conditional structure is internally consistent
(no section listed in both)
- [ ] Confirm functional-cross-ref rule appears in both Rules and
Formatting and is reflected in the SKILL.md self-check
- [ ] Confirm Views & navigation entry mandates the three-column table
and bans a Type column
- [ ] Confirm Common-mistakes table covers each new rule
2026-06-02 12:47:56 +00:00
3a67b35486 i18n - translations (#21149)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 14:40:31 +02:00
939e0b350e i18n - translations (#21148)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 14:32:17 +02:00
Félix MalfaitandGitHub c18f8d6cf7 Always show Favorites section and add favorites via the side panel (#21087)
## What & why

The left-sidebar **Favorites** section was hidden whenever the user had
no favorites, so it was effectively undiscoverable — and personal
favorites could only be created via the record-level "Add to favorites"
action or drag-drop.

This PR:
- **Always shows the Favorites section**, with an empty-state **"Add a
favorite"** call-to-action.
- Adds a **"+" on the Favorites header that opens the same "New menu
item" side panel** the Workspace section already uses, so users can add
personal **Objects, Views, Records, Links and Folders** directly from
the sidebar.

## How

Favorites and workspace navigation are the same `NavigationMenuItem`
entity (a personal favorite simply has `userWorkspaceId` set). Rather
than build a separate favorites-only flow, the shared add/edit
side-panel subsystem is made **section-aware**
(`NavigationMenuItemSection = 'workspace' | 'favorite'`):

- a new `navigationMenuItemEditSectionState` atom records which section
the panel is operating on;
- a new `useNavigationMenuItemEditController` forks persistence — the
**workspace** section stages changes in the draft (saved on
layout-customization exit), while the **favorite** section
creates/updates/deletes personal items **immediately** with
`userWorkspaceId = current member`. This mirrors the existing
`useHandleNavigationMenuItemDragAndDrop` fork.

The existing add/edit hooks, pickers and title editors were rerouted
through the controller and a section-aware items hook, so they work for
both sections with no behavior change to the workspace flow.

**Backend: no changes** — `canUserCreateNavigationMenuItem` already
authorizes personal navigation menu items of every type for any
authenticated user.

## Decisions & tradeoffs

- **Folder button → unified "+":** the folder-only header button is
replaced by the single "+" (Folder is one of the panel's options),
matching the Workspace section. This removed the inline folder-create
code path.
- **Click-to-add only in v1:** dragging items from the panel directly
into Favorites is deferred — those drag handles are disabled in the
favorite section (the drag path is hardwired to workspace layout mode),
with a defense-in-depth no-op in the drop handler.
- **Persist-on-commit:** favorite title/URL edits hit the network once
on blur/enter, never per keystroke.
- **Personal color edits** change only the favorite's own color, never
the shared object metadata (that remains a workspace-customization
behavior).
- The change touches ~37 files because it generalizes the shared
subsystem rather than duplicating it; net diff is slightly negative
(+605 / −636).

## Testing

- `npx nx typecheck twenty-front` — passes
- `npx nx lint twenty-front` (oxlint + oxfmt) — passes
- `navigation-menu-item` unit tests — pass (incl. an updated
`computeInsertIndexAndPosition` test covering personal items)
- Manual end-to-end walkthrough still recommended before merge.
2026-06-02 14:28:10 +02:00
Félix MalfaitandGitHub 431f6ae98f feat(settings): move settings chrome into a single rounded card (#21131)
## What

Replaces `SubMenuTopBarContainer` with a settings-specific
`SettingsPageLayout` that puts the whole page chrome — breadcrumb,
centered title, actions, an optional secondary bar (tabs or wizard
step), and the 760px body — inside **one rounded card**, with
`SidePanelForDesktop` as a sibling. Title, tabs and body content share
one centered vertical axis at every card width.

Supersedes #21122. One PR, no feature flag.

## New components (`@/settings/components/layout/`)

- **SettingsPageLayout** — owns the rounded card + side-panel sibling,
`useCommandMenuHotKeys`, mobile command menu
- **SettingsPageHeader** — breadcrumb · centered title · actions in a
symmetric `1fr auto 1fr` grid (symmetric padding throughout)
- **SettingsSecondaryBar** — the secondary row, bracketed by top +
bottom borders
- **SettingsTabBar** — centered tabs reusing `activeTabIdComponentState`
+ `TabListFromUrlOptionalEffect` for URL-hash sync (does not touch the
shared `TabList`)
- **SettingsWizardStepBar** — back arrow · "N. Label" · optional
trailing slot

## Migrations

- Bulk rename across ~80 call sites (`SubMenuTopBarContainer` →
`SettingsPageLayout`); old component deleted.
- 5 tab pages (AI, APIs & Webhooks, Applications, Members, Role) + the
Data Model object-detail page render their tabs in `secondaryBar`
(object-detail keeps "See records" / "New Field" in the header actions).
- The 2 role object-level steps render the wizard step bar with working
back navigation.
- Accounts consolidated into **General / Emails / Calendars** tabs;
standalone `SettingsAccountsEmails` / `SettingsAccountsCalendars` pages
+ routes + stories removed. `SettingsPath.AccountsEmails` /
`AccountsCalendars` now resolve to `accounts#emails` /
`accounts#calendars`, so existing `getSettingsPath()` links deep-link to
the right tab via the existing hash sync — no call-site changes.

## Verification

- `nx typecheck twenty-front` and `nx lint twenty-front` both clean.
- Browser (logged-in workspace): title / tab / body / card centers align
on a single axis at multiple widths — width-invariant, so alignment
holds when the AI side panel (a sibling) shrinks the card. Rounded card
with even gaps on all four sides; tab row bracketed by two 1px lines;
no-tab pages render header → body with no lines; wizard back navigation
works; `…/accounts#emails` opens the Emails tab.

The shared `PageHeader` and `TabList` are untouched. The settings side
panel itself isn't wired to open yet — that's a follow-up PR.
2026-06-02 14:22:57 +02:00
Rashad KaranouhandGitHub e721ebe300 chore(twenty-partners): bump app version to 0.3.3 (#21140)
Bumps the `twenty-partners` SDK app version 0.3.2 → 0.3.3 so `main`
tracks what's deployed to prod.

This is the deploy version for the partner-app changes that just landed:
marketplace `partnerScope` exposure (#21126), the
`submit-partner-application` endpoint + new Partner categories +
migration (#21040), the marketplace card rebind (#21127), and the signup
wizard (#21039).

No code changes — version bump only.
2026-06-02 11:42:06 +00:00
2048efb75d fix(record-table): keep column header dropdown open after Move Left/Right (#21015)
Fixes #20999

## Summary

Fixes a UX issue where clicking **Move left** or **Move right** in the
column header
dropdown immediately closed the menu, forcing users to reopen it for
every single move.

## Problem

`handleColumnMoveLeft` and `handleColumnMoveRight` both called
`closeDropdownAndToggleScroll()` unconditionally at the top of their
handlers — before
even checking `canMoveLeft` / `canMoveRight`. This immediately set the
Jotai atom
`isDropdownOpenComponentState` to `false`, unmounting the dropdown.

Since move actions are **repeatable** — a user might want to shift a
column several
positions — they were forced into a frustrating loop: click header →
click move → click
header → click move → repeat for every step.

## Fix

Removed the two `closeDropdownAndToggleScroll()` calls from the move
handlers in
`RecordTableColumnHeadDropdownMenu.tsx`.

```diff
  const handleColumnMoveLeft = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveLeft) return;
    moveTableColumn('left', recordField.fieldMetadataItemId);
  };

  const handleColumnMoveRight = () => {
-   closeDropdownAndToggleScroll();
-
    if (!canMoveRight) return;
    moveTableColumn('right', recordField.fieldMetadataItemId);
  };
```

All other handlers — **Filter, Sort, Hide** — are untouched and still
close the dropdown
correctly, since those are one-shot or navigation actions.

## Changes

| File | Change |
|---|---|
| `RecordTableColumnHeadDropdownMenu.tsx` | Remove 2
`closeDropdownAndToggleScroll()` calls from move handlers |
| `RecordTable.stories.tsx` | Add `HeaderMenuStaysOpenAfterMoveRight`
regression story |

## Testing

**Storybook interaction test** — `HeaderMenuStaysOpenAfterMoveRight`:
clicks "Move right" then asserts the menu is still visible.

**Manual checklist:**
- [x] Move right → menu stays open
- [x] Move right again → column moves again, menu still open
- [x] Move left → menu stays open
- [x] Move rightmost column → "Move right" disappears, menu stays open
showing "Move left"
- [x] Filter → menu closes *(unchanged)*
- [x] Sort → menu closes *(unchanged)*
- [x] Hide → menu closes *(unchanged)*
- [x] Click outside → menu closes *(unchanged)*
- [x] Escape → menu closes *(unchanged)*
- [x] TypeScript: zero new errors (`tsc --noEmit`)

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 11:23:26 +00:00
Raphaël BosiandGitHub b2539f5b6a Prevent conditional availability variables from being used at runtime (#21110)
Fixes https://github.com/twentyhq/twenty/issues/21094

Conditional availability variables (`objectMetadataItem`,
`numberOfSelectedRecords`, `objectPermissions`, operators like
`everyEquals`/`none`, etc.) are compile-time-only constructs used in
`conditionalAvailabilityExpression`. They were previously exported from
`twenty-sdk/front-component`, which let developers mistakenly import
them into runtime component code where they have no value.

- Move conditional availability variables from
`twenty-sdk/front-component` to `twenty-sdk/define`.
- Add a build-time manifest validation
(validate-conditional-availability-usage) that fails the build if these
variables are imported/used outside of
`conditionalAvailabilityExpression`.
- Update the github-connector example app to register commands via
dedicated *.command-menu-item.ts files instead of inline command config
in front components.
- Update docs (all locales) and test mocks to reflect the new import
paths.
2026-06-02 11:22:38 +00:00
2375c2f59c i18n - translations (#21141)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 13:35:06 +02:00
Charles BochetandGitHub 58907b733c feat(logic-function): add LIVE / PREBUILT execution modes (#20873)
## Summary

### Why

1. Sending the code to the lambda (~1Mb usually) is heavy on network and
results to a constant traffic of ~30Mb/s on AWS which results into TB of
network data every month
2. eval(1MB of code) is not that fast, it's heavy on memory and CPU on
lambda side

### High level

Adds two execution modes for logic functions, gated behind the new
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` workspace feature flag (off
everywhere by default):

- **LIVE** (current behavior, preserved bit-for-bit): the compiled
bundle is read from object storage and shipped in every Lambda invoke
payload. Used for fast iteration in the workflow editor / Settings test
runs.
- **PREBUILT** (new): the bundle is installed onto the per-function
Lambda alongside the unified executor, and invocations carry only `{
params, env, handlerName }` — saving JSON payload egress and warm-start
`import()` cost on every call.

### Key design choices

- **Unified Lambda handler** (`constants/executor/index.mjs`) dispatches
at runtime: `event.code` present ? LIVE (write to `/tmp`, dynamic
import) : `import('./prebuilt-logic-function.mjs')`. Both code paths
always coexist on the deployment package, so the same Lambda can serve
either mode without redeploying.
- **Install runs inside the `validateBuildAndRun` migration pipeline**,
not at execute time. `Create/UpdateLogicFunctionActionHandlerService`
calls `driver.installPrebuiltBundle` when `executionMode` flips
LIVE?PREBUILT or `checksum` changes while PREBUILT, gated on
`isBuildUpToDate=true` and a fresh checksum.
- **Strict execute, no reconciliation**:
`LogicFunctionExecutorService.execute` resolves `effectiveExecutionMode`
(caller override > feature flag > entity column). For PREBUILT it asks
the driver `getInstalledBundleChecksum` (Lambda `twenty:bundle-checksum`
tag for AWS, sidecar file locally) and throws
`LOGIC_FUNCTION_PREBUILT_BUNDLE_NOT_INSTALLED` on mismatch.
- **Feature flag gates every side effect**: with the flag off the
executor forces LIVE, the action-handler install hooks bail before AWS,
and workflow activation does not flip the mode. Rollback is just turning
the flag off.

### Lifecycle

- New workflow CODE step ? `LIVE`, no install.
- Workflow activated ? build + activation flips `executionMode=PREBUILT`
? action-handler installs the bundle + sets the Lambda tag.
- Draft from active version ? duplicated logic function reset to `LIVE`.
- App install ? manifest converter sets `PREBUILT`, create-action
handler installs.
- Test runs (`executeOneFromSource`, workflow editor) pass
`executionMode=LIVE` explicitly.

### Observability

`[lambda-timing]` log lines now include `effectiveExecutionMode` and
`payloadBytes`; the action handler logs `install_duration_ms` for each
install.

## Test plan

- [x] `npx nx typecheck twenty-server` ? passes
- [x] `npx oxlint --type-aware` on all changed files ? 0 warnings, 0
errors
- [x] `npx nx test twenty-server` ? 588 suites / 5009 tests pass (no
regressions vs main)
- [x] New unit suite `flat-logic-function-validator.service.spec.ts` ?
9/9
- [x] Existing
`workflow-version-step-operations.workspace-service.spec.ts` ? 8/8
(verified the new token-based DI avoids a circular-import regression)
- [x] Snapshot for
`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY` updated
to include `executionMode`
- [x] Integration suite `logic-function-execution.integration-spec.ts`
extended to assert `executionMode=LIVE` on newly-created functions and
continues to exercise the LIVE happy path
- [ ] Manual staging rollout: flip
`IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED` per workspace, observe
`[lambda-timing]` `payloadBytes` drop + `install_duration_ms`, then ramp
in prod.
2026-06-02 11:14:39 +00:00
1ae00d6753 fix(ai): correct find-records tool description (top-level filter fields) (#21109)
## Problem

The AI `find_<object>` tool builds its input schema with
`generateFindToolInputSchema`, which **spreads field filters at the args
root** (alongside `limit`/`offset`/`orderBy`/`and`/`or`/`not`).
`tool-executor.service.ts` then maps the raw model args to a filter
with:

```ts
const { limit, offset, orderBy, ...filter } = args;
```

The zod schema is only used to generate the JSON schema *shown* to the
model (`z.toJSONSchema(...)`) — it is **never used to validate the args
coming back**. So when the model emits a bare operator where a field
name belongs, e.g. `{ ilike: "Foreman" }`, it passes straight to the
query runner, which throws and burns a retry mid-turn:

```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "eq" field.
```

Two contributing faults:
1. **The tool description actively misleads the model** — it says ``use
filter: { id: { eq: "record-id" } }``, a `filter` wrapper the
root-spread schema doesn't have, inviting the malformed shape.
2. **No server-side validation** — invalid root keys reach the query
runner instead of being rejected against the advertised contract.

## Fix

1. **`FindRecordsService` prunes invalid filter keys before querying.**
Using the same filter shape the tool schema advertises
(`generateRecordFilterSchema(...).filterShape`), it drops any key that
is neither a real field nor a logical operator (`and`/`or`/`not`),
recursing through `and`/`or`/`not`. A model that sends `{ ilike:
"Foreman" }` now gets a valid (empty) filter rather than an exception.
Extracted as a pure, unit-tested util `pruneFilterToAllowedKeys`.
2. **Corrected the `find_<object>` tool description** to describe the
real top-level-field shape and explicitly warn against a `filter`
wrapper and bare root operators.

## Test

`__tests__/prune-filter-to-allowed-keys.util.spec.ts` covers: valid
filters untouched, bare root operators dropped, valid siblings
preserved, `and`/`or`/`not` recursion, and non-object input.

## Notes

- Defensive for all `FindRecordsService` callers; `find_one` (`{ id: {
eq } }`) and workflow find-records pass valid filters and are
unaffected.
- Companion to #21106 (RICH_TEXT composite filters). Both surfaced from
the same `"Tom Foreman's notes"` AI-chat repro; this PR addresses the
root-level-operator half.

---------

Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
2026-06-02 10:58:13 +00:00
e1a00ea42f fix(twenty-front): enable text selection for display-mode fields (#21068)
## Description
This PR resolves a usability issue where scalar field values (emails,
phone numbers, dates, IDs, text, etc.) rendered in display-mode or
read-only mode in the record detail side panel could not be highlighted,
selected, or copied natively with the cursor.

## Root Cause
Both `RecordInlineCellContainer` and
`RecordInlineCellHoveredPortalContent` wrapper elements had
`user-select: none;` hardcoded in their styled-component definitions.
This styling propagated down to all nested display widgets, locking
their content and preventing native text selection.

## Changes
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellContainer.tsx` to use `user-select: text;` instead of
`none;`.
- Updated `StyledInlineCellBaseContainer` in
`RecordInlineCellHoveredPortalContent.tsx` to use `user-select: text;`
instead of `none;`.

These changes restore natural browser text selection capabilities for
record detail widgets without altering interactive edit-mode behaviors.

## Verification
- Verified styling changes.
- Tested locally to ensure that text highlighting and copy-pasting
function correctly when dragging over read-only fields.

Closes #21056

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-02 10:53:00 +00:00
Rashad KaranouhandGitHub 4f47885054 feat(twenty-partners): submit-partner-application HTTP logic function (#21040)
## Summary

Adds a public `POST /partner-applications` HTTP logic function on the
twenty-partners SDK app that receives applications from the website
wizard and idempotently upserts the Partner / Person / Company graph in
the partners workspace. Also introduces the validated **Category**
taxonomy on `partnerScope` (additive, prod-safe) plus the legacy→new
migration tooling.

Companion PR (website side): #21039

### Logic function
- `defineLogicFunction({ httpRouteTriggerSettings: { path:
'/partner-applications', httpMethod: 'POST', isAuthRequired: false,
forwardedRequestHeaders: ['x-application-secret'] } })`.
- Authenticates via shared-secret header (`X-Application-Secret` ↔
`PARTNER_APPLICATION_SECRET` workspace variable). Twenty's
`isAuthRequired: true` only accepts user-session JWTs, so the handler
enforces auth itself.
- Idempotent upsert keyed on `Person.emails.primaryEmail`:
  - missing email → create Company → Person → Partner
  - existing Person, no Partner → create Company + Partner, link
- existing Person + Partner → update Partner fields; preserve
staff-owned columns (`validationStage`, `reviewed`, `ranking`,
`partnerTier`, `lastMatchAt`) by omitting them from the update
- Create-time defaults preserved on resubmit: `slug =
slugify(companyName)` ("YC Agency" → "yc-agency"), `reviewed = false`,
`partnerTier = 'NEW'`.
- Currency conversion to `{ amountMicros, currencyCode: 'USD' }` for
`hourlyRate` + `projectBudgetMin`.

### Categories (`partnerScope`) — additive, prod-safe
- Adds 5 validated category options — `ADVISORY`, `SOLUTIONING`,
`DEVELOPMENT`, `HOSTING`, `SUPPORT` — to the `partnerScope` MULTI_SELECT
**without removing** the legacy options (there is production data on
them). Field relabeled **"Categories"**. The website form only emits the
new values.
- **Migration tooling** (run deliberately, *not* in CI):
`scripts/migrate-partner-scope.ts` remaps existing records legacy→new —
dry-run by default, `MIGRATE_APPLY=1` to write, two-pass
(collect-then-apply, no mutate-while-paginating).
`scripts/partner-scope-map.ts` is the single mapping source;
`import-from-tft.ts` now routes imported scope through it so the TFT
import never re-introduces retired values. Removing the legacy options
is deferred until after the migration has run + been verified.

### applicationNotes
- New `applicationNotes` TEXT field holds the wizard's single free-text
"anything else" note (the handler passes it through directly).
`deploymentExpertise` was dropped from the handler
input/validation/builders (the column is retained for now, pending the
same migration cleanup).

### Application variable
- Declares `PARTNER_APPLICATION_SECRET` with `isSecret: true` so each
workspace sets the value via Settings → Apps → Twenty Partners →
Variables. Twenty encrypts at rest and merges the decrypted value into
the handler's `process.env` at execution time (workspace value wins over
container env).

### Code quality (from review)
- One shared `slugify` (`scripts/slugify.ts`, the import's algorithm)
used by both the handler and the import, so the `slug` identity key
can't diverge across paths.
- Unit-test tier: `vitest.unit.config.ts` (no `globalSetup`) + `yarn
test:unit`, so the pure `mapLegacyScope` test runs without a live server
(the integration suite stays server-backed).

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

### Tests
- Integration tests against a local Twenty workspace:
missing-/wrong-secret auth rejections, create flow (asserts slug +
`reviewed: false` + `partnerTier: 'NEW'`), update-on-resubmit +
staff-column preservation, new category values stored,
`applicationNotes` stored, bad-input shape.
- Pure `mapLegacyScope` unit test via `yarn test:unit` (no server).

## Test plan

- [ ] Install / upgrade the app on the target workspace; set
`PARTNER_APPLICATION_SECRET` in Settings → Apps → Twenty Partners →
Variables
- [ ] `curl -i -X POST <workspace-url>/s/partner-applications -H
'X-Application-Secret: <secret>' -H 'Content-Type: application/json' -d
'{"firstName":"Test","lastName":"User","email":"test@example.com","companyName":"YC
Agency","partnerScope":["ADVISORY"],"applicationNotes":"hi"}'` →
`HTTP/1.1 201` + `{"ok":true,"created":true,"partnerId":"..."}`
- [ ] Partner record shows `name: "YC Agency"`, `slug: "yc-agency"`,
`validationStage: APPLICATION`, `reviewed: false`, `partnerTier: 'NEW'`,
`partnerScope: ["ADVISORY"]`, `applicationNotes: "hi"`
- [ ] Re-curl same email with `city: "Paris"` → `created: false`,
`Partner.city` updated, staff-owned columns untouched
- [ ] Wrong / missing secret → `200` +
`{"ok":false,"reason":"unauthorized"}`
- [ ] `yarn test:unit` green (no server); `yarn migrate:partner-scope`
dry-run lists any legacy→new remaps without writing
2026-06-02 10:35:28 +00:00
Rashad KaranouhandGitHub 7e034f711f feat(website): surface partner Categories (partnerScope) in marketplace, drop deploymentExpertise facet (#21127)
## What

Rebinds the marketplace's expertise facet from `deploymentExpertise`
(Cloud / Self-host) to **`partnerScope`** — the five partner Categories:
Advisory & Discovery · Solutioning · Custom Development · Hosting &
Infrastructure · Training & Adoption.

Moves the card chip, the profile facts row, the dropdown filter, the
`?categories=` URL param, and the API-boundary normalization onto
`partnerScope`. The standalone Cloud/Self-host facet is **dropped**
(hosting is now the `HOSTING` category), per the harmonization decision.

## Depends on

- The app exposing `partnerScope` — companion app PR #21126.
- The new `partnerScope` options + data migration — signup app PR
#21040.

## Tests

TDD red→green on: `filter-partners`, both API normalizers,
`filter-url-helpers`, `PartnerCard`, `use-filter-state`. 53/53 pass;
typecheck + lint + format clean.

## Merge order (we'll decide)

Independent diff. Suggested last of the four, after the signup PRs
(#21039 / #21040) and the app PR (#21126). Run `lingui:extract` once
after #21039 merges so the `.po` files don't conflict twice. Deploy the
app + migrate before the website ships.
2026-06-02 10:32:54 +00:00
94d2e386e8 i18n - website translations (#21135)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 12:43:26 +02:00
Rashad KaranouhandGitHub a3557373e6 feat(twenty-partners): expose partnerScope on list + by-slug endpoints (#21126)
## What

Adds `partnerScope` (the partner **Categories** multi-select) to the
output of the two public partner endpoints:
- `list-available-partners` (`/s/partners`)
- `get-partner-by-slug` (`/s/partner-by-slug`)

Additive only — `deploymentExpertise` is kept, so existing consumers
(the current live marketplace) are unaffected.

## Why

Part of the partner marketplace rework. The website marketplace
(companion branch `rk-rework-marketplace-cards`) consumes `partnerScope`
to show/filter partner Categories. The new options + migration live in
the signup app PR #21040.

## Merge order (we'll decide)

Independent diff — can merge in any order. Couplings to keep in mind:
- **Version line:** this branch and #21040 both bump the app
`package.json` version; whoever merges second re-bumps.
- **Deploy (not merge):** the partners app is deployed manually. Deploy
the final combined app (this + #21040) and run `yarn
migrate:partner-scope:prod` **before** the website is deployed.
2026-06-02 10:23:51 +00:00
d0e0e27035 [Website] Partner application wizard + logic-function handover (#21039)
## Summary

Replaces the single-screen partner-application modal with a **4-step
wizard** on the public form, and points the route's upstream at the new
`submit-partner-application` HTTP logic function in the twenty-partners
SDK app.

After design review, the Expertise step landed on the validated
**Category + Skills** model: a small set of *stable* macro categories
the partner operates in, plus a *free, semi-structured* Skills field for
the concrete things that differentiate them (React, SAP, Shopify, …).

Companion PR (partners-app side): #21040

## ⚠️ Deployment notes

Before this can ship to prod, the website worker needs a new env var:

- **Add `PARTNER_APPLICATION_SECRET`** to the deploy config at
https://github.com/twentyhq/twenty-infra/tree/main/cloudflare/website.
Without it the route returns `503` ("Partner application endpoint is not
configured.").
- The value must **match** the `PARTNER_APPLICATION_SECRET` workspace
variable set in the partners workspace UI (Settings → Apps → Twenty
Partners → Variables) — that's how the handler authenticates the
incoming `X-Application-Secret` header.
- `PARTNER_APPLICATION_WEBHOOK_URL` also needs repointing from the TFT
webhook to the logic-function URL
(`https://partner.twenty.com/s/partner-applications` or equivalent) at
the same time.

## Wizard

- 4 steps inside `Modal.Root`: **Identity → Profile → Expertise →
Commercials**. Step-dot indicator, per-step required-field gating, reset
on close. The big serif hero shows **only on step 1**; later steps use
the compact `STEP n OF 4 · NAME` strip to reclaim vertical space.
- **Profile** captures Type of team (Solo/Agency), LinkedIn, City,
Country, Languages. Country uses the searchable Select (placeholder-only
label).
- **Expertise = Category + Skills + Notes:**
- **Category** — multi-select cards over 5 macro categories (`ADVISORY`,
`SOLUTIONING`, `DEVELOPMENT`, `HOSTING`, `SUPPORT`), each with a
one-line description + examples. (Replaces the old draft `partnerScope`
enum; the backend keeps the field name — see #21040.)
- **Skills** — free tag input with a clickable suggestion row + keyboard
autocomplete (↑/↓/Enter/Esc) and "add your own". Empty by default.
- **Notes** — one free textarea (merges the former `workspaceUrl` +
`customerReferences`), reviewed manually.
- `deploymentExpertise` removed from the form (covered by the Hosting
category).
- **In-modal success view** on submit ("Thanks, / we'll be in touch!")
with a Close button — replaces the old silent close.
- Removes the partners-page "Which partner program is right for you?"
three-cards section.

## Design-system primitives

- **`Form.Select`** — searchable popup whose dropdown is **portaled to
`<body>`** (fixed, anchored to the trigger, flips up, height-capped) so
the modal's `overflow`/`transform` can't clip it; pointer events are
stopped so clicking inside it doesn't dismiss the dialog.
- **`Form.TagInput`** — optional `suggestions` prop adds the suggestion
row + autocomplete menu (used by Skills); behaviour unchanged when no
suggestions are passed.
- **`CategoryCardSelect`** — compact multi-select cards.
- `Form.MultiSelect`, `Form.Currency`.

## Validation & payload

- **Single validation source:** client and server share Zod field
schemas (`partner-application-field-schemas.ts`). The reducer validates
via those instead of hand-rolled regexes, so client and server agree by
construction (e.g. both reject non-TLD URLs).
- **Typed request body:** `buildPartnerApplicationRequestBody(state)`
returns a typed `PartnerApplicationRequest` (unit-tested);
`handleSubmit` just serializes it.
- Payload is camelCase matching the logic-function input;
`applicationNotes` replaces `workspaceUrl`/`customerReferences`.
- Auth: the upstream call carries an `X-Application-Secret` header
backed by `PARTNER_APPLICATION_SECRET` (handler-enforced — the SDK's
`isAuthRequired` only accepts user-session JWTs, not workspace API
keys). The webhook-URL env uses `z.url()` (not `z.httpUrl()`) so
`http://localhost:2020/...` dev destinations parse.

## Demo

📹 _Screen recording of the wizard end-to-end (open → walk steps → submit
→ Partner record lands):_


https://github.com/user-attachments/assets/7458dd86-e3ff-47b5-9878-0eb134ff38e3

## Tests

- **62 passing** across reducer, Zod schema, route, the new
payload-builder suite, and Form helper suites. `npx tsc` clean, `nx
lint:diff-with-main` clean, Lingui catalogs regenerated (French slots
are a follow-up).

## Test plan

- [ ] `/partners` → "Become a partner" → wizard opens on Step 1 (full
hero)
- [ ] Identity: name / work email / company → Next
- [ ] Profile: pick **Type of team**; search country ("fra" → France);
pick languages → Next (compact header from here on)
- [ ] Expertise: select 1+ **Category** cards; add **Skills** (click a
suggestion, type one + Enter, drive the ↑/↓ autocomplete); optionally
fill **Notes**
- [ ] Country dropdown opens without being clipped by the modal, and
clicking inside it does **not** close the wizard
- [ ] Commercials → Submit → **in-modal "Thanks, we'll be in touch!"**;
Network shows POST `/api/partner-application` `200`
- [ ] Partner record lands with the chosen categories in `partnerScope`,
plus `skills`, `applicationNotes`, `slug` from company, `reviewed:
false`, `partnerTier: 'NEW'`
- [ ] Re-submit same email + different city → Partner updates;
`validationStage`/`reviewed`/`partnerTier` preserved
- [ ] Back/Next preserves entered values; Reset on close; mobile
single-column / chips wrap

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 10:23:31 +00:00
Paul RastoinandGitHub 6a908b7876 Front component s3 redirect (#21116)
# Introduction
Unload the server of the file stream when possible
Also fix inconsistent pipeline exception management

Needs to highly be QA, not sure how the cors will behave here
2026-06-02 09:35:58 +00:00
445c6fe9f6 feat: expose CURRENCY field settings (format/decimals) in shared types (#21090)
## What

Add a `CURRENCY` entry to `FieldMetadataSettingsMapping` (a
`FieldMetadataCurrencySettings` type of `{ format?: 'short' | 'full';
decimals?: number }`) so `FieldMetadataSettings<CURRENCY>` resolves to
the real settings shape instead of `null`.

## Why

The currency **format** (Short/Full) and **decimals** selectors already
ship in the field settings UI and persist through the generic `settings`
jsonb column — they render via
[`CurrencyDisplay.tsx`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx)
reading `settings.format` / `settings.decimals` (added in #12542 and
#16439).

But `twenty-shared` never got a `CURRENCY` entry in the settings
mapping, so `FieldMetadataSettings<CURRENCY>` is `null`. The SDK's
`defineField` derives its types from this mapping, so an app author
cannot set these from code — `universalSettings: { format: 'full',
decimals: 2 }` on a CURRENCY field is a type error, even though the
server stores and the frontend honours it. This aligns the type layer
with the already-shipped runtime behaviour.

## Changes

- `twenty-shared`: add `FieldMetadataCurrencySettings` +
`FieldCurrencyFormat`, wire the `CURRENCY` mapping entry, export
`FieldCurrencyFormat`.
- `twenty-server`: move `CurrencyFieldMetadata` from the
`NotDefinedSettings` assertions to a defined-settings assertion in the
field-metadata entity type test.

No runtime change — the server already accepts and stores these settings
via the generic jsonb column; this only makes them visible to the type
system and the SDK.

## Test plan

- [ ] `npx nx typecheck twenty-shared` / `twenty-server` pass
- [ ] In an app, `defineField({ type: FieldType.CURRENCY,
universalSettings: { format: 'full', decimals: 2 }, ... })` type-checks
and deploys
- [ ] Field renders with 2 decimals in full format, matching the
equivalent UI configuration

> Follow-up (not in this PR): the frontend keeps its own local
`fieldMetadataCurrencyFormat` / `FieldCurrencyFormat`; it could import
the shared `FieldCurrencyFormat` to de-duplicate.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:42:28 +02:00
neo773andGitHub 1ba7a3fa54 fix: lowercase OAuth handle to prevent duplicate connected accounts (#21120)
Google and Microsoft connect flows stored the provider-returned email
verbatim, so a re-auth with different casing (Sam@ vs sam@) missed the
existing-account lookup and created a duplicate. Normalize the handle to
lowercase at extraction, matching the SSO controller.

Note: not doing backfill for now
2026-06-02 09:27:10 +00:00
Félix MalfaitandGitHub 7d7f32b243 docs: remove the self-host cloud providers page (#21134)
## What

Removes the community-maintained **"Other methods"** cloud-providers
page from the self-host docs (it covered Kubernetes/Terraform/Coolify
community deployments).

## Changes

- **Deleted** `developers/self-host/capabilities/cloud-providers.mdx`
and its 13 localized copies (ar, cs, de, es, fr, it, ja, ko, pt, ro, ru,
tr, zh).
- **Removed the slug** from `navigation/base-structure.json` (the source
of truth) and regenerated the derived files via the repo's own
generators (`yarn docs:generate`, `yarn docs:generate-paths`):
  - `docs.json` — nav entries dropped for every locale.
- `twenty-shared/.../DocumentationPaths.ts` —
`DEVELOPERS_SELF_HOST_CAPABILITIES_CLOUD_PROVIDERS` constant dropped
(was unused elsewhere).
- **Removed the "Cloud Providers" card** from the `self-host` overview
pages across all locales.
- **Dropped the dangling redirect**
`/developers/self-hosting/cloud-providers` (its destination no longer
exists).
- Cleared the matching entry from the unused `navigation-schema.json`
for consistency.

Net: 68 line deletions across config (pure removal); no insertions.

## Verification

- `grep` confirms **0** remaining references to `cloud-providers`
anywhere in the repo.
- All touched JSON files parse; `oxlint` on twenty-docs reports 0
errors.
- Generators (not hand edits) produced `docs.json` and
`DocumentationPaths.ts`.

> Note: `mintlify broken-links` can't run to completion on this branch
due to a **pre-existing** MDX parse error in the unrelated
`l/ar/.../contribute/contribute.mdx`; the grep above is the equivalent
guarantee that no link points at the removed page.
2026-06-02 11:17:37 +02:00
5f0096c464 i18n - website translations (#21088)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 10:45:36 +02:00
6ac797a69c fix: REST cursor encoding for nested order_by composite fields (#20974)
## Summary

fix: REST cursor encoding for nested order_by composite fields

Closes #20109

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-02 07:10:20 +00:00
3d6bcc3102 i18n - translations (#21128)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-02 07:30:02 +02:00
Félix MalfaitandGitHub 75df1f3997 chore(settings): address review comments from PR 21072 (#21121)
## Summary

Round through bosiraphael's 31 review threads on the merged PR #21072
(discovery hero + ephemeral playground token). The user asked to apply
each suggestion only where it adds value, so this PR is split into three
buckets.

### Comments (~17 threads)

- Tightened security-rationale / CSS-gotcha / API-doc comments to one or
two factual lines
- Kept (shortened) the comments above `RequireAccessTokenGuard` call
sites — without them a future reader could remove the guard and silently
reopen the escalation hole
- Kept (shortened) the in-memory-only rationale on
`playgroundApiKeyState` for the same reason
- Kept `flex: 1 + min-height: 0` CSS gotcha on `SubMenuTopBarContainer`
— non-obvious and easy to break

### Structure / extraction

- Move `WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS` to its own constants
file (one-export-per-file)
- Split `SettingsAgentToolsTab` and `SettingsAgentToolsTable` across
queries/, hooks/, types/, utils/:
  - `graphql/queries/findManyApplicationsForToolTable.ts`
  - `graphql/queries/findManyMarketplaceAppsForToolTable.ts`
  - `hooks/useSettingsAgentToolsTable.ts` (data loading + index merging)
  - `types/SettingsAgentToolItem|Application|MarketplaceApp`
  - `utils/getToolApplicationId|getToolLink`
- Extract `SettingsAiModelsTab` optimistic mutations into
`hooks/useSettingsAiModelsActions` (handleModelFieldChange,
handleUseRecommendedToggle, handleModelToggle,
handleToggleAllVisibleModels)
- Extract `SettingsAI.handleCreateTool` into `hooks/useCreateTool`
- Drop unnecessary `useMemo` wrappers on `heroTabs` arrays
(SettingsObjects, SettingsLayout)
- Simplify `MenuItemToggle` handler in SettingsAgentSkillsTab:
`onToggleChange={setShowDeactivated}` (no longer wrapping with arrow +
read of stale `!showDeactivated`)

### Hero assets

- Replace placeholder `customize-illustration` with per-page exports
- Rename `layout/customize-illustration-{light,dark}.png` →
`layout/cover-{light,dark}.png`
- Add `cover-{light,dark}.png` for **applications** and **members**
(they were both pointing at the layout placeholder as a TODO)
- Overwrite `data-model/cover-*.png`, `playground/cover-*.png`,
`ai/ai-tools-cover-*.png` with the new exports

## Test plan

- [ ] `npx nx typecheck twenty-front` 
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint twenty-front`  (oxlint + oxfmt, 0 warnings/errors)
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the new hero illustration (light + dark)
- [ ] AI tab: tool list still loads, search + Custom/Managed/Standard
filters still work, "New Tool" still navigates to detail
- [ ] AI tab: Models tab — smart/fast model select, "Use best models
only" toggle, per-model checkboxes, toggle-all all still
optimistic+revert on error
- [ ] Skills tab: "Deactivated" toggle still flips show/hide
- [ ] Webhooks table still uses the 1fr 28px grid
2026-06-02 07:23:14 +02:00
Paul RastoinandGitHub d6b3527552 Public assets server s3 redirection (#21108)
# Introduction
Avoid overloading the server on file streaming
Take profit of the different origin implied by the redirection to the s3
Only concern being the expiration date on a public file which is
acceptable

closes https://github.com/twentyhq/private-issues/issues/483
related https://github.com/twentyhq/private-issues/issues/491
2026-06-01 16:25:58 +00:00
Paul RastoinandGitHub 989b45db15 Strictly type encryption rotation key site maps constants through entity type derivation (#21085)
# Introduction
Followup https://github.com/twentyhq/twenty/pull/21001

Now that the typeorm entities provide grains over their
`encryptedString` value, we can strictly type the sitemaps of the
encrypted string to rotate in case of encryption key rotation and also
the integration tests tests cases
2026-06-01 15:25:58 +00:00
WeikoandGitHub d86e827563 fix: return proper FORBIDDEN GraphQL errors from ApiKeyResolver (#21107)
## Context
CI is broken on main, regression introduced in
https://github.com/twentyhq/twenty/pull/21072

Guard-rejected ApiKey mutations returned malformed GraphQL responses.
RequireAccessTokenGuard (and SettingsPermissionGuard) throw plain
AuthException/PermissionException classes, which are not GraphQLErrors.
ApiKeyResolver had no @UseFilters, so these exceptions were never
translated, they surfaced as request-level errors with no data key
(data: undefined) and a non-FORBIDDEN code, instead of data: null +
FORBIDDEN.

This broke the `createApiKey › should reject a non-ACCESS token even
with API key permission` integration test
(expect(res.body.data).toBeNull() received undefined). The sibling
generateApiKeyToken test passed only because it lives on AuthResolver,
which already declares these filters.

## Fix
Add the standard exception filters to ApiKeyResolver, matching the idiom
used by other guard-protected resolvers
```ts
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
```
2026-06-01 18:08:02 +02:00
Paul RastoinandGitHub 6ad6fcce0f Bump playwright (#21113)
Playwright installation is infinite looping in the ci
seems like to be a global outage
2026-06-01 18:06:55 +02:00
ad47b2972c i18n - translations (#21112)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 17:42:34 +02:00
ba493e5a23 fix: show relation field changes in the timeline (#21052)
## Summary
- Fixes #20970 
- When we changed only a relation field (e.g. company on a person):
- The diff builder skipped all RELATION fields -> empty diff -> no
timeline row.
- If we change company and something else, only the scalar field
appeared; the company change was missing.
- Even with a diff, the UI validated keys by field name (`company`)
while join columns (`companyId`) could be filtered out.

## Solution
- For `MANY_TO_ONE`, compare join column values (`companyId`) and store
the diff under the relation field name (`company`):
```
"company": {
  "before": { "id": "<old-id>" },
  "after": { "id": "<new-id>" }
}
```
- Frontend
- `filterOutInvalidTimelineActivities`: resolve diff keys by field name
or join column via `findFieldMetadataItemByDiffKey`, so relation diffs
are not stripped.
- `EventRelationFieldDiffValues`: resolve related record labels by id;
show only the new value in the row (same pattern as other fields:
Company -> airSlate).
- Tooltip (relations only): on hover, show before -> after with readable
names (e.g. Microsoft -> Apple). Scalar and composite fields (e.g.
Updated by) are unchanged and do not get this tooltip.
- `EventFieldDiff`: route RELATION diffs to the relation renderer; all
other field types keep the existing FieldDisplay behavior.

### What you’ll see
On a person (or opportunity) timeline after changing company:
```
You updated Company → airSlate
(tooltip: Microsoft → Apple)
```

## Test plan
- Change only company on a person -> timeline shows a company update
with names.
- Change company and name in one save -> both appear in the diff.
- Clear company -> row shows Empty; tooltip reflects previous -> empty
if applicable.
- Same we can do for the Opportunities also
- Scalar / ACTOR fields (e.g. Updated by) - no new tooltip; display
unchanged.

## Screenshots

### Before
<img width="527" height="124" alt="Screenshot 2026-05-29 155123"
src="https://github.com/user-attachments/assets/e067f19a-8184-4e50-9cd0-9135e06188b8"
/>
<br><br>
<img width="535" height="181" alt="Screenshot 2026-05-29 155149"
src="https://github.com/user-attachments/assets/3c513a1e-c5c5-4cff-ae1c-5fed62837798"
/>
<br><br>

### After
<img width="564" height="200" alt="Screenshot 2026-06-01 154539"
src="https://github.com/user-attachments/assets/459ad6b4-af4c-4f7a-b749-30762c979627"
/>
<img width="567" height="187" alt="Screenshot 2026-06-01 154555"
src="https://github.com/user-attachments/assets/6bc4711c-afca-43d8-b874-f51fc0f374df"
/>
<img width="563" height="188" alt="Screenshot 2026-06-01 154639"
src="https://github.com/user-attachments/assets/f275b9fd-a1c3-4c4b-9538-8042657eb593"
/>
<img width="556" height="180" alt="Screenshot 2026-06-01 154654"
src="https://github.com/user-attachments/assets/2e614368-8fd7-4c73-8876-2223f2f98e67"
/>
<img width="560" height="237" alt="Screenshot 2026-06-01 154802"
src="https://github.com/user-attachments/assets/0437aef4-7b2c-4d35-a2e9-f617b90a1beb"
/>

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 14:34:54 +00:00
Thomas TrompetteandGitHub 627b488556 Fix else branches not properly skipped in nested if/else workflows (#20938)
## Summary

- Extract `findParentSteps` utility that recognizes IF-ELSE steps as
parents of their branch children (via
`settings.input.branches[].nextStepIds`), used in all parent detection
sites (`shouldSkipStepExecution`, `shouldExecuteStep`,
`shouldFailSafely`, and their iterator variants)
- Centralize next-step resolution in `getNextStepIdsToExecute` via
extracted `getNextStepIdsForIterator` and `getNextStepIdsForIfElse`
utils — Iterator now properly returns loop children as
`nextStepIdsToSkip`/`nextStepIdsToFailSafely` when skipped
- Refactor `skipAndFailSafelyStepsThenContinue` to delegate to
`getNextStepIdsToExecute` instead of duplicating type-specific
propagation logic

Fixes #20934

## Test plan

- [x] New unit tests for `findParentSteps` (7 tests covering IF-ELSE
branch parent detection)
- [x] New IF-ELSE-specific tests added to `shouldSkipStepExecution`,
`shouldExecuteStep`, `shouldFailSafely` test suites
- [x] Updated Iterator skip/fail-safely tests in
`workflow-executor.workspace-service.spec.ts`
- [x] All 300 workflow executor tests pass
- [x] `lint:ci` passes
2026-06-01 17:03:50 +02:00
WeikoandGitHub b9e5ff2065 fix: broadcast timeline activities to live SSE subscriptions (#21104)
## Context
Timeline activities never updated in real time. They were explicitly
excluded from the database-event pipeline
(formatTwentyOrmEventToDatabaseBatchEvent early-returned for the
timelineActivity object), so no SSE event was ever broadcast, and the
frontend timeline only refreshed on mount/manual refetch.

## Implementation
Backend
- Feat: Stop dropping timeline-activity events in
formatTwentyOrmEventToDatabaseBatchEvent.
Instead route them through EntityEventsToDbListener, which publishes
them directly to live subscriptions (but still skipping webhook/audit
handling).
- Fix: Harden ObjectRecordEventPublisher: wrap nested-relation
enrichment in try/catch so a failure broadcasts the event without
relations instead of dropping it (logs a warning).
- Fix: Skip unreadable relation targets in CommonSelectFieldsHelper when
the role lacks canReadObjectRecords, preventing errors while computing
selected fields.
- Fix: Support MORPH_RELATION alongside RELATION in RLS row-level
permission predicate matching (timeline activities use morph targets).

Frontend
- Feat: useTimelineActivities now registers the timeline query with the
SSE system via useListenToEventsForQuery and refetches on incoming
timeline-activity record operations.
- Feat: Add a skip option to useListenToEventsForQuery so the listener
isn't registered when the object has no timeline field.

## Test


https://github.com/user-attachments/assets/ed1d1c66-d6ea-434d-ac9c-9b83d2b78338


Note: "UpdatedBy" seems to be listen to and visible in the timeline
activity summary, this is probably a bug that we want to fix
2026-06-01 16:32:08 +02:00
EtienneandGitHub 381ca32055 Billing - Fix credit upgrade invoice error (#21097)
In createImmediateUpgradeInvoice, the invoice is finalized with
auto_advance: true, which causes Stripe to automatically attempt payment
asynchronously. Then the explicit stripe.invoices.pay(invoice.id) call
races against that auto-payment — if Stripe already paid it, this throws
"Invoice is already paid".

The fix is to finalize with auto_advance: false and keep the explicit
pay call
2026-06-01 13:55:06 +00:00
nitinandGitHub 6029847491 fix navigation item tree breadcrumb active state (#21101)
before - 



https://github.com/user-attachments/assets/9a35e07c-def3-47eb-aab4-0bdcaf302d38



after - 


https://github.com/user-attachments/assets/bf452b5a-7a31-4838-83ca-27cae598ef4d
2026-06-01 13:53:42 +00:00
MarieandGitHub 66afd5a1de Fix array-typed parameters in code/logic-function action forms (#21102)
## Problem

`any[]` type prevented value input:
<img width="544" height="349" alt="Screenshot 2026-06-01 at 14 08 47"
src="https://github.com/user-attachments/assets/956238d0-6fea-4be1-b75a-ab0e6e6424ac"
/>

In the workflow Code action (and the Logic Function action), parameters
typed as any[], string[], etc. rendered as an empty grey box instead of
an "Enter value" text input. After any debounced save, even a properly
initialised array field would also collapse into an empty container.

The Array<T> / ReadonlyArray<T> generic form fell through to a generic
text input by accident (which "looked" right, but for the wrong reason —
no schema info downstream).

## Root causes
Three places treated arrays as plain objects via @sniptt/guards'
isObject (which is true for arrays):

1. WorkflowEditActionCodeFields.tsx — arrays went into the nested-fields
branch; Object.entries([]) is empty → empty container, no placeholder.
2. mergeDefaultFunctionInputAndFunctionInput.ts — recursed into arrays
during merge, turning [] into {}. Triggered on every debounced save, so
the bug surfaced after any edit.
3. get-function-input-schema.ts — only handled T[]
(SyntaxKind.ArrayType); Array<T> (SyntaxKind.TypeReference) was
unrecognised, so the form lost any item-type info.
2026-06-01 13:14:46 +00:00
da5e1152bb i18n - translations (#21103)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-01 14:24:47 +02:00
Félix MalfaitandGitHub b338a7a1d2 feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary

Two intertwined streams of work:

### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.

### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.

- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.

### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.

## Test plan

### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active

### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200

### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px

### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
2026-06-01 14:16:02 +02:00
nitinandGitHub 6e00a122c6 fix(kanban): contain checkbox hover reveal within card bounds (#21100)
follow up to https://github.com/twentyhq/twenty/pull/20455

before - 


https://github.com/user-attachments/assets/e5a8a328-81ec-4dc4-8e54-1a54cf252135


after - 



https://github.com/user-attachments/assets/61cbb856-564c-487f-81e5-e27adc4a0d2d
2026-06-01 12:20:40 +02:00
4dff30f676 Twenty server:Fix REST pagination issues (#20980)
Fixes #20109 

The entry was repeating because in the database we store DateTime fields
with microsecond precision (timestamptz), but when JS parses timestamptz
into a Date object it only keeps millisecond precision.

### Example
If previous cursor was:
```
{
    name: "Quick Lead",
    createdAt: "2026-05-21T15:33:00.708Z",
}
```
The resulting query look something like:
```
...
WHERE (
  "workflow"."name" > "Quick Lead"
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" > "2026-05-21T15:33:00.708Z"
  )
  OR (
    "workflow"."name" = "Quick Lead"
    AND "workflow"."createdAt" = "2026-05-21T15:33:00.708Z"
    AND "workflow"."id" > "8b213cac-a68b-4ffe-817a-3ec994e9932d"
  )
)
```
So, when comparing the 2nd condition the `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` would always result to true because in db
the data for createdAt is `2026-05-21 21:03:00.708 +0530` which will
always be greater than `2026-05-21T15:33:00.708Z`
The second condition `"workflow"."createdAt" >
"2026-05-21T15:33:00.708Z"` always evaluates to true, because the value
actually stored in the DB for createdAt is something like `2026-05-21
21:03:00.708264 +0530`, which is always greater than
`2026-05-21T15:33:00.708Z` in the cursor. The row used to generate the
cursor therefore reappears on the next page.

### My solution
Truncate the column to milliseconds in the comparison so both sides have
the same precision: `date_trunc('milliseconds', ${fieldReference})`.

For the issue of nested sorting filters, when ordering by a composite
field (e.g. `createdBy.name`), `encodeCursor` stored the entire
composite object (`source`, `workspaceMemberId`, `name`, `context`). The
where-condition builder later iterated those sub-keys and threw "Invalid
cursor" because only name had an orderBy direction.

P.S: Duplicate of #20867 because last fork got polluted.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-06-01 08:50:14 +00:00
71c377484e fix(front): keep app variable cache in sync after update (#20861)
Updating an application variable in Workspace / Applications / <App> /
Settings persisted server-side but the Apollo cache kept the old value.
Switching tabs unmounted the settings tab, and remounting reseeded the
input from the stale cache — only a full refresh showed the new value.

Mutation now writes the new value into the ApplicationVariable entity
via cache.modify, so FindOneApplication reflects the change immediately.
Hook + table updated to pass the variable id through.

Adds a hook test that pre-seeds the cache and asserts the cached value
after the mutation.

NOTE: saving the plain value in Apollo's cache might not be the best
approach here

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:14:28 +00:00
26906951b3 fix(twenty-sdk): minify front-component bundles & set NODE_ENV=production in deploy build (#20937)
## Summary

`twenty deploy` (and `twenty build`) currently ship front-component
`.mjs` bundles **unminified**, with `process.env.NODE_ENV` undefined at
build time. Two missing options in
`get-base-front-component-build-options.ts` — fix is two lines.

## Why this matters

Each front-component bundle includes React + ReactDOM + the design
system AOT (only `twenty-client-sdk/{core,metadata}` are listed in
`FRONT_COMPONENT_EXTERNAL_MODULES`). A trivial widget measures **~2 MB**
unminified. Because every widget mount spawns a fresh Web Worker that
re-fetches and re-parses the bundle (`FrontComponentWorkerEffect.tsx`),
that 2× size translates directly into 2× cold-start latency on **every
record-page navigation and every browser refresh**. On a CRM with even a
handful of custom widgets this dominates perceived UI latency.

## Why it bites every app, not one user

Reference apps in this repo
(`packages/twenty-apps/fixtures/{minimal,rich}-app`,
`community/github-connector`, `internal/twenty-for-twenty`) ship the
same way — verified by inspecting the released `twenty-sdk@2.5.0`
bundle. There's no CLI flag, env var, or config option to opt into a
production build. A search of issues/PRs for "minify", "bundle size",
"production build" surfaces nothing tracking this.

## Fix

Enable `minify: true` and `define: { 'process.env.NODE_ENV':
'"production"' }` in the base front-component build options. These flow
through `build-application.ts` (the orchestrator for `twenty deploy` and
`twenty build`).

**Watch mode (`twenty dev`) is intentionally untouched.**
`esbuild-watcher.ts` has its own configuration path that doesn't consume
`getBaseFrontComponentBuildOptions()`; it stays unminified so local
rebuilds remain fast and stack traces remain readable during
development.

## Measured impact

Two production extension apps using `twenty-sdk@2.5.0`:

| File | Before | After | Δ |
|---|---:|---:|---:|
| `oapps-deal-items` (single widget) | 2,114,953 B | 863,444 B |
**−59%** |
| `oapps-document-hub/documents-panel` | 2,120,341 B | 872,478 B |
**−59%** |
| `oapps-document-hub/hub-document-record` | 2,077,013 B | 852,544 B |
**−59%** |
| `oapps-document-hub/field-mapping-editor` | 1,168,941 B | 255,787 B |
**−78%** |

End-user effect: opening an Opportunity record with two custom-widget
tabs went from ~3-4s widget paint to under 1s on the same machine, same
browser, same record.

## Risk / scope

- **No behavior change.** Minification is a transparent transform;
`NODE_ENV=production` is the standard signal libraries already gate on.
No app code changes needed.
- **No effect on `twenty dev`** — separate code path.
- **No effect on logic functions** — they use their own build-options
object.
- One file touched.

## Test plan

- [ ] `yarn twenty deploy` on
`packages/twenty-apps/fixtures/minimal-app` → output `.mjs` is mangled
and `process.env.NODE_ENV` no longer appears literally inside the
bundle.
- [ ] `yarn twenty dev` on the same app → output `.mjs` remains
readable.
- [ ] Existing CI green.

Happy to add a feature-flag (`TWENTY_BUILD_MODE` env var or `twenty
deploy --no-minify` escape hatch) if maintainers prefer that over
unconditional minification.

---------

Co-authored-by: 8Maverik8 <8maverik8@users.noreply.github.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-01 08:13:55 +00:00
51202d5a32 fix(front): scroll long content in rich text editor (#20319)
## Summary

Fixes scroll behavior in the rich text editor.

Long content was unbounded — the popup grew off-screen, the
expand-to-side-panel button became unreachable, and the side panel
itself didn't scroll either.

Closes #20309

## Changes

- `RichTextFieldInput`: cap the popup at `min(60vh, 500px)` and wrap the
editor in a scrollable region. Collapse button stays at the top via
`align-items: flex-start`.

- `RecordInlineCellEditMode`: add `shift()` middleware to keep the popup
inside the viewport after `flip()` triggers (previously the popup could
extend above the viewport top with the collapse button out of reach).

- `SidePanelContainer` / `SidePanelRouter`: add `min-height: 0` to the
flex-column chain so the existing `overflow-y: auto` on the content area
can actually clip and scroll long children.

The previous attempt in #20310 added the same `min-height: 0` plus a
nested overflow wrapper inside the rich-text page; the nested wrapper
turned out to be the reason scrolling didn't work.

## Test plan

- [x] Open a record with a long `RICH_TEXT` field
- [x] Click the field — popup opens bounded; long content scrolls inside
- [x] Click the expand button (top-right) — side panel opens
- [x] Side panel: long content scrolls vertically
- [x] Popup near the bottom of the viewport flips upward and stays fully
inside the viewport (collapse button remains visible)
- [x] `lint:diff-with-main`, `typecheck`, prettier — green



https://github.com/user-attachments/assets/827be881-aadd-49ed-9ddc-7566c00cf4be

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 08:13:11 +00:00
Thomas TrompetteandGitHub f4380f89a8 fix: SSE event stream reconnection after idle connection death (#21061)
The SSE event stream could silently die from network partitions, NAT
table flushes, browser tab throttling, or server restarts. When this
happened:
1. The `error` callback only called `captureException` — no reconnection
was triggered
2. The `complete` callback was `() => {}` — a cleanly terminated stream
left the client permanently broken
3. No mechanism existed to detect a silently dead connection where no
FIN/RST was received

## Summary

- **Fix `error`/`complete` callbacks**: The `graphql-sse` subscription's
`error` callback only reported to Sentry, and `complete` was a no-op.
Both now set `shouldDestroyEventStreamState = true` to trigger the
destroy-recreate lifecycle, ensuring detected transport failures and
clean stream terminations lead to automatic reconnection.
- **Add server-side keepalive**: The existing heartbeat timer now runs
every 30s (instead of 6min) and publishes empty events through the Redis
pub/sub channel in addition to refreshing the Redis TTL (throttled to
~6min). Unlike GraphQL Yoga's opaque SSE comment pings, these are real
subscription events that flow through the client's `next`/`message`
handlers.
- **Add client-side keepalive monitor (`SSEKeepAliveEffect`)**: Tracks
the timestamp of the last received event. If no event arrives within 90
seconds (3x the keepalive interval), it clears query listeners and
triggers a stream destroy-recreate cycle.

## Test plan

- [x] Start the app, verify SSE events flow normally (workflow runs
update in real-time)
- [x] Leave the app idle for >90 seconds, then trigger a workflow run —
verify the stream auto-reconnects and events are delivered
- [x] Kill the server, restart it, verify the frontend recovers its
event stream
- [x] Verify keepalive events (empty
`objectRecordEventsWithQueryIds`/`metadataEvents`) appear in browser
network tab every ~30s
- [x] Verify no regressions in SSE-dependent features (record updates,
metadata changes, workflow run visualization)
2026-06-01 08:09:22 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
e430e4ea0a fix(ai): route xAI search through Responses API as native tools (#21037)
xAI deprecated Live Search, so the `searchParameters` provider option
now returns 410. This routes all xAI models through the Responses API
and binds web/X search as native agent tools, matching how
Anthropic/OpenAI expose search.

- xAI provider now uses `provider.responses()` — its
`webSearch()`/`xSearch()` tools only run against the Responses endpoint,
not chat completions
- web/X search migrated from the `provider-option` variant to `sdk-tool`
(`web_search`/`x_search`); deleted the dead `searchParameters` path, the
`provider-option` variant, and `providerOptions` on `NativeModelBinding`
- dropped a dead `rolePermissionConfig` param on `getAgentRoleId`, left
over from #20331

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-31 15:11:04 +02:00
Abdullah.andGitHub b027e4bdb1 [Website] i18n module, page-local sections, translatable copy (#21082)
**i18n** — collapsed the ~22 scattered i18n files into a single module
and turned on Spanish alongside French.

**Sections** — dropped the old compound pattern (`Section.Root`,
`Section.Heading`, …). Reusable layout shells moved to `src/templates/`,
atomic bits stay in `design-system/`, and each page now owns its copy in
local `_components` blocks instead of pulling it out of shared sections.
Data files hold arrays only, no prose.

**Copy → `<Trans>`** — A lot of headings were split across several
`<HeadingPart>`s just for font styling, which meant each piece was a
separate translation string. A translator got "Build your Enterprise
CRM" and "at AI Speed" as two unrelated strings and had no way to
reorder them for their language. Those are now single `<Trans>` units
with placeholders. Same idea for the old `\n` + `white-space: pre-line`
line-break trick: replaced with a small `ResponsiveLineBreak` element so
the break is doesn't quietly rot, and did a dead-code pass.

The de-fragmentation changes the message IDs, so around 60 strings will
fall back to English in fr/es until Crowdin re-syncs.
2026-05-31 12:39:35 +00:00
fc90b4ba8b i18n - docs translations (#21064)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 19:30:26 +02:00
88b77cb699 feat(server): opt-in FRONT_AUTO_BASE_URL for hostname-relative API URL (#20504)
## Problem

`generateFrontConfig()` writes `window._env_.REACT_APP_SERVER_BASE_URL =
process.env.SERVER_URL` unconditionally. The frontend then pins to that
absolute URL. For self-hosted deployments reachable from multiple
hostnames (Tailscale IP, LAN IP, internal DNS, SSH tunnel to localhost,
public DNS), only the one matching `SERVER_URL` works — others hit CORS
errors or unreachable hosts because the frontend tries to call the API
at the configured URL, not the one the user came in via.

The frontend already supports the right fallback:
`packages/twenty-front/src/config/index.ts:20-21` reads
`window._env_?.REACT_APP_SERVER_BASE_URL` and falls back to
`getDefaultUrl()` (which uses `window.location`) when the env var is
absent. But the server-side `generateFrontConfig` always populates
`_env_`, so the fallback never runs.

## Fix

One file: `packages/twenty-server/src/utils/generate-front-config.ts`.
Add a `FRONT_AUTO_BASE_URL=true` opt-in (also triggered when
`SERVER_URL` is unset entirely). When the toggle is on, inject
`window._env_ = {}` so the frontend's existing `getDefaultUrl()`
fallback resolves the origin from `window.location` at runtime.

## Backwards compatibility

When `SERVER_URL` is set AND `FRONT_AUTO_BASE_URL` is unset (or anything
other than `'true'`): unchanged — `REACT_APP_SERVER_BASE_URL:
process.env.SERVER_URL` is injected exactly as before.

The toggle is strictly additive. Existing single-hostname deployments
are not affected.

## Use case

Self-hosted Twenty reachable via:
- `http://100.115.12.29` over Tailscale
- `http://localhost:4440` over SSH tunnel
- `http://twenty.internal` over LAN DNS
- `http://crm.example.com` public

With `FRONT_AUTO_BASE_URL=true`, all four paths work without rebuilds or
per-hostname server processes.

## Test plan

- [ ] `SERVER_URL=http://x.com` (toggle unset) → `<script>window._env_ =
{"REACT_APP_SERVER_BASE_URL":"http://x.com"};</script>` (unchanged from
main)
- [ ] `SERVER_URL` unset → `<script>window._env_ = {};</script>` (new
fallback path)
- [ ] `SERVER_URL=http://x.com FRONT_AUTO_BASE_URL=true` →
`<script>window._env_ = {};</script>` (toggle wins)
- [ ] `FRONT_AUTO_BASE_URL=false SERVER_URL=http://x.com` → unchanged
(only `'true'` triggers the toggle)

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-29 16:26:43 +00:00
Raphaël BosiandGitHub 0ed2e9d82d Docs: clarify numberOfSelectedRecords usage for RECORD_SELECTION items (#21059)
Add a note to the command menu items docs explaining that
RECORD_SELECTION already guarantees a non-empty selection, so
numberOfSelectedRecords > 0 is redundant in
conditionalAvailabilityExpression.
2026-05-29 17:59:14 +02:00
643cfe9b13 i18n - docs translations (#21062)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 17:29:40 +02:00
Thomas TrompetteandGitHub bc1b7f6fdf fix: resolve workflow form step auto-open race condition (#21053)
## Summary
- Fix intermittent failure where the Quick Lead workflow form step did
not auto-open
- Root cause: race conditions between SSE events, Apollo cache writes,
and the `runWorkflowVersion` mutation timing
- Add generic monotonicity guard in the SSE handler that drops stale
updates for all records (not just WorkflowRun)

## Changes
- **`useTriggerOptimisticEffectFromSseUpdateEvents.ts`**: Compare
incoming `updatedAt` with cached record before writing — skip if stale.
Moved `upsertRecordsInStore` after the guard so neither Apollo cache nor
Jotai store receive stale data.
- **`useRunWorkflowVersion.tsx`**: Await mutation before opening side
panel; register SSE listener eagerly before mutation
- **`useWorkflowRun.ts`**: Simplified back to plain `useFindOneRecord` +
schema parse (no extra state needed)
- **`generateWorkflowRunDiagram.ts`**: `shouldOpenStep` matches both
PENDING and RUNNING for form steps (backend RUNNING means "waiting for
user input")
- **`WorkflowRunVisualizerEffect.tsx`**: Pass `runStatus` directly
without status mapping
- **`WorkflowRunStepNodeDetail.tsx`**: Form is interactive when step is
PENDING or RUNNING
- **Deleted `latestWorkflowRunFamilyState.ts`**: No longer needed — the
generic SSE guard replaces it

## Test plan
- [x] Hard refresh, run Quick Lead workflow 10+ times — form should
always auto-open
- [x] Complete the form and verify all subsequent steps execute without
getting stuck
- [x] Verify the workflow diagram is always visible (never disappears)
- [x] Verify other record types still update correctly via SSE (e.g.
edit a person in another tab)
2026-05-29 14:18:24 +00:00
Raphaël BosiandGitHub 57118a868f Docs update: Calling a logic function from a front component (#21057)
Documents how a headless front component calls a server-side logic
function over HTTP via the /s/ route, so AI agents have a clear
reference for implementing this pattern.
2026-05-29 14:04:19 +00:00
93f848fd2f i18n - translations (#21055)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 15:30:31 +02:00
Félix MalfaitandGitHub 667cb95730 fix(sso): accept HTTP-POST binding and surface descriptive parser errors (#21051)
Fixes https://github.com/twentyhq/twenty/issues/21044

## Summary
- Closes [#490](https://github.com/twentyhq/private-issues/issues/490) —
JumpCloud customers (and anyone else whose IdP only advertises
`HTTP-POST` for `SingleSignOnService`) could not upload their SAML
metadata; the parser silently rejected them with a generic `Invalid
file` toast.
- The SAML IdP metadata parser now falls back to `HTTP-POST` when
`HTTP-Redirect` is not advertised. Both are valid SAML 2.0 bindings.
- The parser now returns a descriptive `reason` string (Zod issues +
custom errors) instead of an opaque `error: unknown`, and the upload
snack bar surfaces it so the customer can self-diagnose (e.g. `entityID:
entityID is not a valid URL` if they forgot to fill in their IdP Entity
ID).
- Added unit tests for HTTP-POST-only metadata, HTTP-Redirect
preference, and each descriptive-error path.

## Test plan
- [x] `npx jest parseSAMLMetadataFromXMLFile
--config=packages/twenty-front/jest.config.mjs` — 8/8 pass
- [x] `npx oxlint -c packages/twenty-front/.oxlintrc.json` on changed
files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [ ] Manual: upload the customer's JumpCloud metadata (HTTP-POST only,
placeholder `entityID`) and confirm the error now says `Invalid file:
entityID: entityID is not a valid URL` instead of `Invalid file`
- [ ] Manual: upload metadata with a real `entityID` and HTTP-POST-only
binding, confirm the form populates correctly
2026-05-29 15:22:50 +02:00
martmullandGitHub 4e5d47168c 2439 improve command menu item display in right panel (#21020)
## Before

<img width="1512" height="389" alt="image"
src="https://github.com/user-attachments/assets/33274356-fb99-4a02-baa7-c324e6d151c6"
/>


## After

<img width="1512" height="357" alt="image"
src="https://github.com/user-attachments/assets/c0affb71-e920-4d64-b2f0-1bed53209ea5"
/>
2026-05-29 11:37:27 +00:00
neo773andGitHub 10c0bed462 fix: harden email-group SES provisioning, cleanup, and inbound replay (#21046)
## Changes

**Provisioning idempotent** (`aws-ses-register-domain.service.ts`)
- Each SES create call (`CreateConfigurationSet`, event destination,
contact list, tenant association) now swallow `AlreadyExistsException`
via `.send().catch()`.
- Retry after partial failure re-run every step, no blow up on "already
exists". Before: one existing resource kill whole provision.

**Workspace delete clean up cloud** (`workspace.service.ts`,
`emailing-domain-workspace-cleanup.job.ts`,
`emailing-domain.service.ts`)
- On workspace delete, fetch domain list first, pass domains to cleanup
job.
- Cleanup now loop `driver.cleanupDomain(domain)` per domain +
`deprovisionWorkspace`. Tear down SES identity/tenant/config-set, not
just delete DB rows.
- Before: DB rows gone, SES resources orphaned forever. Now: cloud match
DB.

**Inbound replay dedupe** (`ses-inbound-mail-handler.service.ts`)
- Use `snsMessageId` as job id. SNS deliver same message twice → second
is no-op. No duplicate inbound email import.
2026-05-29 10:31:03 +00:00
08b1c5738d i18n - translations (#21050)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 12:12:35 +02:00
martmullandGitHub c2df39405c Fix admin pannel server variable config tab (#21017)
## Before

<img width="1046" height="490" alt="image"
src="https://github.com/user-attachments/assets/450557de-fcf5-4b51-afdb-36c0c36e43d8"
/>


## After

<img width="1040" height="414" alt="image"
src="https://github.com/user-attachments/assets/4a5fe2ab-85d6-4431-9397-6f81ae24055d"
/>
2026-05-29 09:55:11 +00:00
3e2c50c6cf i18n - translations (#21048)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:21:23 +02:00
f67db8d8b2 i18n - translations (#21047)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:17:19 +02:00
3cd2458fdf i18n - website translations (#21045)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-29 11:12:31 +02:00
nitinandGitHub 13f09d8946 [Dashboards] Remove gauge chart types and code (#20410)
Follow-up cleanup to #20172.
2026-05-29 11:08:50 +02:00
martmullandGitHub 961745e9ba Fix latency spike on application lookup (#21042)
Fixes
https://discord.com/channels/1130383047699738754/1509645089062781058

Caching application entities to improve authentication latency
2026-05-29 11:06:36 +02:00
MarieandGitHub 41832c8d82 Fix workflow creation on view filtered by status (#21027)
Creating a workflow on a table with with a filter on status (eg: status
is "active") failed because it added the status to createOneWorkflow (in
order to have the record belonging to the view) - while
createOneWorkflow throwed a 400 exception when attempting to create a
workflow with a status (does not correpsond to a valid behaviour).

Silently stripping status rom create workflow endpoints.
2026-05-29 08:36:59 +00:00
3041ed3b6e chore: sync AI model catalog from models.dev (#21041)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-29 09:12:25 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Félix MalfaitWeikoclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
6d550611d2 chore(deps): bump typescript from 5.9.2 to 5.9.3 (#20991)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2
to 5.9.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/TypeScript/releases">typescript's
releases</a>.</em></p>
<blockquote>
<h2>TypeScript 5.9.3</h2>
<p>Note: this tag was recreated to point at the correct commit. The npm
package contained the correct content.</p>
<p>For release notes, check out the <a
href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-9/">release
announcement</a></p>
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.0%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.0 (Beta)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.1%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.1 (RC)</a>.</li>
<li><em>No specific changes for TypeScript 5.9.2 (Stable)</em></li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.9.3%22+is%3Aclosed+">fixed
issues query for Typescript 5.9.3 (Stable)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/TypeScript/commit/c63de15a992d37f0d6cec03ac7631872838602cb"><code>c63de15</code></a>
Bump version to 5.9.3 and LKG</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/8428ca4cc8a7ecc9ac18dd0258016228814f5eaf"><code>8428ca4</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62438">#62438</a>
(Fix incorrectly ignored dts file fr...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/a131cac6831aa6532ea963d0cb3131b957cad980"><code>a131cac</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62351">#62351</a>
(Add missing Float16Array constructo...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/04243333584a5bfaeb3434c0982c6280fe87b8d5"><code>0424333</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62423">#62423</a>
(Revert PR 61928) into release-5.9 (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62425">#62425</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/bdb641a4347af822916fb8cdb9894c9c2d2421dd"><code>bdb641a</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/62311">#62311</a>
(Fix parenthesizer rules for manuall...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/0d9b9b92e2aca2f75c979a801abbc21bff473748"><code>0d9b9b9</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/61978">#61978</a>
(Restructure CI to prepare for requi...) into release-5.9 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/2dce0c58af51cf9a9068365dc2f756c61b82b597"><code>2dce0c5</code></a>
Intentionally regress one buggy declaration output to an older version
(<a
href="https://redirect.github.com/microsoft/TypeScript/issues/62163">#62163</a>)</li>
<li>See full diff in <a
href="https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript&package-manager=npm_and_yarn&previous-version=5.9.2&new-version=5.9.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-29 08:39:35 +02:00
Félix MalfaitandGitHub 3c97d9648b fix(address): show saved address in record detail when street1 is null (#21033)
## Fixes #20084

### Problem
A saved address is visible in the **table view** but shows **"Empty"**
in the **record detail page** when `addressStreet1` is `null`.

This reproduces with the default seed data out of the box — e.g.
**Google** (city "Mountain View", no street), **Microsoft** (Redmond),
**Meta** (Menlo Park) — which is why several users reported hitting it
immediately.

### Root cause
The frontend zod schema required `addressStreet1` to be a **non-null**
string:

```ts
// isFieldAddressValue.ts
export const addressSchema = z.object({
  addressStreet1: z.string(),          // ← required non-null
  addressStreet2: z.string().nullable(),
  ...
});
```

…but the backend composite type marks it `isRequired: false`
(`address.composite-type.ts`), and the DB column is nullable. So the API
legitimately returns `addressStreet1: null` when only other subfields
are filled.

The two views diverge on how they render:

- **Record detail** gates the value behind `useIsFieldEmpty()` →
`isFieldValueEmpty()`, which for addresses calls
`isFieldAddressValue()`. With `addressStreet1: null` the `safeParse`
**fails**, so `isFieldValueEmpty` returns `true` and the `"Empty"`
placeholder is shown (`RecordInlineCellDisplayMode`).
- **Table view** (`RecordTableCellDisplayMode`) renders
`AddressFieldDisplay` directly with **no** empty check, so the address
stays visible.

This was a latent mismatch since the address guard was introduced.

### Fix
Make `addressStreet1` nullable to match the backend and the other
subfields:

- `addressSchema` → `addressStreet1: z.string().nullable()`
- `FieldAddressValue.addressStreet1` → `string | null`
- `FieldAddressDraftValue.addressStreet1` → `string | null` (keeps the
input/draft type consistent; the text input already renders `?? ''`)

The change is strictly more permissive — persisting and the settings
default-value form still accept string values; they now also accept
`null`.

### Tests
- `isFieldAddressValue.test.ts` — guard returns `true` for
`addressStreet1: null` with other subfields filled.
- `isFieldValueEmpty.test.ts` — new address coverage: empty address is
empty; **`street1: null` + city filled is NOT empty**; normal address is
not empty. (Added an `addressFieldDefinition` mock.)

Both new assertions were confirmed to **fail before the fix** and pass
after.

### Verification
- `npx jest isFieldValueEmpty isFieldAddressValue
normalize-address-field-value-for-persist` → 17 passed
- `npx nx typecheck twenty-front` → pass
- `npx nx lint:diff-with-main twenty-front` → 0 warnings, 0 errors
2026-05-29 08:38:47 +02:00
Félix MalfaitandGitHub 3afdabb93e fix(dashboards): isolate pie chart slice labels per widget (#21034)
## Summary

Fixes [#21014](https://github.com/twentyhq/twenty/issues/21014).

When two pie chart widgets shared the same group-by field (and therefore
the same slice ids) but used different aggregation operators (e.g.
`count` vs `sum`), the arc-link labels would mirror between the two
charts — both ending up showing either the count or the sum values,
depending on render order. Center metrics stayed correct.

**Root cause.** Nivo's `ArcLinkLabelsLayer` and `ArcsLayer` (from
`@nivo/arcs`) wire `react-spring`'s `useTransition` with `keys: e =>
e.id`. When two `<ResponsivePie>` instances render with overlapping ids,
the transitioned data bleeds across charts. The center metric is
unaffected because it's computed by a separate hook
(`usePieChartCenterMetricData`).

**Fix.** Namespace the Nivo-computed slice id per widget by passing an
`id` accessor to `<ResponsivePie>`:
```tsx
id={(datum) => `${id}:${String(datum.id)}`}
```
Lookups inside the widget switch to `datum.data.id` (the original,
un-namespaced id stored on the raw datum), so value/percentage
formatting, the custom tooltip, and the legend hover-dim behavior all
keep working.

Touched files:
- `GraphWidgetPieChart.tsx` — add `id` accessor
- `CustomArcsLayer.tsx` — compare legend highlight against
`datum.data.id`
- `getPieChartFormattedValue.ts`, `getPieChartTooltipData.ts` — match on
`datum.data.id`
- Tests for both utils get a regression case covering the namespaced
computed id

## Test plan

- [ ] `npx jest getPieChartFormattedValue` 
- [ ] `npx jest getPieChartTooltipData` 
- [ ] `npx tsc --noEmit` 
- [ ] Manual: dashboard with two pies on the same group-by field, one
`count` and one `sum`, "Display data label" on for both — confirm each
chart shows its own metric on the slices, and the central total is
unchanged.
- [ ] Manual: hover a legend item — the matching slice in that chart
stays solid while the others dim, and the sibling chart is not affected.
- [ ] Manual: clicking a slice still drills into the correctly filtered
view.
2026-05-29 05:51:10 +00:00
25b0e0d091 fix: correct typo occurence -> occurrence in metadata-event-emitter.ts (#21036)
## Summary

Fixes a spelling typo in
`packages/twenty-server/src/engine/subscriptions/metadata-event/metadata-event-emitter.ts`:

- Variable name `occurence` → `occurrence` (4 references on lines 101,
103, 114, 115)

## Changes

-
`packages/twenty-server/src/engine/subscriptions/metadata-event/metadata-event-emitter.ts`
— rename misspelled variable

Co-authored-by: james <li@jamesdeMacBook-Pro.local>
2026-05-29 05:48:57 +00:00
a43e5c3fb3 i18n - translations (#21032)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 22:15:29 +02:00
996cdaf3ff refactor(agents): split tool resolution into native and action rails (#20331)
## Summary

Splits AI agent tool resolution into two independent rails:

- **Native tools** — capabilities baked into the model SDK
(Anthropic/OpenAI `web_search`, xAI `web`/`x` provider options). Bound
by `NativeToolBinderService`, controlled by per-agent
`modelConfiguration` toggles. Opaque to Twenty — executed on the model
provider's servers.
- **Action tools** — registry-scoped tools from `ToolRegistryService`
(code interpreter, send email, record CRUD, etc.). Permission-gated via
the agent's role. Executed on Twenty's server.

Both rails merge into a single `ToolSet` at call time. When both
surfaces expose a search tool the model picks at runtime — coexistence
is intentional (relevant once Exa returns as an action, see below).

## Notable changes worth calling out

**Contract change: `AgentAsyncExecutorService.executeAgent` no longer
accepts `rolePermissionConfig`.** Workflow agents now scope exclusively
by the agent's own permission-tab role (`unionOf: [agentRoleId]`). The
previous role-merging path (caller role intersected with agent role) is
removed. No agent role → no registry tools (fail-closed by design).

**`NativeToolBinderService` relocated** from
`core-modules/tool-provider/native/` →
`metadata-modules/ai/ai-models/services/`. The binder needs SDK-package
knowledge, which lives in `ai-models`. Old location created a backwards
module dependency.

**`NATIVE_MODEL_TOOLS_BY_SDK_PACKAGE` is exhaustive over
`AiSdkPackage`** (`Record<>`, not `Partial<Record<>>`). Adding a new SDK
without thinking about native tools now fails the build. SDKs without
native tools (Bedrock, Google, Mistral, Azure, OpenAI-compatible) get
explicit `{}` entries.

**Discriminated union `kind: 'sdk-tool' | 'provider-option'`** lets one
registry describe both function tools (Anthropic/OpenAI) and runtime
sources (xAI). Follows the local `tool-provider` convention from #19321.

## Deferred to follow-ups

- **Exa web search is dropped from this PR** (along with its
`WEB_SEARCH_TOOL` permission flag and the Exa-specific gating). Exa
comes back as an **action/app tool** once apps can define permission
flags through the SDK — ongoing work in #20481.
- **xAI native search currently errors.** xAI deprecated its Live Search
API (the `web`/`x` provider-option sources this rail maps to), so xAI
returns `410` when native search is actually exercised. The code path
itself is clear — it's only hit if you test xAI native tools. Fixed
separately alongside the broader xAI model fixes.

## Conscious non-decisions

- **No "twenty-native" category.** `native` is reserved for
model/provider SDK features; everything Twenty-owned is just a
tool/action.
- **Coexistence over precedence.** No rule forcing an action search tool
to override native search (or vice-versa) — when both exist, it's the
user's choice in workflow agents and the model's choice in chat.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
2026-05-28 22:08:05 +02:00
Raphaël BosiandGitHub 1d84695fb0 Fix: focus stack overwritten when auto-opening title cell on new record (#21029)
Fixes https://github.com/twentyhq/twenty/issues/20894

In `PageChangeEffect` `resetFocusStackToFocusItem` ran right after
`openNewRecordTitleCell`, wiping the title cell entry. Typing in the
auto-opened breadcrumb input (e.g. new workflow) triggered global
shortcuts and ignored Enter / Escape / Tab.

Reordered so the page reset runs first, then the title cell push lands
on top.

## Before


https://github.com/user-attachments/assets/d3c0c266-a493-46b8-b99b-32f4381b8664


## After


https://github.com/user-attachments/assets/3a886386-f438-46d2-a6ea-9ee6908d6df2
2026-05-28 19:23:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Weiko
64e0b76d00 chore(deps): bump js-cookie from 3.0.5 to 3.0.7 (#20992)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.5 to
3.0.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/js-cookie/js-cookie/releases">js-cookie's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.7</h2>
<ul>
<li>Prevent cookie attribute injection: CVE-2026-46625 (eb3c40e)</li>
<li>Add <code>Partitioned</code> attribute to readme (b994768)</li>
<li>Publish to npm registry via trusted publisher exclusively
(4dc71be)</li>
<li>Ensure consistent behaviour for <code>get('name')</code> +
<code>get()</code> (1953d30)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/17bacba0171dd022728d8fdeba3203c60791bf58"><code>17bacba</code></a>
Craft v3.0.7 release</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/adb823cb7e95ead47f3af4d4951e589acbde2077"><code>adb823c</code></a>
Fix release workflow halting at <code>git tag</code></li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/5f9e759b07d2752e8407a3a43fb5f879bf384c5e"><code>5f9e759</code></a>
May remove Git user config from release workflow</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/6ac921184c7b3b7d9431c88707f56521acd72ab4"><code>6ac9211</code></a>
Fix release workflow not able to push commit + tag</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/2278bc55e1804c4c2d9bd2110a9b449949a52751"><code>2278bc5</code></a>
Fix missing package version bump</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/eb3c40e89731e99b8970faaf35ddad249c6c0020"><code>eb3c40e</code></a>
Prevent cookie attribute injection</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/f6f157f430d707d2ffd0c9c9138227a6cea564e5"><code>f6f157f</code></a>
Bump globals from 17.5.0 to 17.6.0</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/f409d022da50a0c6fa8724f087fbc50fab9a9533"><code>f409d02</code></a>
Bump eslint from 10.2.0 to 10.3.0</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/a686883c03a754c04546cfc1653911a70a640b40"><code>a686883</code></a>
Bump protobufjs in the npm_and_yarn group across 1 directory</li>
<li><a
href="https://github.com/js-cookie/js-cookie/commit/c6112d2d4f2881a12aaf89d9e2996ef6870eb6d0"><code>c6112d2</code></a>
Bump <code>@​protobufjs/utf8</code> in the npm_and_yarn group across 1
directory</li>
<li>Additional commits viewable in <a
href="https://github.com/js-cookie/js-cookie/compare/v3.0.5...v3.0.7">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for js-cookie since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-cookie&package-manager=npm_and_yarn&previous-version=3.0.5&new-version=3.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
2026-05-28 21:25:35 +02:00
Félix MalfaitandGitHub 8a74ea8829 fix(contact-creation): enrich missing names on auto-created contacts (#21018)
## Summary

Three related fixes to the auto-creation of People records from calendar
events and email messages, all centred on the data-quality problem of
contacts being created with missing or malformed names.

### 1. Enrich names on existing contacts (commit 1)

Previously: when an email or calendar import matched an existing Person
by email, the existing record was left untouched — even if the new
source carried a better name.

This is the root cause of contacts like `"Félix"` (no last name)
sticking around forever: the `To:`/`Cc:` headers of outbound emails
rarely include a display name, and Google Calendar only returns
`displayName` for attendees already in the organizer's address book. So
the first sighting often creates a Person as `{firstName: "felix",
lastName: ""}`, and a later inbound `From: "Félix Malfait"
<felix@twenty.com>` — which would have produced the right name — gets
silently dropped because the Person already exists.

The new `computePeopleToEnrichNames` bucket and
`CreatePersonService.enrichPeopleNames` method fill in missing
`firstName`/`lastName` fields from the new parsed name, with
conservative rules:
- Only enrich when the existing Person's `createdBy.source` is
`CALENDAR` or `EMAIL` — `MANUAL`, `IMPORT`, `API`, `WORKFLOW`, etc. are
never touched.
- Only fill empty fields. Non-empty `firstName`/`lastName` are never
overwritten.
- Soft-deleted contacts continue to be handled by the existing restore
path.

### 2. Handle multi-comma "Last, First, Suffix" display names (commit 2)

The comma-inverted swap in the parser previously required *exactly* one
comma. Names like `"Smith, Jane, Jr."`, `"O'Brien, Mary, MD"` or `"Doe,
John, Patrick"` fell through to the space-split fallback, which stored
the comma in `firstName` (e.g. `"Smith,"`) and produced garbled records
(the avatar shows a single "B" and the name reads `"Barbey, Julien"`
because the entire string lives in `firstName`).

The regex now splits on the first comma and treats the remainder as the
first name, collapsing any further commas to spaces. Single-comma
behaviour is unchanged.

### 3. Perf: skip the parser when an existing record is already
populated (commit 3)

`computePeopleToEnrichNames` runs on every cron-driven email/calendar
import batch. The first version called the display-name parser for every
matched existing person, even when both `firstName` and `lastName` were
already set — i.e. the steady-state case after the initial enrichment
pass.

Reordered so the cheap "both fields populated" check short-circuits
before any parsing happens. Same behaviour, fewer parser calls on the
hot path.

## Test plan

- [x] 8 new unit tests for the enrichment bucket: empty `lastName`
enrichment, both `EMAIL` and `CALENDAR` sources, non-overwrite of
non-empty fields, skip on `MANUAL`/`IMPORT`, skip when the new source
also has no last name, skip for soft-deleted, fill `firstName` while
preserving `lastName`, handle null `name` field
- [x] 5 new parser tests for multi-comma forms: `"Last, First, Suffix"`,
credential suffixes (`MD`), three-token forms, whitespace around inner
commas, `:GROUP` tag interaction
- [x] 1 new parser test on the single-comma path covering multi-word
first names (`"Smith, Mary Jane"`)
- [x] All 15 existing parser tests still pass
- [x] All 116 tests in `contact-creation-manager` pass
- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` + `npx oxfmt --check` on changed files
- [ ] Manual: trigger a fresh contact creation from an outbound email
with no display name, then a subsequent inbound email from the same
address with a full display name, and confirm the Person's last name
gets populated
2026-05-28 20:49:56 +02:00
Félix MalfaitandGitHub f4ead89956 refactor(twenty-orm): migrate 23 grandfathered entities to WorkspaceScopedRepository (#20987)
## Summary

Follow-up to #20953. Migrates 23 of the 30 entities that were left in
`WORKSPACE_SCOPED_EXEMPTIONS` last time, so the lint rule's
workspaceId-enforcement default now covers most of the core/metadata
schema.

### Migrated (23 entities, 88 files, 22 commits)

| Family | Entities |
|---|---|
| Trivial caches | `NavigationMenuItem`, `Skill`, `DataSource`,
`Webhook`, `CommandMenuItem`, `IndexMetadata` |
| Views | `View`, `ViewField`, `ViewFieldGroup`, `ViewFilter`,
`ViewFilterGroup`, `ViewGroup`, `ViewSort` |
| Layouts | `PageLayout`, `PageLayoutTab`, `PageLayoutWidget` |
| Roles & permissions | `Role`, `RoleTarget`, `PermissionFlag`,
`ObjectPermission`, `FieldPermission`, `RowLevelPermissionPredicate`,
`RowLevelPermissionPredicateGroup` |

For each entity: swap `@InjectRepository(X)` →
`@InjectWorkspaceScopedRepository(X)` (and the field type →
`WorkspaceScopedRepository<X>`); rewrite every call site to pass
`workspaceId` as the first arg (stripped from `where`/criteria — the
wrapper throws if you include it now); register
`provideWorkspaceScopedRepository(X)` in every owning NestJS module;
update affected spec providers to
`getWorkspaceScopedRepositoryToken(X)`.

### Rule update

- `ApplicationRegistrationVariableEntity` was misclassified — moved to
`STRUCTURAL_EXEMPTIONS` (no `workspaceId` column; it's keyed on
`applicationRegistrationId` at the instance level).
- 22 of the 23 migrated entities removed from
`WORKSPACE_SCOPED_EXEMPTIONS` entirely (zero remaining raw
`@InjectRepository` sites).
- `RoleTargetEntity` also removed; one call site in
`user-workspace.service.ts` keeps a raw injection with an
`eslint-disable` + reason because `softRemove(...)` is not on the
wrapper API yet (the migration would require threading `workspaceId`
through `deleteUserWorkspace`'s three callers).

### Still exempted (7 entities, follow-up PRs)

| Entity | Why deferred |
|---|---|
| `ApplicationEntity` | ~50 sites with several cross-workspace lookups
by id (auth, OAuth, file-storage, cleanup) |
| `CalendarChannelEntity` / `MessageChannelEntity` | Use
`.increment(...)` (not on wrapper) and
`repository.manager.transaction(...)` — wrapper needs to grow
`.increment` + the transaction sites need `withManager` or dual-inject |
| `FieldMetadataEntity` / `ObjectMetadataEntity` | The metadata services
`extends TypeOrmQueryService<X>` and `super(rawRepo)` — requires
dual-inject or reworking the inheritance |
| `KeyValuePairEntity` | Allows `workspaceId: IsNull()` for
instance-level config; wrapper rejects null |
| `UpgradeMigrationEntity` | Same — instance-level + cross-workspace
ledger |

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — clean (0/0)
- [x] All 10 affected unit specs pass (115 tests) — api-key, agent-role,
permissions, workspace-roles-permissions-cache, view-filter-group,
workflow-version-step-operations, two-factor-authentication (service +
resolver), user-workspace, file
- [ ] Server integration tests in CI
2026-05-28 20:46:21 +02:00
Félix MalfaitandGitHub 865ca697ca Fix AI permission gating: use Ask AI for chat UI, AI Settings for admin endpoints (#21030)
## Summary

Closes #20662.

Two AI permission flags exist:
- **`AI`** (label "Ask AI") — user-facing: chat with AI agents, use AI
features
- **`AI_SETTINGS`** (label "AI") — admin: create and configure AI agents

After auditing every use of these flags I found:

### Frontend — chat UI gated by the admin permission (user-facing bug
from the issue)
A user granted only `Ask AI` could not see chat tabs, the "new chat"
button (desktop & mobile), or the chat content pane; thread
initialization was also skipped, leaving the chat in a half-initialized
state and producing intermittent `THREAD_NOT_FOUND` errors. Switched
these to `AI`:
- `MainNavigationDrawerTabsRow.tsx`
- `MainNavigationDrawer.tsx`
- `MobileNavigationBar.tsx`
- `AgentChatThreadInitializationEffect.tsx`

### Backend — admin-only resolvers gated by the user permission
(privilege escalation)
Two resolvers had a class-level guard of `AI`, letting any user with the
user-facing flag reach admin endpoints (skill CRUD, eval runs). Switched
the class-level guards to `AI_SETTINGS`:
- `SkillResolver` — create/update/delete/activate/deactivate skills
- `AgentTurnResolver` — read turns, run/grade evaluations

### Left as-is (already correct)
- `AgentResolver` — class-level `AI` for reads (workflow editors and
admin pages both need them), mutation-level `AI_SETTINGS` overrides for
writes
- `AgentChatResolver` & `AgentChatSubscriptionResolver` — already `AI`
- `AiGenerateTextController` — already `AI`
- Workspace AI config fields in `workspace.service.ts` — already
`AI_SETTINGS`

## Test plan

- [ ] As a user with `Ask AI` only (no `AI_SETTINGS`): chat tabs, "new
chat" button, and chat history pane are visible on desktop + mobile;
sending a message works; no `THREAD_NOT_FOUND` errors
- [ ] As a user with `AI_SETTINGS` but no `Ask AI`: chat UI is hidden
- [ ] As a user with `Ask AI` only: calling `skills` / `createSkill` /
`agentTurns` / `runEvaluationInput` via GraphQL returns permission
denied
- [ ] As an admin (`AI_SETTINGS`): skill settings and agent eval pages
still work
2026-05-28 20:21:58 +02:00
Paul RastoinandGitHub ebfaca5b3d EncryptedString PlaintextString branded string types (#21001)
## Summary

closes https://github.com/twentyhq/core-team-issues/issues/2464

Introduces compile-time branded types to distinguish encrypted
ciphertext from plaintext strings, preventing mix-ups like the one fixed
in #20819 — but at the type level rather in addition to the one existing
at runtime.

### Branded string primitives

- Created `EncryptedString` and `PlaintextString` as hard nominal brands
using `z.string().brand(...)`, making them non-assignable to each other
or to raw `string`
- Created `isEncryptedString` type predicate to narrow `string` to
`EncryptedString` based on the `enc:v2:` envelope prefix
- Retyped `SecretEncryptionService`: `encryptVersioned` accepts
`PlaintextString`, `decryptVersioned` returns `PlaintextString`

### Entity typing

- Typed encrypted columns across entities:
`SigningKeyEntity.privateKey`,
`TwoFactorAuthenticationMethodEntity.secret`,
`ApplicationRegistrationVariableEntity.encryptedValue`,
`ApplicationVariableEntity.value`
- Parameterized JSONB types for connected account connection parameters
(`ImapSmtpCaldavParams<Pwd>`) with reusable aliases
`EncryptedImapSmtpCaldavParams` / `DecryptedImapSmtpCaldavParams`
- Typed DTOs (`CreateApplicationRegistrationVariableInput`,
`UpdateApplicationRegistrationVariablePayload`,
`UpdateApplicationVariableEntityInput`) with `PlaintextString`

### ApplicationVariable always-encrypt uniformization

- Retyped `ApplicationVariableEntity.value` to `EncryptedString | ''` —
all values are now encrypted regardless of `isSecret`
- Updated `ApplicationVariableEntityService` to always encrypt on write
and always decrypt on read
- Simplified `UpdateApplicationVariableActionHandlerService` by removing
conditional encrypt/decrypt-on-isSecret-toggle logic
- Added slow instance command (`2.9.0`) to backfill-encrypt existing
`isSecret=false` plaintext rows and tighten the `CHECK` constraint

### ConfigStorageService refactor

- Split `convertAndSecureValue` (which used `any`) into two well-typed
methods: `convertAndDecrypt` and `convertAndEncrypt`
- Introduced `isSensitiveStringValue` type predicate to narrow values
before encryption/decryption

### What's next
- Typeorm entity derivation to strictly type sitemap configuration as
code + handler logic for encryption rotation
- https://github.com/twentyhq/core-team-issues/issues/2465
2026-05-28 17:41:16 +02:00
WeikoandGitHub 9b54200d8c Fix playwright CI (#21024)
## Context
The Install Playwright step ran npx playwright install with no
arguments, which downloads all browsers (Chromium + Firefox + WebKit +
ffmpeg, ~500MB+) on every run with no caching.

Fix:
- Install Chromium only — npx playwright install chromium instead of all
browsers.
- Cache the browser binaries — actions/cache on ~/.cache/ms-playwright,
keyed on the resolved Playwright version (v4-playwright-browsers-${{
runner.os }}-<version>). On a cache hit the install step is skipped
entirely; the cache invalidates automatically when the Playwright
version bumps.
2026-05-28 14:19:58 +00:00
bd6811d060 i18n - translations (#21022)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:59:42 +02:00
b32249877a i18n - translations (#21021)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:55:32 +02:00
martmullandGitHub 6fb6ef4e7a Add darkmode for oAuth screen (#21005)
## after

<img width="1512" height="828" alt="image"
src="https://github.com/user-attachments/assets/71664eab-c921-45da-ac67-7a660c976d5c"
/>
2026-05-28 15:48:57 +02:00
Raphaël BosiandGitHub bb4e28904f Support the "Me" filter for workspace members in dashboard widgets and add multi select (#20971)
Fixes https://github.com/twentyhq/twenty/issues/20225

The "Me" filter (current workspace member) worked in view filters but
not in dashboard widget filters — the server never resolved the
placeholder, and the widget side-panel UI had no "Me" option and only
allowed single selection.

Backend: `ChartDataQueryService` now forwards the current workspace
member id (from authContext) into filterValueDependencies, so the shared
filter logic resolves "Me" the same way it does for view filters. Added
unit tests for the converter.

Frontend: new multi-select picker for workspace member filters in the
widget side panel, mirroring the view filter's actor select: search
input, "Me" pinned item, and a multi-select workspace member list.

## Before
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 16
36@2x"
src="https://github.com/user-attachments/assets/b2cff46c-53e5-4e8a-a463-b106daf96c8c"
/>

## After
<img width="3024" height="1488" alt="CleanShot 2026-05-27 at 17 14
05@2x"
src="https://github.com/user-attachments/assets/8b3b5f11-44b9-4ae5-a2f3-9c7a689f4bb2"
/>
2026-05-28 15:43:47 +02:00
c3d1af89ae i18n - docs translations (#21019)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 15:43:18 +02:00
Raphaël BosiandGitHub e6221c5f0e Fix twenty sdk billing exports (#21016)
Fix two omissions from #19973 that prevented `twenty-sdk/billing` from
being a fully exported subpath:

- `package.json`: add `billing` to `typesVersions` (every other subpath
was listed; billing was the only one missing, breaking type resolution
for consumers using classic TS moduleResolution).
- `project.json`: add the billing vite build and `dist/billing` output
to the `build:sdk` target
2026-05-28 15:41:24 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6566a918af chore(deps): bump @apollo/client from 4.1.6 to 4.2.0 (#20993)
Bumps [@apollo/client](https://github.com/apollographql/apollo-client)
from 4.1.6 to 4.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/apollographql/apollo-client/releases">@​apollo/client's
releases</a>.</em></p>
<blockquote>
<h2><code>@​apollo/client</code><a
href="https://github.com/4"><code>@​4</code></a>.2.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/apollographql/apollo-client/pull/13132">#13132</a>
<a
href="https://github.com/apollographql/apollo-client/commit/f3ce805425d10a9666218a8e109288a2d46dcab1"><code>f3ce805</code></a>
Thanks <a
href="https://github.com/phryneas"><code>@​phryneas</code></a>! -
Introduce &quot;classic&quot; and &quot;modern&quot; method and hook
signatures.</p>
<p>Apollo Client 4.2 introduces two signature styles for methods and
hooks. All signatures previously present are now &quot;classic&quot;
signatures, and a new set of &quot;modern&quot; signatures are added
alongside them.</p>
<p><strong>Classic signatures</strong> are the default and are identical
to the signatures before Apollo Client 4.2, preserving backward
compatibility. Classic signatures still work with manually specified
TypeScript generics (e.g.,
<code>useSuspenseQuery&lt;MyData&gt;(...)</code>). However, manually
specifying generics has been discouraged for a long time—instead, we
recommend using <code>TypedDocumentNode</code> to automatically infer
types, which provides more accurate results without any manual
annotations.</p>
<p><strong>Modern signatures</strong> automatically incorporate your
declared <code>defaultOptions</code> into return types, providing more
accurate types. Modern signatures infer types from the document node and
do not support manually passing generic type arguments; TypeScript will
produce a type error if you attempt to do so.</p>
<p>Methods and hooks automatically switch to modern signatures the
moment any non-optional property is declared in
<code>DeclareDefaultOptions</code>. The switch happens across all
methods and hooks globally:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  namespace ApolloClient {
    namespace DeclareDefaultOptions {
      interface WatchQuery {
errorPolicy: &quot;all&quot;; // non-optional → modern signatures
activated automatically
      }
    }
  }
}
</code></pre>
<p>Users can also manually switch to modern signatures without declaring
any <code>defaultOptions</code>, for example when wanting accurate type
inference without relying on global <code>defaultOptions</code>:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;modern&quot;;
  }
}
</code></pre>
<p>Users can do a global <code>DeclareDefaultOptions</code> type
augmentation and then manually switch back to &quot;classic&quot; for
migration purposes:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;classic&quot;;
  }
}
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md">@​apollo/client's
changelog</a>.</em></p>
<blockquote>
<h2>4.2.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/apollographql/apollo-client/pull/13132">#13132</a>
<a
href="https://github.com/apollographql/apollo-client/commit/f3ce805425d10a9666218a8e109288a2d46dcab1"><code>f3ce805</code></a>
Thanks <a
href="https://github.com/phryneas"><code>@​phryneas</code></a>! -
Introduce &quot;classic&quot; and &quot;modern&quot; method and hook
signatures.</p>
<p>Apollo Client 4.2 introduces two signature styles for methods and
hooks. All signatures previously present are now &quot;classic&quot;
signatures, and a new set of &quot;modern&quot; signatures are added
alongside them.</p>
<p><strong>Classic signatures</strong> are the default and are identical
to the signatures before Apollo Client 4.2, preserving backward
compatibility. Classic signatures still work with manually specified
TypeScript generics (e.g.,
<code>useSuspenseQuery&lt;MyData&gt;(...)</code>). However, manually
specifying generics has been discouraged for a long time—instead, we
recommend using <code>TypedDocumentNode</code> to automatically infer
types, which provides more accurate results without any manual
annotations.</p>
<p><strong>Modern signatures</strong> automatically incorporate your
declared <code>defaultOptions</code> into return types, providing more
accurate types. Modern signatures infer types from the document node and
do not support manually passing generic type arguments; TypeScript will
produce a type error if you attempt to do so.</p>
<p>Methods and hooks automatically switch to modern signatures the
moment any non-optional property is declared in
<code>DeclareDefaultOptions</code>. The switch happens across all
methods and hooks globally:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  namespace ApolloClient {
    namespace DeclareDefaultOptions {
      interface WatchQuery {
errorPolicy: &quot;all&quot;; // non-optional → modern signatures
activated automatically
      }
    }
  }
}
</code></pre>
<p>Users can also manually switch to modern signatures without declaring
any <code>defaultOptions</code>, for example when wanting accurate type
inference without relying on global <code>defaultOptions</code>:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;modern&quot;;
  }
}
</code></pre>
<p>Users can do a global <code>DeclareDefaultOptions</code> type
augmentation and then manually switch back to &quot;classic&quot; for
migration purposes:</p>
<pre lang="ts"><code>// apollo.d.ts
import &quot;@apollo/client&quot;;
declare module &quot;@apollo/client&quot; {
  export interface TypeOverrides {
    signatureStyle: &quot;classic&quot;;
  }
}
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apollographql/apollo-client/commit/e010bdd239b5c10415d4b70ca791467cde12fc88"><code>e010bdd</code></a>
Version Packages (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13241">#13241</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/9c4c01a640b43bfb47bd52b25d5881c4ad7bec71"><code>9c4c01a</code></a>
Release 4.2 (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13129">#13129</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/222838e99bc6054120cc1f881bb225b1ef049de9"><code>222838e</code></a>
Exit prerelease mode</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/7d3a533c811a8423536ceeebccc06413ded5b6a3"><code>7d3a533</code></a>
Merge branch 'main' into release-4.2</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/f20d591bbf74cb4f0d87ec9a14b93a59fe46b039"><code>f20d591</code></a>
chore(deps): update actions/create-github-app-token digest to d72941d
(<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13239">#13239</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/d4a28b6142e47164c8a24bd8c05a8aa3f1ce4eee"><code>d4a28b6</code></a>
chore(deps): pin dependencies (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13237">#13237</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/c1f39cf5402b052ab92886a1857840a745aee02b"><code>c1f39cf</code></a>
ci: pin Actions@SHA and disable cache on workflows with elevated OIDC
permiss...</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/511048b7bd6253a38a6b7ebe58e9674a39c74273"><code>511048b</code></a>
Event-based refetching docs (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13228">#13228</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/d1f68f1a5fdb7c6915a72b2426cad373a0526c06"><code>d1f68f1</code></a>
Version Packages (rc) (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13234">#13234</a>)</li>
<li><a
href="https://github.com/apollographql/apollo-client/commit/f1b541fed4111028b6842727178288156582e669"><code>f1b541f</code></a>
Prepare for rc release (<a
href="https://redirect.github.com/apollographql/apollo-client/issues/13232">#13232</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/apollographql/apollo-client/compare/@apollo/client@4.1.6...@apollo/client@4.2.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@apollo/client&package-manager=npm_and_yarn&previous-version=4.1.6&new-version=4.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-28 15:12:52 +02:00
martmullandGitHub 6ea637d6c5 Export STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS (#21010)
fix https://discord.com/channels/1130383047699738754/1509086323464474705
2026-05-28 12:51:01 +00:00
Félix MalfaitandGitHub 531410f64a fix(ai): expose MORPH_RELATION join columns in AI/MCP tool schemas (#21012)
## Summary

- Fixes a bug where `noteTarget` (and any other morph-relation join
object) created via AI/MCP would land with `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` left null, even though the tool
reported success.
- Root cause: the Zod schema generators for the AI tools only branched
on `FieldMetadataType.RELATION`. MORPH_RELATION fields fell through to
the default case — for `create_*` they were exposed as `targetCompany:
string` instead of `targetCompanyId: uuid`, and for `group_by_*` they
were silently skipped entirely. Downstream
(`data-arg-processor.service.ts` and the group-by arg processor) already
accept the join-column form for both kinds of relations via
`computeMorphOrRelationFieldJoinColumnName` and
`isMorphOrRelationFlatFieldMetadata`, so the fix is purely in the schema
generators.

## Changes

- `record-properties.zod-schema.ts` — extend the existing RELATION
MANY_TO_ONE / ONE_TO_MANY branches to also match MORPH_RELATION.
- `group-by-tool.zod-schema.ts` — replace the silent MORPH_RELATION skip
with the same treatment as RELATION MANY_TO_ONE (exposes `${name}Id` as
a groupBy option).
- `test/integration/ai/suites/mcp-tool-execution.integration-spec.ts` —
new file. First integration test for tool execution end-to-end. Drives
the real MCP JSON-RPC endpoint with the seeded API key (`learn_tools`
for schema introspection, `execute_tool` for invocation):
- asserts `create_note_target`'s schema exposes `targetCompanyId` /
`targetPersonId` / `targetOpportunityId` as UUIDs and does **not**
expose `targetCompany` / `targetPerson` / `targetOpportunity`.
- creates a company + note + noteTarget via MCP, then queries the
workspace schema to confirm `targetCompanyId` is actually persisted in
the FK column.
- asserts `group_by_note_targets` schema accepts `targetCompanyId` as a
groupBy key.
- sets up 3 noteTargets (2 → company A, 1 → company B), calls
`group_by_note_targets` by `targetCompanyId`, and asserts the counts.

Out of scope: `record-filter.zod-schema.ts` has the same pattern (only
RELATION) — left for a follow-up so this PR stays focused on what was
reported.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx oxlint --type-aware` on changed files — clean
- [x] `npx oxfmt --check` on changed files — clean
- [x] Integration tests pass (4/4) after `database:reset`:
- `should expose the morph-relation join columns as \`${name}Id\` UUID
parameters`
- `should persist targetCompanyId when create_note_target is invoked via
MCP`
  - `should expose targetCompanyId as a valid groupBy option`
  - `should group noteTargets by targetCompanyId via MCP`
2026-05-28 14:17:10 +02:00
182960051f i18n - translations (#21013)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-28 13:30:39 +02:00
Félix MalfaitandGitHub 7d9f9605a2 feat(settings): move email handles and emailing domains to dedicated Email page (#21008)
## Summary

Both **Email Handles** and **Emailing Domains** were rendered on the
General workspace settings page, but they're workspace-level *email
infrastructure* (inbound shared addresses + outbound sender
authentication) and don't belong with the workspace name, picture, and
domain config.

- New `SettingsWorkspaceEmail` page at `/settings/email`
- Nav item under **Workspace**, hidden when `IS_EMAIL_GROUP_ENABLED` is
off (and gated by `WORKSPACE` permission)
- Related sub-routes (`email-group/:messageChannelId`,
`emailing-domain/:domainId`, etc.) moved from `general/` to `email/` so
the URL space stays consistent with the page
- General page now only contains name, picture, workspace domain, and
the delete-workspace section

No behavior changes to the underlying section components — they're
imported as-is into the new page.

## Test plan

- [ ] With `IS_EMAIL_GROUP_ENABLED` enabled: **Email** appears in the
Workspace nav and the page renders both sections
- [ ] With the flag disabled: **Email** is hidden from nav; navigating
to `/settings/email` directly renders nothing
- [ ] General page no longer shows Email Handles / Emailing Domains
- [ ] Clicking a shared inbox row navigates to
`/settings/email/email-group/:id` (was `general/...`)
- [ ] "Add emailing domain" navigates to
`/settings/email/emailing-domain/new`

## Notes

- Pre-existing `twenty-front` typecheck error in
`FrontComponentRendererProvider.tsx` (React types mismatch between
sibling packages) reproduces on `main` and is unrelated to this PR.
2026-05-28 13:22:55 +02:00
Félix MalfaitandGitHub 7065441972 fix: refresh admin panel feature flags after toggling (#21007)
## Summary

- Add `refetchQueries` to the `updateWorkspaceFeatureFlag` mutation in
the admin workspace detail page so the toggle reflects the new value
after toggling.
- Rename `useFeatureFlagState` → `useAdminUpdateFeatureFlag` since the
hook lives under `admin-panel` and is only consumed by the admin
workspace detail page.

## Bug

In the admin panel, toggling a feature flag for a workspace other than
the admin's own workspace sent the backend mutation successfully, but
the toggle in the UI remained unchanged.

The displayed value is derived from:
```tsx
const currentWorkspaceValue =
  currentWorkspace?.id === workspaceId
    ? currentWorkspace?.featureFlags?.find((f) => f.key === flag.key)?.value
    : undefined;
const displayedValue = currentWorkspaceValue ?? flag.value;
```

When viewing a different workspace, `currentWorkspaceValue` is
`undefined` so the toggle reads `flag.value` from the
`WORKSPACE_LOOKUP_ADMIN_PANEL` query. That query was never refetched
after the mutation, so the displayed value stayed stale.

The existing optimistic Jotai update on `currentWorkspaceState` still
runs — it is needed so the rest of the app (anything consuming
`useIsFeatureEnabled`) reacts immediately when an admin toggles a flag
on their own workspace.

## Test plan

- [ ] Open the admin panel → pick a workspace that is not your own →
Feature Flags tab → toggle a flag → toggle visually flips after the
mutation completes.
- [ ] Same flow on your own workspace → toggle flips, and any UI gated
on that flag also reacts.
- [ ] If the mutation fails, the toggle reverts (existing `onError`
rollback path).
2026-05-28 12:23:26 +02:00
6e2cf4cbc0 Fix lambda timeout diagnostics (#21002)
Trying to fix
https://twenty-v7.sentry.io/issues/7420384466/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=date

## Summary
- Prevent invoking Lambda functions stuck in `Pending` state by checking
`Configuration.State === 'Active'` in `checkLambdaExecutorBuildStatus` —
a Pending function now goes through `ensureLambdaExecutor` which waits
for Active.
- Track execution phase (`build`/`fetch-code`/`invoke`) via a
`LambdaExecutionPhase` enum and include phase timing + function state in
all error messages for faster debugging.

Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
2026-05-28 09:06:33 +00:00
de7daaa81a fix: exclude system objects and workflow/dashboard from AI/MCP write tool descriptors (#20973)
## Summary

fix: exclude system join objects from AI/MCP create/update/delete tool
descriptors

Closes #20403

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-27 20:01:11 +02:00
c0cbe67bcd i18n - translations (#20982)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:47:07 +02:00
neo773GitHubFélix Malfaitclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix Malfait
c5606212f2 Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature
and the new email groups feature.

These are workspace level shared inboxes that are shared between all
workspace members.

outbound sending with SES works, we only listen for tenant status
events, rest is managed by AWS

PR refactors old code and webhook to be split for outbound and inbound
for proper separation


| Area | Change |
|---|---|
| AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant +
identity + DKIM + MAIL FROM + configuration-set + EventBridge dest +
contact list) and `AwsSesSendEmailService` (SendEmail). |
| Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES
→ EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS →
router → `SesOutboundSendingStateHandlerService` updates
`emailing_domain.tenantStatus`. |
| Inbound webhook | Refactored into `SesInboundWebhookRouterService` +
`SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` +
`SnsSubscriptionConfirmerService` across both routes. |
| Global uniqueness | New migration + instance command:
`emailing_domain.domain` is now globally unique (one tenant per domain
across workspaces). |
| Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` /
`PAUSED`) + `EmailingDomainTenantStatusService`. |
| Send-email mutation | New `sendEmailViaDomain` GraphQL mutation +
DTOs. |
| Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into
`WorkspaceService.deleteWorkspace` — tears down SES tenant association +
identity on workspace delete. |
| Settings UI | Rewritten around reusable `SettingsTableListSection`.
"Email Group" → "Email Handle" rename. New cells for
status/source/forwarding. Outbound domains surfaced on workspace
settings page. |

### Env vars (new)

All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional:

- `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider`
+ driver factory
- `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory
- `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound
webhook routers, comma-separated list of accepted SNS topic ARNs
(verified via `sns-payload-validator`)

### Migrations

- `1778862608620-add-emailing-domain-tenant-status` (fast) — adds
`tenantStatus` column.
- `1778865501791-unique-emailing-domain-globally` (slow, idempotent) —
enforces global uniqueness on `domain`.
- Instance commands bumped to `2.5`.

### Infra dependency

Two coupled twenty-infra PRs:

- `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket
policy + KMS grant + `email_group_*` outputs.
- `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM
policy + outbound `webhook_url` subscription. **Based on
`ses-inbound-email`.**

Merge order: inbound first, then outbound. Outbound PR's chart edit owns
the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs).


Features lives under `/settings/general`

<img width="1496" height="845" alt="SCR-20260519-ofhi-2"
src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
2026-05-27 19:38:44 +02:00
0702e72e3f i18n - translations (#20979)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:18:37 +02:00
martmullandGitHub 0503aa7982 Display application info in workflow side panel (#20976)
To comply this design
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=99421-167676&t=GXw0bOgp2P51YVKi-0


## Before

<img width="569" height="470" alt="image"
src="https://github.com/user-attachments/assets/c52c8775-2934-4620-8885-dff5a8230a89"
/>


## After

<img width="314" height="258" alt="image"
src="https://github.com/user-attachments/assets/df9cbd15-cf7b-4a47-ac8d-1292c71a7c94"
/>
2026-05-27 16:57:48 +00:00
f98c5d0609 i18n - translations (#20978)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 19:01:41 +02:00
Félix MalfaitandGitHub 4797d2f270 feat(twenty-orm): introduce WorkspaceScopedRepository for core/metadata workspace-scoped entities (#20953)
## Summary

Adds a third tenancy enforcement layer for entities that live in shared
schemas (`core`, `metadata`) and carry a `workspaceId` column —
previously the only safeguard at this layer was developer discipline
(remembering to put `workspaceId` in every WHERE clause).

### The three layers, after this PR

| Layer | Scope | How it's enforced |
|---|---|---|
| 1. Workspace data | per-workspace schema (companies, people, custom
objects) | `twentyORMManager.getRepository(workspace, E)` — physical
isolation (own data source) |
| 2. Metadata | shared `metadata` schema (objectMetadata, fieldMetadata,
views, roles…) | Flat-entity-maps cache — workspace-scoped in-memory
map, lookups by id within it |
| 3. Core (new) | shared `core` schema (agent threads/turns/messages,
app tokens, etc.) | `WorkspaceScopedRepository<T>` — `workspaceId` is a
required positional argument on every read/write |

## What's in the PR

### The wrapper
(`packages/twenty-server/src/engine/twenty-orm/workspace-scoped-repository/`)
- `WorkspaceScopedRepository<T extends WorkspaceScopedEntity>` — wraps a
TypeORM `Repository<T>`, requires `workspaceId` on every
`find`/`findOne`/`findOneOrFail`/`update`/`delete`/`softDelete`/`insert`/`save`/`count`
call, merging it into the WHERE or stamping it on the entity.
`createQueryBuilder` is an explicit escape hatch (caller scopes
manually).
- Provided via Nest DI with
`@InjectWorkspaceScopedRepository(EntityClass)` and the
`provideWorkspaceScopedRepository(EntityClass)` provider factory.
- 19 unit tests cover the merge behavior, override-on-conflict, and the
array-where (OR) case.

### Lint enforcement
(`packages/twenty-oxlint-rules/rules/prefer-workspace-scoped-repository.ts`)
- New `twenty/prefer-workspace-scoped-repository` rule (level:
**error**).
- Blacklist of entity names: raw `@InjectRepository(E)` is rejected if
`E` is on the list.
- Initial list: `AgentTurnEntity`, `AgentMessageEntity`,
`AgentMessagePartEntity`, `AgentChatThreadEntity`,
`AgentTurnEvaluationEntity`, `AgentEntity`.
- Designed to grow over time as more consumers are migrated.
- 5 rule tests.

### Migration in this PR
All consumers of the six blacklisted entities, including:
- AI agent / chat / monitor resolvers, services, and jobs
- `AgentService`, `AiAgentRoleService`, `AiAgentWorkflowAction`,
`ApplicationService`, `WorkspaceFlatAgentMapCacheService`
- Admin-panel chat (migrated where the lookup is workspace-known; one
documented `eslint-disable` on the threadId-discovery lookup that
necessarily precedes the `allowImpersonation` permission check)
- `AiAgentRoleService` unit spec updated to mock the scoped wrapper

## Future work (deliberately not in this PR)

A standalone audit identified ~14 additional `core`/`metadata` entities
with `workspaceId` that currently use raw `@InjectRepository` and could
be added to the blacklist. Notable candidates: `UserWorkspaceEntity` (42
sites), `AppTokenEntity` (10), `FileEntity` (7),
`BillingCustomerEntity`/`BillingSubscriptionEntity` (~22 combined). Each
should be its own PR — the migration is mechanical but the surface is
wide.

## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx lint twenty-server` — 0 warnings, 0 errors
- [x] `npx jest workspace-scoped-repository` — 19/19 pass
- [x] `npx nx test twenty-oxlint-rules` — 215/215 pass
- [x] `npx jest src/engine/metadata-modules/ai` — 44/44 pass
- [ ] Manual smoke: end-to-end AI agent chat send/receive (reviewer)
- [ ] Manual smoke: AI agent monitor — list turns, run evaluation
(reviewer)
- [ ] Manual smoke: admin-panel chat thread inspection (reviewer)
2026-05-27 18:52:53 +02:00
Raphaël BosiandGitHub c8b9dace72 Fix focus in front components inputs (#20961)
Fixes https://github.com/twentyhq/twenty/issues/20714

Fixes keyboard hotkey conflicts when typing inside `<input>` /
`<textarea>` elements rendered by Front Components. Editable fields
rendered through the component renderer now properly push/pop a focus
item onto Twenty's focus stack, disabling global keyboard hotkeys while
the user is typing.

## Before


https://github.com/user-attachments/assets/2003c2cb-2698-480f-aedf-bb2f30396572


## After


https://github.com/user-attachments/assets/2c7c6cb0-ecd7-4557-a77b-4d1f264345f0
2026-05-27 16:30:49 +00:00
martmullandGitHub fbae66de8a Fix error when token invalid (#20972)
## Before

<img width="914" height="519" alt="image"
src="https://github.com/user-attachments/assets/8933bf9e-d8db-4670-ad08-69e900083fc1"
/>

## After

<img width="1105" height="523" alt="image"
src="https://github.com/user-attachments/assets/d8638dbb-adee-4b7d-9d29-1a2a0185ab4f"
/>
2026-05-27 16:30:14 +00:00
46e7f23df1 fix(contact-creation): handle common email display-name shapes when auto-creating People (#20639)
## Summary

When messages are imported, Twenty auto-creates a Person record for any
recipient that doesn't exist yet. The display-name parser used at that
point is `displayName.split(' ')[0] / [1]`, which silently mangles
several common header shapes:

| Header | Old result |

|-------------------------------------------------|-----------------------------------------|
| `"Doe, John" <...>` | `firstName="Doe,"`, `lastName="John"` |
| `"John.Doe Doe" <...>` | `firstName="John.Doe"`, `lastName="Doe"`|
| `"Mary Jane Watson" <...>` | `lastName="Jane"` ("Watson" dropped) |
| `"john.doe@x.com" <john.doe@x.com>` (forwarder) | full address in
`firstName` |
| `"Doe, John:GROUP" <...>` (group-tag servers) |
`firstName="John:GROUP"` |

This PR rewrites `getFirstNameAndLastNameFromHandleAndDisplayName` to
handle each pattern. Behaviour in order:

1. Trim + strip wrapping quotes
2. Swap `"Last, First"` comma form
3. Fall back to handle parsing when display name contains `@` (real
names don't)
4. Split single dotted tokens (`"john.doe"` → `"John"`, `"Doe"`)
5. Preserve multi-word last names (`tokens.slice(1).join(' ')`)
6. De-synthesize dot-glued first names (`"John.Doe Doe"` → `"John"`,
`"Doe"`)
7. Strip `:XXX` trailing tag suffix from each parsed field

## Test plan

- [x] 16 new unit test cases covering each shape
(`__tests__/get-first-name-and-last-name-from-handle-and-display-name.util.spec.ts`)
- [x] Lint + typecheck clean
- [ ] No regression in the messaging import flow

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-27 15:49:46 +00:00
6f3541fd7c i18n - translations (#20975)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 17:47:07 +02:00
nitinandGitHub 863f3f29a2 Fix page layout widget tab moves (#20915)
Fixes widget moves between page layout tabs by making pageLayoutTabId
part of the flat-entity diff, so the save mutation no longer silently
drops the new tab assignment.

https://discord.com/channels/1130383047699738754/1508737039128985680
2026-05-27 15:29:12 +00:00
martmullandGitHub 9264480824 Display which remote is used when twenty-sdk runs command (#20969)
## After
<img width="669" height="186" alt="image"
src="https://github.com/user-attachments/assets/c6ce25c9-35a9-4b2f-a473-59803b8931cc"
/>
2026-05-27 15:23:17 +00:00
nitinandGitHub 5ff8c3b219 revert(navigation-drawer): unwanted desktop design changes from #20634 (#20955)
Follow-up to #20634. Removes three desktop design changes that bled in
unintentionally:

- `font-weight: regular` → restored to `medium` on nav item labels
- `MenuItemIconBoxContainer` wrap around bare icons → removed
- Section title `padding-right/top` tweak → restored to original values

Mobile-specific fixes from #20634 (slide-over drawer width, min-width
overflow fixes, breadcrumb cleanup, etc.) are preserved.
2026-05-27 15:06:45 +00:00
Thomas TrompetteandGitHub ff28547a36 tt-fix-logic-function-error-handling (#20928)
## Summary
- **Fix `callWithTimeout` timer leak**: the `setTimeout` was never
cleared when the callback resolved first, leaving orphaned timers (up to
15 minutes) in the Node.js event loop. Now uses `try/finally` with
`clearTimeout`.
- **Properly classify timeout errors**: introduced
`ExecutionTimedOutError` so the Lambda driver can throw
`LOGIC_FUNCTION_EXECUTION_TIMEOUT` instead of
`LOGIC_FUNCTION_EXECUTION_FAILED` — users now see "Function execution
timed out" instead of the generic "Function execution failed."

## Test plan
- [x] Execute a logic function normally and verify it still works
- [x] Trigger a logic function timeout and verify the error message says
"Function execution timed out" (not "Function execution failed")
- [x] Verify that a deleted logic function invocation logs a specific
"was deleted" warning
- [x] Verify no orphaned timers remain after logic function execution
completes
2026-05-27 14:32:12 +00:00
Félix MalfaitandGitHub dde6df7a26 refactor(website): replace axios with native fetch (#20967)
## Summary

Follow-up to #20966 (Stripe fetch client). axios's default Node http
adapter on workerd has the same TLS hang the Stripe SDK had — it just
doesn't surface today because all three call sites are wrapped in
\`unstable_cache(revalidate: 3600)\` and the cache is populated at build
time, so misses are rare and the failure mode is a silent \`null\` to
the layout.

This swaps the three remaining axios calls in \`twenty-website\` for
native \`fetch\` and removes axios from
\`packages/twenty-website/package.json\`. The package is still used by
other workspaces, so yarn.lock keeps the other resolution.

Touched call sites:
- \`src/lib/releases/fetch-latest-release-tag.ts\` (GitHub releases —
runs at build time, cosmetic)
- \`src/lib/community/fetch-github-star-count.ts\` (GitHub star count in
menu)
- \`src/lib/community/fetch-discord-member-count.ts\` (Discord member
count in menu)

## Test plan

- [ ] After merge + deploy: confirm GitHub star + Discord member counts
render in the site menu (non-zero, formatted)
- [ ] Confirm \`/releases\` shows the latest tag-gated visible release
notes
- [ ] No \`axios\` in worker bundle (\`grep axios .open-next/worker.js\`
should be empty)
2026-05-27 16:31:39 +02:00
DeviSriSaiCharanandGitHub 92b0e07617 fix: separate initial timeline loading from fetchMore loading state (#20896)
Fixes: #20742 

# Issue:
In the timeline activity inside the side panel, when we scroll down it
fetches more data and it displays a skeleton and after fetching finishes
the scroll position always jumps back to the top. Because of this, we
have to scroll to the bottom again to load more data.



https://github.com/user-attachments/assets/40d99df7-bdfb-4351-bc4f-baec2a035f13

## Root Cause
`useTimelineActivities` exposed a single `loading` state from
`useFindManyRecords`, which became true for both:
- Initial timeline fetch
- Pagination / fetchMore requests

`loadingTimelineActivities` becomes true whenever a network request is
triggered, including pagination requests where timeline records are
already available.

Because of this, the UI could not distinguish between the first query
loading state and subsequent fetchMore loading states.

## Fix
Added a separate firstQueryLoading state to detect only the first
timeline request.

The first query is identified by checking:

- the request is still loading
- and no timeline activities have been loaded yet

Once activities are already available, any future loading state is
treated as pagination/loading more records instead of initial loading.

This allows the UI to correctly handle:

- Skeleton loaders for first load
- Infinite scroll loaders for pagination
- Empty states after loading finishes



https://github.com/user-attachments/assets/48e8e078-82e2-43d8-823f-2f71e4f4f6e1
2026-05-27 14:20:59 +00:00
951ead5a1e fix(front): ignore IME composition Enter in input hotkeys (#20958)
## What

Pressing Enter to confirm an IME (CJK) composition no longer submits the
input. The Enter / Escape / Tab handlers now ignore key events fired
while a composition is in progress (`isComposing`, or the legacy
`keyCode === 229`).

Fixes #20954

## Why

`isComposing` was not checked anywhere in `twenty-front`, so the Enter
that confirms a Japanese / Chinese / Korean conversion was also consumed
as a submit / escape / tab hotkey — making it very hard to type CJK text
into any input that submits on Enter.

## Changes

- `useHotkeysOnFocusedElement` — central guard; covers every input wired
through `useRegisterInputEvents` (~13 components) and all hotkeys routed
through this hook.
- Direct `onKeyDown` Enter handlers: `CreateWorkspace`,
`SettingsDevelopersApiKeysNew`, `SettingsAccountsBlocklistInput`.

## Notes

- No effect on non-IME (Latin) typing — `isComposing` is only true
during an active composition. It also improves accented / dead-key input
on Latin layouts.
- `react-hotkeys-hook@4` does not handle IME composition on its own, so
the guard is explicit.

## Testing

Manually verified with a Japanese IME on Chrome (macOS) against the
v2.8.3 self-hosted image: romaji + Enter now only confirms the
conversion; a second Enter on committed text submits as expected. The
GIF in #20954 shows the original buggy behavior.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:09:10 +00:00
Félix MalfaitandGitHub e79f43813b fix(website): use Stripe fetch client on Cloudflare Workers (#20966)
## Summary

Every `/api/enterprise/*` route on `twenty-website-prod` is currently
timing out at exactly 80s with `Request aborted due to timeout being
reached (80000ms)` — that's `Stripe.DEFAULT_TIMEOUT` aborting itself,
not Cloudflare.

The Stripe SDK's default transport uses Node's `http`/`https` module.
Under workerd, even with `nodejs_compat`, the outbound TLS connection to
`api.stripe.com` hangs and the SDK eventually times out at its built-in
80s ceiling. Worked on EKS (real Node), breaks on Cloudflare Workers.

Fix is one line: pass `httpClient: Stripe.createFetchHttpClient()` so
the SDK uses workerd's native `fetch` instead of the polyfilled Node
transport.

## Blast radius

`getStripeClient()` is shared across every enterprise route. All of
these are silently broken on prod right now:

- `POST /api/enterprise/checkout` (confirmed in logs)
- `POST /api/enterprise/portal`
- `POST /api/enterprise/seats`
- `GET  /api/enterprise/status`
- `POST /api/enterprise/activate`
- `POST /api/enterprise/validate`

Single change in `stripe-client.ts` unblocks all of them.

## Test plan

- [ ] After merge + deploy to prod: `wrangler tail twenty-website-prod`
while hitting the enterprise checkout flow; same call should complete in
<2s instead of 80s
- [ ] Verify a real Stripe checkout session is created (Stripe dashboard
→ Payments → Checkout sessions)
- [ ] Spot-check `/api/enterprise/status` and `/api/enterprise/portal`
are no longer timing out
2026-05-27 16:03:48 +02:00
73ab5de46c i18n - translations (#20963)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 14:59:02 +02:00
MarieandGitHub 986b9dcb3d Deprecate dummy enterprise key 1/2 (#20890)
Remove usage of hasValidEnterpriseKey in FE (replaced by
hasValidSignedEnterpriseKey)

To avoid breaking change at deploy time, we will wait until after this
has been deployed in prod, to remove hasValidEnterpriseKey in the BE.
2026-05-27 12:34:48 +00:00
ae7db23bdd i18n - translations (#20959)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 14:33:57 +02:00
WeikoandGitHub 2f358a1775 Add a Table display mode to relation field widgets (#20929)
## Context

Adds a new Table layout to the FIELD widget for to-many relation fields.
On a record page, a relation can now be displayed as a full record table
(the same component used for record indexes and dashboard table widgets)
scoped to the records related to the current record.



https://github.com/user-attachments/assets/320b24dc-f019-4d0e-bc71-3e64d032d75a



https://github.com/user-attachments/assets/2f6d4f8e-de26-4fc1-ae12-c9b9c19654dc



https://github.com/user-attachments/assets/3fb6d512-f83c-4818-823e-46ad2644fbc2
2026-05-27 12:13:58 +00:00
EtienneandGitHub fc3004b2f6 fix(website) - fix getPartners (#20947)
**Context**
Fix empty marketplace at /partners/list: page was being statically
prerendered with getPartners() returning [] (build-time fetch failure),
then served from OpenNext's R2 cache forever — so the partners API was
never actually called in production.

**Change**
Wrap getPartners in unstable_cache with revalidate: 300, matching the
existing fetchGithubStarCount / fetchDiscordMemberCount ISR pattern.
After deploy, the cached empty result expires within 5 minutes and the
worker refetches from the partners API at runtime (where the env vars
actually exist), populating the page. Also drops the now-redundant
cache: 'no-store' from partnersApiFetch.
2026-05-27 10:53:17 +00:00
086b79e5b9 i18n - translations (#20952)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 11:02:42 +02:00
Félix MalfaitandGitHub 8cb88cabee fix(role): rebind API keys + agents before deleting their role (#20935)
## Customer-reported bug

A customer hit this when using the AI chat:

```json
{
  "message": "API key 760d4822-da40-4b3f-9031-40563d7ed6c9 has no role assigned",
  "extensions": {
    "code": "INTERNAL_SERVER_ERROR",
    "userFriendlyMessage": "This API key has no role assigned."
  }
}
```

Their integration authenticates via API key. Somewhere along the way,
the role bound to that API key was deleted, leaving the API key
authenticated but role-less. Any request that hits a permission check
(`getRoleIdForApiKeyId`) blows up.

## Root cause

In `RoleService.deleteManyRoles`, the pre-deletion cleanup
(`assignDefaultRoleToMembersWithRoleToDelete`) only rebinds **user
workspaces** to the workspace default role. API keys and agents pointing
at the role are ignored. Because `RoleTargetEntity.role` declares
`onDelete: 'CASCADE'`, the FK then drops the role_target rows for those
API keys / agents — but the API keys themselves stay in `api_key`, now
orphaned in `apiKeyRoleMap`.

A previous read-side workaround
([2767ddac44](https://github.com/twentyhq/twenty/commit/2767ddac44) —
make the `role` ResolveField nullable) handled the API-key-details page,
but did not address the write paths (`getRoleIdForApiKeyId`).

## Fix

- Rename `assignDefaultRoleToMembersWithRoleToDelete` →
`rebindTargetsOfRoleToDeleteToDefaultRole` and extend it to rebind API
keys (via `ApiKeyRoleService.assignRoleToApiKey`) and agents (via
`AiAgentRoleService.assignRoleToAgent`) in the same step, before the
role is deleted.
- If the workspace default role doesn't satisfy `canBeAssignedToApiKeys`
/ `canBeAssignedToAgents`, the inner `assignRoleTo*` validation throws.
We catch that and rethrow as a `PermissionsException` with a
role-deletion-context message and two new codes —
`ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS` /
`ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS` — so the admin sees a clear
"reassign these first" prompt rather than a confusing inner error.

## Scope / non-goals

- **Already-orphaned API keys are not auto-healed.** The customer still
needs to reassign a role to their existing orphan API key via the UI
(Settings > API Keys > [the key] > role). A separate cleanup command for
existing orphans is a follow-up.
- I did not investigate *why* the customer's session was authenticated
via API key in the AI chat — that may be their integration setup. Worth
confirming with them separately.

## Test plan

- [ ] Workspace with default role `Admin` (which has
`canBeAssignedToApiKeys: true`): create an API key with a custom role,
delete the custom role → API key is rebound to Admin, requests keep
working.
- [ ] Workspace with default role `Member` (default, has
`canBeAssignedToApiKeys: false`): create an API key with a custom role,
delete the custom role → role deletion fails with the new
`ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS` error explaining the admin must
reassign first. API key + custom role are both unchanged.
- [ ] Same two scenarios for agents (`canBeAssignedToAgents`).
- [ ] Existing user-workspace rebind behavior is unchanged.
- [ ] Role deletion with no dependent API keys / agents still works.
2026-05-27 10:54:02 +02:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
ac89d2ff56 feat: raise FILES field max number of values from 10 to 60 (#20950)
## Summary

Raises the artificial hardcoded ceiling on `maxNumberOfValues` for
custom FILES fields from `10` to `60` so users can attach more files per
record.

- Bumped `FILES_FIELD_MAX_NUMBER_OF_VALUES` constant in `twenty-shared`
from `10` to `60`
- Updated validator unit test (inline snapshots + "exceeds max" case)
- Updated create/update files-field metadata integration tests and Jest
snapshots

The frontend Zod schema only enforces a `min`, so no frontend changes
are required — the backend constant is the single source of truth for
the upper bound.

Refs #20942

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-27 10:53:09 +02:00
423faa6153 i18n - translations (#20951)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-27 10:40:15 +02:00
martmullandGitHub a051490ec9 Basic app logo fixes (#20919)
as title, took the quick win fixes from
https://github.com/twentyhq/twenty/pull/20909/changes#diff-3367344412b2f44f0273d8019c1bc36396198244b9558d02921b135f62522baaR180
and leave the main fix for later as it requires an architectural update
2026-05-27 08:21:35 +00:00
34db7ac8b4 chore: sync AI model catalog from models.dev (#20948)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-27 09:19:32 +02:00
EtienneandGitHub 4a82cddad6 remove ai-model-preferences var env and config (#20859)
Split the single AI_MODEL_PREFERENCES JSON config into 4 array configs
and migrates existing workspace data.
2026-05-27 06:47:47 +00:00
Abdullah.andGitHub 9d6c5b7d58 [Website] Restore shared build dependency for typecheck (#20936)
Should fix the CI failure on `typecheck` by building twenty-shared
first.
2026-05-26 19:33:04 +02:00
Paul RastoinandGitHub a2db0b6932 Fix index cache computation (#20933)
## Introduction
Should rely on custom typeorm entity loader layer that inspects the
upgradeMigration that has bene run to dynamically request existing col
only
2026-05-26 18:55:24 +02:00
EtienneandGitHub 5eb79e7797 fix(billing) - fix orphaned stripe subs 2/2 (#20916)
Fixes https://sonarly.com/issue/40688

Should have been included in
https://github.com/twentyhq/twenty/commit/0edd8d400c646cd6a40ff0fea5342a0f61645d5e
2026-05-26 16:28:21 +00:00
Paul RastoinandGitHub f19647617a Fix cross version upgrade for 2.8 (#20927) 2026-05-26 15:46:06 +00:00
Thomas TrompetteandGitHub 424c6737a1 Fix workflow step output schema not reflecting user-renamed step name (#20922)
## Summary
- Fixes https://github.com/twentyhq/twenty/issues/20906
- Moves `markStepForRecomputation` from `useUpdateStep` into the
lower-level `useUpdateWorkflowVersionStep` hook, so **every** caller
that updates a step also triggers output schema recomputation.
- Previously, renaming a step via the side panel title input called
`useUpdateWorkflowVersionStep` directly (bypassing `useUpdateStep`), so
the variable picker kept showing the initial default name (e.g. "Create
Record") instead of the user's custom name.

## Test plan
- [x] Rename a workflow step via the side panel title input
- [x] Verify the variable picker dropdown shows the updated name
- [x] Verify variable tags/chips in subsequent steps reflect the updated
name
- [x] Verify that updating step settings (e.g. changing object type)
still refreshes the output schema correctly
2026-05-26 15:38:36 +00:00
be8c25dfc2 i18n - translations (#20930)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:46:04 +02:00
Paul RastoinandGitHub b8de469f37 Refactor and centralize file mimeType integrity check and sanitization (#20889)
# Introduction
closes https://github.com/twentyhq/private-issues/issues/484

This PR refactors the writeFile API to never expect to be passed a
mimetype, its extract is done programmatically low level so any callers
will pass through
Same for the file sanitization

## IANA override
Disclaimer for consistency we existing behavior we wanted to always have
`application/typescript`
- should we rather consider fallbacking to octect-steam instead ?
- Any pulbic assets that has .ts will now also fallback to
`application/typescript` instead of the official IANA

## Integration
Added coverage
2026-05-26 15:10:52 +00:00
c74337978b i18n - docs translations (#20926)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:25:43 +02:00
059e75e532 chore: bump version to 2.9.0 (#20925)
## 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

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-05-26 17:14:22 +02:00
bb5c2bd00c Fix/restore channel association scalar field metadata (#20920)
[#20836](https://github.com/twentyhq/twenty/pull/20836) dropped the
channel objects but even though
calendarChannelId/messageChannelId/messageFolderId already existed in
compute standard flat field, there was never an upgrade command to readd
them on the surviving association objects

so existing workspaces lost the field metadata (columns survived) and
import workers throw
```Error: Unknown error importing calendar events for calendar channel <REDACTED> in workspace <REDACTED>: Query runner already released. Cannot run queries anymore.```

This PR adds that command

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-26 14:58:33 +00:00
a2673da164 i18n - translations (#20924)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 17:03:31 +02:00
WeikoandGitHub 08682fb3f5 Various fixes - some AI findings (#20921)
App permissions tab:
- The fallback uuidv4() for a marketplace field was generated twice, so
id and universalIdentifier could diverge; it's now computed once and
reused as it seemed to be the intention (even though I don't really
think it's a good idea)
- Renamed buildobjectMetadataItemsFromMarketplaceApp →
buildObjectMetadataItemsFromMarketplaceApp to follow camelCase.

Morph relation validation:
- Fixed the user-facing message "At least one relation is require" →
"...is required"
- Typos in the related test descriptions (Morh → Morph, samefield → same
field) and their snapshots.

Docs
- The UUID field-type row in views.mdx only listed IS; updated to the
full set supported by FILTER_OPERANDS_MAP (IS, IS_NOT, IS_EMPTY,
IS_NOT_EMPTY).
2026-05-26 14:43:50 +00:00
53d4e92dda [Website] Generate release notes manifest at build time (#20913)
Removed the releases page’s runtime dependency on `fs` and
`process.cwd()` by introducing a build-time manifest generator: release
notes still live as markdown under `src/content/releases`, but a new
script now parses their frontmatter/content, validates that each note
has a release, title, and preview image (and that the image actually
exists), sorts the notes, and emits a typed `generated-release-notes.ts`
file that the app imports at runtime.

Updated the releases loader to return that generated data, changed the
menu releases preview and release JSON-LD to use explicit typed fields
(`title`, `previewImage`) instead of scraping markdown with regex at
runtime, wired the generator into Nx so it runs automatically before
`dev`, `build`, and `typecheck`, and fixed two stale image references in
the release MDX files that the new validation exposed.

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-26 16:46:11 +02:00
Rashad KaranouhandGitHub 53392f9a16 feat(twenty-partners): partnerContent catalog + TFT import improvements (#20904)
## Summary

Two related threads for the internal `twenty-partners` app:

1. **Redesign `partnerQuote` → `partnerContent`.** The object was
mis-modeled as a sales/pre-invoice doc (`amount`, opportunity link). In
TFT it's actually a marketing-content catalog — customer quotes, case
studies, partner quotes, logos — moving through a production lifecycle.
This renames it in place and reshapes it to mirror TFT's
`CustomerContent`.
2. **Import tooling improvements** to the TFT importer + multi-env
workflow.

## Changes

**Schema (`partnerContent`)**
- Rename `partnerQuote` → `partnerContent` (object, view, nav, relation
fields, identifiers).
- Add `contentType` MULTI_SELECT `[CUSTOMER_QUOTE, CASE_STUDY,
PARTNER_QUOTE, LOGO]` and `interview` LINKS.
- Add `customerCompany` / `customerPerson` relations; keep `partner`;
drop the `opportunity` link (TFT has none).
- Drop `amount`; rename the FILES field `quoteFile` → `documents`
(`attachments` is a reserved morph-relation name).

**Importer (`import-from-tft.ts`)**
- Import the full content catalog (all types), not just `PARTNER_QUOTE`.
- Map TFT `partnerTimezone` → `region`, default
`languagesSpoken=[ENGLISH]`, and set `deploymentExpertise=[SELF_HOST]`
when scope includes `HOSTING_ENVIRONMENT`.
- Filter to partner-relevant records only: opportunities linked to a
partner (20 of 164), content linked to a partner (10 of 22). Drops
general sales-pipeline / customer-only noise.
- Dedupe companies by **normalized domain** (Twenty's unique key), not
just name — fixes duplicate-entry crashes when the same company arrives
under different names.
- Progress logging throughout.

**Tooling**
- `purge-soft-deleted` script (soft-deleted rows block re-imports via
unique constraints).
- Multi-env script variants (`*:prod`) selected via `ENV_FILE`.

## Testing

Verified on a local Twenty instance and on `partner.twenty.com`:
- 122 partners, 20 partner-linked opportunities, 10 partner-linked
content (all types), 229 domain-deduped companies.
- Schema confirmed via metadata introspection; `yarn twenty typecheck`
clean.

## Notes

- Renaming an installed object isn't a pure in-place migration on a
server that already had `partnerQuote` — the working path is `uninstall
→ deploy → install` (safe here: prod had no data).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-26 14:09:57 +00:00
7e7bb81586 i18n - translations (#20911)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 12:13:49 +02:00
3703b3e2f6 i18n - translations (#20910)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-26 12:06:57 +02:00
Charles BochetandGitHub e72d10f550 chore(security): bump esbuild to ^0.28.0 to clear CVE-2025-68121 (CVSS 10.0) (#20902)
## Summary

Follow-up to #20876. That PR bumped `esbuild` to `^0.27.3` to address
the Go-stdlib CVEs the self-hoster reported, but only one of the two Go
CVEs is actually fixed at that level. This PR closes the remaining gap.

### Why 0.27.3 wasn't enough

`esbuild` ships a Go-built binary inside the `@esbuild/<platform>`
packages. The vulnerability lives in the bundled Go toolchain, not in
any JavaScript. Verified by reading the Go `buildinfo` section from
`node_modules/@esbuild/<platform>/bin/esbuild`:

- `esbuild@0.27.7` → built with **Go 1.23.8**
- `esbuild@0.28.0` → built with **Go 1.26.1**

CVE-2024-24790 (IPv6 zone parsing) is fixed in Go 1.21.11 / 1.22.4, so
0.27.x covers it.

**CVE-2025-68121** (crypto/tls cert validation bypass via TLS session
resumption, **CVSS 10.0 / Critical** per
[NVD](https://nvd.nist.gov/vuln/detail/cve-2025-68121)) is fixed only in
Go 1.24.13, 1.25.7, and 1.26.0-rc.3+. Go 1.23.x is past Go's support
window and will not receive this fix. So `esbuild@0.27.x` still ships a
Go binary that Trivy correctly flags as vulnerable.

### Reachable risk in Twenty

Low. `esbuild` does not use `crypto/tls` at runtime — it reads files,
parses, transforms, and writes. The vulnerable code path is dead code
inside the binary, present but never executed. The scan finding is what
we are clearing, not an exploitation risk.

### Fix

Bump `twenty-client-sdk`'s `esbuild` from `^0.27.3` to `^0.28.0`
(resolves to 0.28.0, built with Go 1.26.1).

### Verification

Ran `yarn workspaces focus --production twenty twenty-server
twenty-emails twenty-shared twenty-client-sdk` (the same install the
Dockerfile uses) and confirmed:

- `node_modules/esbuild/` resolves to `esbuild@0.28.0` (single copy)
- The bundled `node_modules/@esbuild/<platform>/bin/esbuild` binary
reports `go1.26.1` in its `buildinfo`

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] `nx build twenty-client-sdk` passes (esbuild's `build()` API is
stable across 0.27 → 0.28)
- [x] Production focus install shows Go 1.26.1 in the shipped binary
- [ ] CI green
- [ ] Re-run Trivy against the resulting image; confirm CVE-2025-68121
no longer appears
2026-05-26 09:49:25 +00:00
martmullandGitHub 7ca9081efa Add application installation validation modale (#20907)
## After

<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/231d4f0d-6052-4c4e-9a2a-0244d2b3832e"
/>
2026-05-26 09:48:31 +00:00
015dca95fc Edit actor chip icon style & read-only behavior (#18552)
https://github.com/user-attachments/assets/925f4380-e3e2-430d-a8e3-7e1242298900

Removed background color from icons for consistency
Removed chip hover state as chips are not navigable
Updated read-only design (text/secondary on chip hover)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
2026-05-26 09:38:39 +00:00
Paul RastoinandGitHub 076c05cbd0 File service uniformize not found behavior and stream management (#20891)
# Introduction

closes https://github.com/twentyhq/private-issues/issues/485
2026-05-26 09:23:48 +00:00
Félix MalfaitandGitHub 82c565f7dc perf(website): cache prerendered pages in CF Cache API (withRegionalCache) (#20905)
## Summary
Recovers most of the TTFB the EKS→Cloudflare migration lost on
`twenty.com`. OpenStatus's P50 chart shows the regression clearly: TTFB
went from ~50–80ms (pre-migration, CF edge cache HIT) to ~250–350ms
(post-migration, every request hits Worker → R2 → respond).

## Why the existing Cache Rule stopped working
The zone-level `Twenty Website - Aggressive cache` Cache Rule was
correctly configured and was the reason pre-migration TTFB was low. It
still exists, still has `cache: true`, Edge TTL 1d. But it doesn't apply
to Worker responses on a Worker custom domain:

- **Pre-migration** request flow: `edge → Cache Rule lookup → HIT
(~20ms) / MISS → origin → cache the response`
- **Post-migration**: `edge → Worker runs first (custom domain) → Worker
generates synthetic response from R2 → return`

Cache Rules cache responses obtained via `fetch()` from the Worker, not
synthetic responses constructed inside the Worker. OpenNext for SSG
pages reads prerendered HTML from R2 and returns it — that's synthetic.
So the rule has no insertion point.

This is structural to how CF Workers handle custom domains; not a
misconfiguration on your side.

## The fix
`open-next.config.ts`:
```ts
const incrementalCache = withRegionalCache(r2IncrementalCache, {
  mode: 'long-lived',
});
const baseConfig = defineCloudflareConfig({ incrementalCache });
```

OpenNext-native wrapper. The Worker still runs per request (~5–20ms
execution), but the ISR cache lookup goes through CF's per-region Cache
API (~5–20ms) instead of R2 (~50–150ms). For pages whose prerender
doesn't change between requests, that's the bulk of the TTFB recovered.

## Measured impact (live before/after on twenty.com today)
| URL | Before (avg of 3) | After cold (first 2 hits/region) | After
warm |
|---|---|---|---|
| `/` | 322ms | 600–640ms | **110–125ms** |
| `/pricing` | 267ms | 630–690ms | **104–110ms** |
| `/why-twenty` | 250ms | 175–270ms | **100–175ms** |

First 1–2 hits per CF region after this deploys will be slower than
baseline (regional Cache API populating from R2), then it sustains.
Steady state is significantly better than pre-fix.

## What this doesn't recover
Pre-migration `cf-cache-status: HIT` was ~20–30ms because the Worker
wasn't invoked at all. We can't get there without either:
- Moving SSG hosting off the Worker (back to a static origin Cache Rules
would cover)
- OpenNext gaining a "publish responses to caches.default" mode (doesn't
exist today)

Realistic-best on CF Workers + OpenNext is around the ~80–130ms range
we're now seeing.

## Live state
Already deployed to both prod (Version `40dfaa1a-...`) and dev (Version
`b45cc2de-...`) ahead of opening this PR, so the OpenStatus chart should
start improving immediately. This PR makes `main` reflect the change.
2026-05-26 09:20:55 +02:00
martmullandGitHub 44bae39538 Publish new cli tool version (#20903)
publish 2.8.0
2026-05-26 07:06:04 +00:00
WeikoandGitHub 1188ea9cd5 chore(server): remove unused REST→GraphQL HTTP bridge (#20897)
## Summary

Removes the unused `RestApiService` HTTP bridge that posted to
`/graphql` and `/metadata` from inside the server.
2026-05-25 19:56:32 +00:00
f721420705 i18n - docs translations (#20898)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 20:50:59 +02:00
96d247e94d i18n - translations (#20895)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 19:20:56 +02:00
WeikoandGitHub 90f711361c Add definePermissionFlag for app-defined permission flags (#20887)
## Context
Adds the SDK plumbing for apps to declare custom permission flags and
the server-side manifest pipeline to persist them.

```typescript
import { definePermissionFlag } from 'twenty-sdk/define';

export const MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER = '…';

export default definePermissionFlag({
  universalIdentifier: MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  key: 'MANAGE_INVOICES',
  label: 'Manage Invoices',
  description: 'Create, edit, and delete invoices',
  icon: 'IconReceipt',
});
```

```typescript
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
import { MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER } from './permission-flags/manage-invoices';

export default defineApplicationRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: `${APP_DISPLAY_NAME} default function role`,
  // ...
  permissionFlagUniversalIdentifiers: [
    SystemPermissionFlag.UPLOAD_FILE,
    MANAGE_INVOICES_PERMISSION_FLAG_UNIVERSAL_IDENTIFIER,
  ],
});
```

The flag can then be referenced by UUID in a role's
permissionFlagUniversalIdentifiers. On sync, the catalog row lands in
core.permissionFlag and the link in core.rolePermissionFlag.

## Not in this PR
- Runtime permission checks.
PermissionsService.getUserWorkspacePermissions still builds its result
from Object.values(PermissionFlagType), so custom flags are stored but
not yet enforced, code asking "does this role have MANAGE_INVOICES?"
won't get a meaningful answer. Widening PermissionsService and
UserWorkspacePermissions.permissionFlags to support arbitrary flag keys
is the next PR.
- PermissionFlag from apps can only define "tool" permissions and not
"settings" as a permissionType, this parameter is not mutable. This is
because "settings" are for settings page (until we might decide to
separate both type of permissions into 2 different entities) and apps
can't declare settings page or interact with them so this parameter
would be unnecessary.
2026-05-25 16:53:37 +00:00
98d47d0dd0 i18n - docs translations (#20893)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 19:02:12 +02:00
f613886511 fix(localization): parse date-only ISO strings as local midnight in relative date formatter (#20630)
## Summary

Fixes #19634

### Root Cause

The ECMAScript spec treats date-only strings (`YYYY-MM-DD`) as **UTC
midnight** when passed to `new Date()`. But `date-fns` comparison
functions (`isToday`, `isYesterday`, `isTomorrow`) operate in **local
time**. For users in UTC-negative timezones, UTC midnight April 14 is
April 13 evening locally — so the label shows "Yesterday" instead of
"Today".

### Fix

In `formatDateISOStringToRelativeDate.ts`, detect date-only strings
(length === 10) and append `T00:00:00` (no `Z`) to force local-time
parsing:

```ts
// Before
const targetDate = new Date(isoDate);

// After
const targetDate =
  isoDate.length === 10 ? new Date(isoDate + 'T00:00:00') : new Date(isoDate);
```

Full datetime strings (with time component) are left unchanged — they
already carry timezone information.

### Tests

Added `formatDateISOStringToRelativeDate.test.ts` covering:
- `Today` / `Yesterday` / `Tomorrow` labels for date-only strings
- Regression case: date-only string parsed at local midnight (not UTC
midnight)
- Full datetime strings continue to work as before

## Before / After

| Scenario | Before | After |
|---|---|---|
| `"2026-04-14"` viewed at UTC-5 on April 14 | Yesterday  | Today ✓ |
| `"2026-04-14"` viewed at UTC+0 on April 14 | Today ✓ | Today ✓ |
| `"2026-04-14T12:00:00Z"` | Today ✓ | Today ✓ |

---------

Co-authored-by: Marie Stoppa <marie@twenty.com>
2026-05-25 15:54:36 +00:00
a3e41ec267 i18n - translations (#20892)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 17:54:07 +02:00
Félix MalfaitandGitHub d602f35cbd feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary

Brings indexes management into the per-object Settings tab as a section
under Search (no feature flag, advanced mode only). Admins can create /
delete non-unique indexes with the UI; apps can declare indexes in code
with `defineIndex`. Composite-typed fields are now indexable by picking
a specific sub-column (e.g. `Address > City`).

A few related polish items also land here (invite-user dropdown lands on
the Invite tab; standard warning callout above the new-index form).

## What ships

### UI — custom indexes on per-object Settings
- New section directly under Search, wrapped in
`AdvancedSettingsWrapper`.
- Filter dropdown on the search bar toggles system-index visibility
(shown by default since advanced mode).
- **+ Add Index** button (disabled with tooltip once the per-object cap
is reached) navigates to a dedicated `SettingsObjectNewIndex` page
(matches the field-creation pattern, not a modal):
- Field picker mirrors the webhook event-form layout (rows of dropdowns,
implicit trailing empty row).
- Composite fields surface their sub-properties (`Address > City`,
`Currency > Amount`, …).
  - BTREE / GIN type selector.
- Standard warning Callout: "Use indexes sparingly — each one speeds
reads but slows writes."
- Trash icon on `isCustom: true` rows → confirmation modal →
`deleteOneIndex`.

### Server — `createOneIndex` / `deleteOneIndex` mutations
- Gated by `SettingsPermissionGuard(DATA_MODEL)`.
- `IndexMetadataService` wraps the existing migration runner via
`WorkspaceMigrationValidateBuildAndRunService` so the metadata row and
the SQL index land atomically.
- Validation: rejects empty fields, duplicate `(fieldMetadataId,
subFieldName)` pairs, fields not on the object, requires `subFieldName`
for composite parents, forbids `subFieldName` on scalar/relation,
enforces `MAX_CUSTOM_INDEXES_PER_OBJECT = 10`.
- Delete refuses on `isCustom: false` rows so system indexes can't be
removed via this API.
- Dedicated GraphQL exception handler maps each typed error to the right
transport error class.

### Composite sub-field indexing
- Adds `subFieldName: string | null` column to
`IndexFieldMetadataEntity` (fast instance command).
- The flat-entity flow (`UniversalFlatIndexFieldMetadata`,
`FlatIndexFieldMetadata`, `from-universal-flat-index-to-flat-index`,
runner column resolution) all carry `subFieldName` through.
- For composite parents, the runner uses
`computeCompositeColumnName({...}, property)` for the picked sub-column;
for non-composite parents, behavior is unchanged.
- The `'::'` separator encodes `(fieldMetadataId, subFieldName)` for
dedup on the wire; the frontend uses the same separator inside the
Select component's string value.

### Apps can declare indexes in code (`defineIndex`)
- New `IndexManifest` + `IndexFieldManifest` types in
`twenty-shared/application` wired into the `Manifest` type.
- `defineIndex` SDK helper + `IndexConfig`. CLI manifest builder +
extractor recognize `defineIndex` / `ManifestEntityKey.Indexes`.
- Server: `from-index-manifest-to-universal-flat-index` converter
resolves field IDs, validates composite/scalar `subFieldName` rules, and
delegates to `generateFlatIndexMetadataWithNameOrThrow` for the
deterministic name.
- Orchestrator wires the loop after the field-resolution pass;
per-object cap enforced inline against the manifest.
- Cascade on uninstall is automatic — when an app disappears its indexes
drop with it (universal-flat-entity diff handles it).
- Rich-app fixture ships a real `defineIndex` on `PostCard.status`,
exercising the full manifest → install path in CI.

### Closed for now (open later if needed)
- Apps cannot declare `isUnique` indexes — unique constraints stay with
the field-creation flow.
- Apps cannot use a partial-`indexWhereClause` — the UI surface keeps
the framework's hardcoded allowlist.
- UI cannot create unique or partial indexes either; same reasons.

### Cleanups along the way
- Reused the existing `getCompositeSubFieldLabel` +
`COMPOSITE_FIELD_SUB_FIELD_LABELS` (deleted the duplicates I'd created
early in the PR).
- Moved `MAX_CUSTOM_INDEXES_PER_OBJECT` to `twenty-shared/constants`
(single source for FE + BE).
- Replaced inline `isDefined(x) && x !== ''` with `isNonEmptyString`
(from `@sniptt/guards`).
- Hoisted the per-object fields Map + inlined the cap counter into the
indexes orchestrator loop (drops the install scan from O(indexes ×
totalFields) to O(totalFields + indexes)).
- Per design-feedback: page-based create flow (not a modal), filter
dropdown on the SearchInput (not a separate toggle), webhook-style
picker, field icons.

### Unrelated polish that lands here
- "Invite user" link in the multi-workspace dropdown now lands on the
Invite tab directly (`#invite`) instead of the first tab of the members
page.

## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
2026-05-25 17:47:09 +02:00
Paul RastoinandGitHub 69d89f8cfc Early return in public assets (#20881)
# Introduction
Related https://github.com/twentyhq/twenty/issues/20879

More abstracted response error and cleaner integrity check before
performing any in database search
Nothing critical patched here

Also added integration coverage to the related endpoint

Fixed the stream on error throw that would have been bubbling up into
node process

## Next
Once this has been approved will re-apply to all the existing prone
file.getBy* methods and controllers endpoints
2026-05-25 14:17:32 +00:00
Charles BochetandGitHub be39702fd2 chore(security): bump protobufjs and esbuild to clear CVEs (#20876)
## Summary

A self-hoster reported that Trivy blocks the `twentycrm/twenty:v2.7.x`
image on three fixed-critical CVEs. The reachable risk is low (none of
the vulnerable code paths are exposed to attacker-controlled input in
our deployment), but the findings are real and easy to clear by bumping
the affected dependencies in their owning workspaces.

### CVE-2026-41242 — `protobufjs` < 7.5.5

Pulled transitively into the production image via
`@opentelemetry/sdk-node`, `@opentelemetry/auto-instrumentations-node`,
and `@grpc/grpc-js` → `@grpc/proto-loader`. Lockfile was on 7.5.3; this
matches dismissed dependabot alert #1009 (Critical 9.4).

**Fix:** add `protobufjs: ^7.5.5` as a direct dep of `twenty-server`
(the workspace that exercises it via the OpenTelemetry gRPC exporters)
and run `yarn dedupe protobufjs` to collapse the residual transitive
7.5.3 copy. Resolves to 7.6.0.

### CVE-2024-24790 and CVE-2025-68121 — Go stdlib in bundled binaries

Present in the Go-built `bin/esbuild` shipped by `@esbuild/<platform>`
packages. Two paths put esbuild into the production image:

1. `twenty-client-sdk` declares `esbuild` as a runtime dep (used by its
`./generate` entry point).
2. `twenty-server` had `@lingui/vite-plugin` in `dependencies`, which
pulls `@lingui/cli` as a runtime sub-dep, which bundles `esbuild@0.21.5`
nested under `node_modules/@lingui/cli/node_modules/esbuild/`.

**Fix:**
- Bump `twenty-client-sdk`'s `esbuild` from `^0.25.0` to `^0.27.3`
(resolves to 0.27.7, built with patched Go).
- Move `@lingui/vite-plugin` from `dependencies` to `devDependencies` in
`twenty-server`. The plugin is not imported by any source file — it was
misclassified.

### Verification

Ran `yarn workspaces focus --production twenty twenty-server
twenty-emails twenty-shared twenty-client-sdk` (the same command the
Dockerfile uses) and inventoried the resulting `node_modules`. After all
three changes:

- `node_modules/esbuild/` → **0.27.7 only** (Go-patched)
- `node_modules/protobufjs/` → **7.6.0 only** (CVE-patched)

No nested copies of either package remain in the production install.

### Follow-up worth tracking separately

`esbuild` should arguably not be in `twenty-client-sdk`'s `dependencies`
at all — only the `./generate` entry point uses it, and the server never
imports that entry. Moving it to optional `peerDependencies` would stop
shipping a Go binary into the production image entirely. Out of scope
for this PR.

## Test plan

- [x] `yarn install` succeeds; `protobufjs` and `esbuild` each resolve
to a single version in production focus
- [x] `nx build twenty-client-sdk` passes
- [x] `nx typecheck twenty-server` passes
- [x] `nx build twenty-server` passes
- [x] Production focus install confirmed clean (`node_modules/esbuild`
and `node_modules/protobufjs` both single-version, both patched)
- [ ] CI green
- [ ] Re-run Trivy against the resulting image; confirm the three CVEs
no longer appear
2026-05-25 12:36:53 +00:00
EtienneandGitHub 50c8834e21 fix(billing) - skip redundant cap writes (#20882)
fixes https://sonarly.com/issue/40207
2026-05-25 12:03:18 +00:00
d54caf10c1 i18n - translations (#20885)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-25 14:10:10 +02:00
nitinandGitHub 0b38b4ffc4 fix(page-layout): respect tab layoutMode on standalone pages (#20856)
Standalone-page tabs with `layoutMode: CANVAS` were silently rendering
as GRID (border, padding, scroll). Now they render full-bleed, matching
the CANVAS contract elsewhere.

Three layered fixes:
- `getTabLayoutMode`: respect `tab.layoutMode` for `STANDALONE_PAGE`
(was hardcoded to GRID for any non-`RECORD_PAGE`)
- `getWidgetCardVariant`: CANVAS now wins regardless of page type —
refactored to early-return + exhaustive switch on `pageLayoutType`
- `FrontComponentWidgetRenderer`: removed hardcoded `overflow: auto`
(workflow/tasks/timeline widgets don't have it either)

New `getTabLayoutMode.test.ts`. Variant tests refactored to declarative
+ parameterized.

QA:

<img width="3024" height="1654" alt="CleanShot 2026-05-22 at 20 46
50@2x"
src="https://github.com/user-attachments/assets/cc61d459-6bc6-48de-ac79-d63a2ccd8957"
/>


https://github.com/user-attachments/assets/a3374e18-ad1b-4888-ab2b-d07730edccac
2026-05-25 11:59:31 +00:00
Paul RastoinandGitHub b8b115f4e3 FileStorageService Dedicated file and folder code flow + integrity check (#20831)
# Introduction

Next handling mimetype integrity check and checksum integrity check for
s3 storage type

Always expecting a trailing end slash when deleting a folder etc

## Application
Uninstalling an application now deletes all its related files

## File storage service
Making a distincton between folder path and file path

## Validation Pipeline

Every file operation in `FileStorageService.buildOnStoragePath` runs
through `validateResourcePath`, which chains three validators in order:

**1. `validateSafeRelativePath`** -- rejects path traversal attacks

| Input | Result | Error |
|---|---|---|
| `../../../etc/passwd` | Rejected | `Resource path must not contain
path traversal (..)` |
| `/etc/passwd` | Rejected | `Resource path must be relative, not
absolute` |
| `file\0.txt` | Rejected | `Resource path contains null bytes` |
| `..\\..\\etc\\passwd` | Rejected | `Resource path must not contain
backslashes` |
| _(empty)_ | Rejected | `Resource path must not be empty` |

**2. `validateFilenameIntegrity`** -- enforces safe characters, length
limits, extension required

| Input | Result | Error |
|---|---|---|
| `my folder/file.mjs` | Rejected | `A path segment contains invalid
characters...` |
| `Makefile` | Rejected | `Filename must have an extension` |
| `aaa...(256 chars).mjs` | Rejected | `A path segment exceeds the
maximum length of 255 characters` |
| `a/b/.../file.mjs` (1025+ chars) | Rejected | `Resource path exceeds
maximum length of 1024 characters` |
| `src/handlers/index.mjs` | Accepted | -- |
| `my-app/my_file.tsx` | Accepted | -- |
| `v1.0/module.config.mjs` | Accepted | -- |

Allowed characters per segment: `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`

**3. `validateResourceExtension`** -- checks extension against the
`FileFolder` allowlist

| Input | FileFolder | Result | Error |
|---|---|---|---|
| `handler.js` | `BuiltLogicFunction` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `card.tsx` | `BuiltFrontComponent` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `script.js` | `PublicAsset` | Rejected | `Invalid file extension.
Allowed extensions: .png, .jpg, ...` |
| `index.mjs` | `BuiltLogicFunction` | Accepted | -- |
| `app.tsx` | `Source` | Accepted | -- |
| `photo.png` | `CorePicture` | Accepted | -- (unconfigured folder,
passes through) |

## Consumers

- **`FileStorageService`** -- calls `validateResourcePath`, throws
`FileStorageException` on failure (last-resort defense)
- **Resolver (`uploadApplicationFile`)** -- calls
`validateResourcePath`, throws `ApplicationException` on failure
(user-facing)
- **Flat validators** -- call `validateResourcePath`, push the error to
`validationResult.errors` (non-throwing, collects all errors)

All error messages are translated via Lingui `t` and returned in a
discriminated union `{ isValid: true } | { isValid: false, error: string
}`, letting each consumer decide how to handle failures.
2026-05-25 11:52:54 +00:00
85d649e831 [Fix] Backfill missing command menu items conditional availability expression (#20852)
## Description

Following [report in
discord](https://discord.com/channels/1130383047699738754/1498690477044793386/1506602927412744242)

Some command menu items were showing to all users because they had no
conditional availability expression, whereas users did not actually have
access to the page or feature behind. For instance: "Go to Admin panel",
"Go to AI settings", "Send email" etc.

<img width="833" height="1245" alt="image"
src="https://github.com/user-attachments/assets/8d2a9404-9b81-4d58-9522-558e9924c457"
/>


## Fix

- Add conditional availability expressions
- Backfill expressions for existing workspaces as they are stored in db
(commandMenuItems table)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-23 13:22:25 +00:00
Charles BochetandGitHub 2f4ebf8160 Move preview environment workflow to ci-privileged (#20872)
## Summary

Slims `preview-env-dispatch.yaml` to a single dispatch and deletes
`preview-env-keepalive.yaml`. The actual preview-env work moves to
**twentyhq/ci-privileged#22** (must merge as a pair).

## Why

Context: PR #20867 was a credential-exfil attempt against our workflows.
GitHub's default fork-PR-no-secrets policy + our existing gates
(`author_association` checks, `pull_request_target` checking out base,
`enableScripts: false`) neutralized the actual attack — but the audit
surfaced one workflow that *would* have given a malicious external PR
access to a real secret if a maintainer had applied the `preview-app`
label: `preview-env-keepalive.yaml`.

That workflow checked out the PR head SHA, did `docker login` with
`DOCKERHUB_PASSWORD`, then ran the PR's `docker-compose.yml`. A
malicious compose could have mounted `~/.docker/config.json` and
exfiltrated the Dockerhub credential.

After this PR, that workflow lives in `twentyhq/ci-privileged` instead,
paired with a rename of the credential to `DOCKERHUB_RO_TOKEN`
(Dockerhub PAT with `Public Repo Read-only` scope). A read-only PAT has
no exfiltration value — it's equivalent to anonymous Dockerhub access
plus rate-limit headroom — so the credential lives safely on the runner
without further hygiene tricks.

## What this PR does

- **Modifies** `.github/workflows/preview-env-dispatch.yaml`:
- Single dispatch to `twentyhq/ci-privileged` (was: self-dispatch to
twenty for the env + a separate dispatch to ci-privileged for the PR
comment).
  - `permissions: {}` (was: `contents: write`).
  - Drops `preview-env-keepalive.yaml` from the path-trigger list.
- **Deletes** `.github/workflows/preview-env-keepalive.yaml`. The
207-line workflow now lives in
`twentyhq/ci-privileged/.github/workflows/preview-env.yaml`.

Net `twenty` repo change: **-204 lines / +3 lines**.

## Companion PR

twentyhq/ci-privileged#22 — adds the new `preview-env.yaml`, deletes the
now-redundant `post-preview-comment.yaml`.

## Secrets fallout in this repo

After this PR, `DOCKERHUB_PASSWORD` in `twentyhq/twenty` secrets is only
used by `ci-test-docker-compose.yaml`, where:
- It evaluates to empty for fork PRs (GitHub default — secrets aren't
passed to fork-PR workflows).
- It's only needed for internal / merge_queue runs, for Dockerhub
rate-limit headroom on base-image pulls.

Recommend (separate change): also convert the twenty-side
`DOCKERHUB_PASSWORD` to a `Public Repo Read-only` Dockerhub PAT, and
rename it to `DOCKERHUB_RO_TOKEN` for consistency with ci-privileged.
The workflow change for `ci-test-docker-compose.yaml` would just be a
rename — login flow is identical for password vs. PAT.

## Test plan

- [ ] Merge twentyhq/ci-privileged#22 first (so the dispatched event has
a handler)
- [ ] Open an internal PR touching `packages/twenty-docker/**`, confirm
`Preview Environment Dispatch` runs and ci-privileged's `Preview
Environment` workflow runs the docker compose + posts the URL
- [ ] On an external contributor PR, apply the `preview-app` label,
confirm the same flow
- [ ] Confirm closing the PR doesn't break (no cleanup workflow was
changed)
2026-05-23 11:37:37 +00:00
77603a4102 i18n - docs translations (#20869)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-23 12:47:00 +02:00
Charles BochetandGitHub 91ce59d8e2 refactor(jwt): gate signing-key auto-rotation cron on SIGNING_KEY_ROTATION_DAYS (#20866)
Only register the JWT signing-key rotation cron when
`SIGNING_KEY_ROTATION_DAYS` is set, and move that variable to Advanced
Settings.
2026-05-23 09:51:06 +00:00
martmullandGitHub 056e3a4cd8 Add check for breaking api changes (#20848)
- update ci-breaking-changes.yaml so it check for api contrat breaks
- check fails properly when removing fix
https://github.com/twentyhq/twenty/pull/20825
- check it turns green again when adding fix back
2026-05-23 08:50:05 +00:00
9876af5587 i18n - translations (#20865)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-23 10:46:12 +02:00
Charles BochetandGitHub 563acc3f57 Allow copy to clipboard and pointer/mousemove events in front components (#20858)
Follow-up to #20525, picks up the clipboard + mouse/pointer events asks
from the "Allow to copy to clipboard in front-component" Slack thread.
`navigator.geolocation` and `getBoundingClientRect` are intentionally
out of scope until we have a permission model.

### `copyToClipboard` host API

New SDK function `copyToClipboard` (in `twenty-sdk/front-component`)
that goes through the host bridge to `useCopyToClipboard` in
`twenty-front`:

```ts
import { copyToClipboard } from 'twenty-sdk/front-component';

await copyToClipboard('hello');
```

Host-side hardening (front-component code is untrusted):
- Drops anything that isn't a non-empty string
- Caps payload at 64KB
- Throttles to 1 call/sec per front-component instance
- Snackbar shows a truncated preview so the user can spot a mismatch
between the affordance they clicked and what actually got copied

### `mousemove` and pointer events

Added to `COMMON_HTML_EVENTS` (and the React mapping) so they fire on
every HTML tag the renderer ships: `mousemove`, `pointerdown/up/move`,
`pointerover/out/enter/leave/cancel`. Generator rerun for
`remote-elements.ts` and `remote-components.ts`.

`SerializedEventData` now also forwards pointer geometry: `pointerId`,
`pointerType`, `pressure`, `tangentialPressure`, `tiltX/Y`, `twist`,
`width/height`, `isPrimary`. Existing positional fields are unchanged.

### Coverage

- New Storybook stories: `HostApi/CopyToClipboard` and
`HtmlTag/Grouping/Div/Events::PointerMove`
- `useFrontComponentExecutionContext` unit tests cover the API call,
preview truncation, type guard, length cap, and rate limit
- Renderer Storybook suite 227 → 229, prebuild bundle count 219 → 221
2026-05-23 08:28:11 +00:00
Charles BochetandGitHub 452433a9ae feat(upgrade): emit twenty_upgrade_instance_info gauge with version attribute (#20854)
Adds `twenty_upgrade_instance_info` — a new "info"-style gauge that
carries the inferred instance version (derived from the last applied
upgrade migration) as the `version` label.

This follows the standard Prometheus pattern for surfacing string-valued
metadata: value is always `1` (load-bearing for PromQL `group_left`
joins), the data lives on the label. Same shape as `node_uname_info`,
`go_info`, `kube_pod_info`, etc. — see [Prometheus naming
conventions](https://prometheus.io/docs/practices/naming/#metric-names).

On `/metrics`:

```
# HELP twenty_upgrade_instance_info Inferred instance version (semver-ish, derived from the last applied upgrade migration), carried as the `version` attribute
# TYPE twenty_upgrade_instance_info gauge
twenty_upgrade_instance_info{version="2.7.3"} 1
```

Also adds `MetricsService.createInfoGauge` as the helper for the pattern
(auto-suffixes `_info`, enforces value=1). Consumed by
twentyhq/twenty-eng#65.
2026-05-23 07:19:26 +00:00
WeikoandGitHub 3bda05ea57 [Breaking change] Prepare non-system permission flags (#20847)
# Summary

Replaces the enum-keyed `permissionFlags: PermissionFlag[]` on roles
with `permissionFlagUniversalIdentifiers: string[]`

This unlocks mixing system flags (`SystemPermissionFlag.*`) with
app-defined flags in a role config.

This is a breaking change. Existing app source must switch to the new
field.

# Breaking changes

- `RoleManifest.permissionFlags` removed. Use
`RoleManifest.permissionFlagUniversalIdentifiers: string[]`.
- `RoleConfig.permissionFlags` removed (was `PermissionFlagType[]`). Use
`RoleConfig.permissionFlagUniversalIdentifiers: string[]`.
- `PermissionFlagManifest` type removed from
`twenty-shared/application`.
- `PermissionFlag` re-export removed from `twenty-sdk/define`.
`SystemPermissionFlag` is re-exported in its place.
- Retargeting a permission flag between roles is now classified as
delete + create instead of update

 ### Not in this PR
- definePermissionFlag SDK function and top-level
Manifest.permissionFlags catalog (apps defining their own custom flags).
Until those land, permissionFlagUniversalIdentifiers only accepts
SystemPermissionFlag.* UUIDs; arbitrary UUIDs fail validation.
2026-05-22 21:22:28 +00:00
EtienneandGitHub eda41b4eba feat(ai) - add observability (#20850)
**AI Chat - Tool Executions (counters, tagged with model)**
ai-chat/tool-execution-succeeded: number of tool calls invoked by the AI
that completed without error
ai-chat/tool-execution-failed: number of tool calls invoked by the AI
that threw an error
**AI Chat - Token Usage (counters, tagged with model)**
ai-chat/input-tokens: total input tokens sent to the model across all
turns
ai-chat/output-tokens: total output tokens generated by the model
ai-chat/cache-read-tokens: input tokens served from the model's prompt
cache (cheaper)
ai-chat/cache-write-tokens: input tokens written into the prompt cache
for future reuse
**AI Chat - Latency (histograms in ms, tagged with model)**
ai-chat/turn-latency-ms: total duration of a full chat turn (from stream
start to stream end)
ai-chat/step-latency-ms: duration of a single reasoning/tool-call step
within a turn
ai-chat/ttft-ms: time-to-first-token, i.e. how long until the model
starts streaming output
**MCP - Tool Executions (counters)**
mcp/tool-execution-succeeded: number of MCP tool calls that completed
successfully
mcp/tool-execution-failed: number of MCP tool calls that threw an error
2026-05-22 15:32:51 +00:00
Félix MalfaitandGitHub de044f4b45 feat(ai-chat): add navigation menu item + webhook tool providers (#20759)
## Summary

Exposes two Twenty primitives to the AI chat that it could not
previously manage:

- **Navigation menu items** — workspace nav and personal favorites
(favorites are just nav items with `scope: 'user'`).
- **Webhooks** — full CRUD with a structured operations input (record +
metadata events).

Page layouts and workflow runs were originally in this PR but have been
split out — they touch heavier surfaces (21 widget configurations and
the workflow runner cycle, respectively) and deserve their own focused
PRs.

### Tool inventory (8 new tools across 2 providers)

| Provider | Tools |
|---|---|
| NavigationMenuItem | `list_`, `create_`, `update_`,
`delete_navigation_menu_item` |
| Webhook | `list_`, `create_`, `update_`, `delete_webhook` |

### Design notes

- Both providers follow the established **view-style pattern**: tool
workspace service lives in the entity module's `tools/` folder, is
provided + exported by the entity module, and `ToolProviderModule`
imports the entity module. No `@Global()` modules or injection tokens
introduced.
- `create_navigation_menu_item` uses a Zod `discriminatedUnion` on
`type` (`FOLDER` / `LINK` / `OBJECT` / `VIEW` / `RECORD` /
`PAGE_LAYOUT`). `scope: 'workspace' | 'user'` switches between shared
nav and personal favorites — the underlying
`NavigationMenuItemAccessService` enforces LAYOUTS for workspace writes.
- Webhook operations accept both record events (`{kind:'record', object,
event}` → `<object>.<event>`) and metadata events (`{kind:'metadata',
metadataName, operation}` → `metadata.<metadataName>.<operation>`).
- Permissions reuse existing flags (`LAYOUTS`, `API_KEYS_AND_WEBHOOKS`).
No new permission flags, no migrations.

### Category cleanup

- New: `ToolCategory.NAVIGATION_MENU_ITEM`, `ToolCategory.WEBHOOK`.
- `ToolCategory.VIEW_FIELD` → folded into `VIEW`. Same permission gate,
same domain — separate category was organizational drift.
- `navigate_app` action stays in `ToolCategory.ACTION` where it belongs.

### System prompt addition


[chat-system-prompts.const.ts](packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts)
now teaches the AI:
- Favorites are nav items with `scope: 'user'`.
- A default OBJECT nav item is auto-created with
`create_object_metadata` — don't double-create.

### One file = one export

Every new schema / type / util file has exactly one top-level export.

## Test plan

- [ ] `npx nx typecheck twenty-server` — passes
- [ ] Spin up locally and exercise via AI chat:
- [ ] "Pin the Companies view to my favorites in a folder called
Important." → `create_navigation_menu_item` (FOLDER, user) then (VIEW,
user, folderId)
- [ ] "Register a webhook to https://example.com firing when any person
is created or updated." → `create_webhook` with discriminated operations
- [ ] Verify workspace-scoped nav writes are denied for a user without
LAYOUTS permission
- [ ] Verify user-scoped nav writes work without LAYOUTS permission

## Follow-ups (separate PRs)

- Page layout tools (record-page, record-index, standalone) — needs
widget-config strategy.
- Workflow run tools (list, get, run, stop) — uses the workflow-runner
cycle path.
- Dashboard / page-layout tool unification —
`DashboardToolWorkspaceService` and a future
`PageLayoutToolWorkspaceService` both inject the same trio
(PageLayout/Tab/Widget services).
- Webhook Settings page reads from raw Apollo query — switch to the
metadata store so it refreshes when the AI mutates webhooks.
2026-05-22 17:27:06 +02:00
e3c79c803c Fix standard React form event targets in front components (#20525)
Fixes #20354

## Problem

Front component form events currently expose serialized form state
through a sandbox-specific event shape, such as `event.detail.value` and
`event.detail.checked`.

That works for examples that explicitly read `event.detail`, but it is
surprising for app authors writing standard React form handlers:

```tsx
onChange={(event) => {
  setValue(event.target.value);
}}
Internal app code already has to defend against multiple possible shapes:

// Values may live on e.detail.value, e.value, or e.target.value.
This suggests the sandbox event shape is leaking into userland.

Solution
This change keeps the existing event.detail behavior, but also syncs serialized event target properties back onto the remote element before dispatching the event.

That means both styles work:

// Existing sandbox-specific style
event.detail.value;

// Standard React style
event.target.value;
The same applies to checked, files, scroll/media target properties, and similar serialized target state.

What Changed
Added a shared helper to apply serialized event target properties onto the remote element.
Updated generated remote element event configs to dispatch serialized events through a custom event config.
Updated the remote-dom element generator so regenerated files preserve this behavior.
Updated Storybook form-event examples to use standard React event target reads.
Added/updated Storybook coverage for input, checkbox, textarea, select, submit, and caret preservation flows.
Validation
Ran git diff --check
Ran a targeted TypeScript error scan for the changed front component renderer files
Manually verified the Storybook FrontComponent/EventForwarding form event story locally:
text input updates state
checkbox updates state
submit reflects the updated JSON
Note: local Storybook verification on Windows required temporary local build/cache fixes that are not included in this PR, to keep this change focused on front component event behavior.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-22 14:58:33 +00:00
nitinandGitHub 59d69e2f5c fix(sdk): link dev UI to workspace application detail page (#20849)
sdk handle auth of one workspace per session -- but server could be
configured as multi or single -- hence for multi get subdomain -- and
for single the localhost fallback!
also: link includes applicationId so it opens the app detail page
directly (not the list)

## QA
multi workspace flag on -

<img width="2996" height="1712" alt="CleanShot 2026-05-22 at 18 21
31@2x"
src="https://github.com/user-attachments/assets/8499b9f3-b22e-45e2-8b97-4b27fadc3c94"
/>

multi workspace flag off - 

<img width="3012" height="1734" alt="CleanShot 2026-05-22 at 18 14
37@2x"
src="https://github.com/user-attachments/assets/3af2f492-5e2d-4a4b-8251-c3343d79ae9e"
/>
2026-05-22 14:19:27 +00:00
neo773andGitHub 4554dbe3c9 make connectedAccount clients self contained (#20827)
Replace OAuth2ClientManagerService with per provider each loading their
own entity and resolving tokens internally

Removes the ugly spread pattern of sprinkling tokens everywhere, this
caused downtime of messaging when we migrated to encrypted tokens
2026-05-22 14:17:07 +00:00
martmullGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
efba71a938 Download tarball instead of yarn install when installing application (#20835)
as title

Tested on Vexa public application install
- before -> 5.4s
- after -> 2.55s

Tested on Exa public application install
- before -> 5.5s
- after -> 2.24s

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-22 14:08:12 +00:00
e6cc783a23 i18n - translations (#20853)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 15:56:56 +02:00
76e144e85a Deprecate messageChannel messageFolder calendarChannel standard objects (#20836)
# Introduction

Removing old standard objects `messageChannel` and `messageFolder` and
`calendarChannel`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 13:40:53 +00:00
WeikoandGitHub 8f24cda586 Fix permission flag removed flag property from diffing (#20845)
## Why it fixes the bug

RolePermissionFlagEntity.flag (role-permission-flag.entity.ts:54) is
marked @WasRemovedInUpgrade for
2.7.0_FinalizeRolePermissionFlagCutoverFastInstanceCommand.
After 2.7.0, the column is gone from real DBs and the metadata layer no
longer accepts writes to it — but the diffing config still listed flag
with toCompare: true. So when an SDK-generated manifest carried a flag
value, computeUniversalFlatEntityPropertiesToCompareAndStringify
(all-universal-flat-entity-properties-to-compare-and-stringify.constant.ts:55-69)
included it in the comparison, the diff emitted { update: { flag:
"UPLOAD_FILE" } }, and the metadata update failed with Property "flag"
was not found in "RolePermissionFlagEntity".

Switching toCompare: false makes the diff skip flag; the only properties
compared are now permissionFlagUniversalIdentifier and
roleUniversalIdentifier, which is what the post-cutover
entity actually supports.

Fixes https://github.com/twentyhq/twenty/issues/20843
2026-05-22 13:20:27 +00:00
c2a48a1cc2 i18n - docs translations (#20839)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 13:22:12 +02:00
37a1d3980f i18n - translations (#20838)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 12:26:58 +02:00
nitinandGitHub 05f58b2dba Fix onboarding modals spacings (#20682)
closes
https://discord.com/channels/1130383047699738754/1496415056085389422
2026-05-22 10:06:18 +00:00
Abdullah.andGitHub 4e0b69eb8d [Website] force-static releases page & move workspace bullet to pro self-host (#20834)
Add `export const dynamic = 'force-static'` to the releases page to
prevent runtime re-renders on Cloudflare Workers where fs is
unavailable, which caused stale "Releases were not found" errors after
ISR cache eviction.

Also moves the "Up to 5 workspaces" bullet from the Organization plan to
the Pro plan on the self-hosting pricing view.
2026-05-22 09:58:22 +00:00
99799073e9 i18n - translations (#20837)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 12:04:44 +02:00
Félix MalfaitandGitHub 084fa8eaba fix(server): auto-index target<X>Id join columns on polymorphic standard objects (#20820)
Closes #20726

## The bug

`timelineActivity` (and the three other polymorphic standard objects —
`attachment`, `noteTarget`, `taskTarget`) store relations as N nullable
`target<X>Id` columns, one per related object. Each one is a join key
queried as `WHERE target<X>Id IN (...) AND deletedAt IS NULL`.

For **built-in** related objects (Person, Company, Opportunity, …), each
`target<X>Id` column gets a BTREE index, declared statically in
`compute-{timelineActivity,attachment,noteTarget,taskTarget}-standard-flat-index-metadata.util.ts`.

For **custom** related objects, the same `target<CustomObject>Id` column
was added — **without an index**. On a `timelineActivity` table at
issue-reporter scale (~21.9M rows, 7.1 GB), this turned record loads
into 20–40s sequential scans and produced `QueryFailedError: Query read
timeout` for end users.

## Diagnosis

The morph/relation field generator
(`generateMorphOrRelationFlatFieldMetadataPair`) already creates a BTREE
index for the field that owns the join column and returns it alongside
the field metadata pair. The two user-driven entry points
(`fromRelationCreateFieldInput…`, `fromMorphRelationCreateFieldInput…`)
correctly destructure and propagate that index.

But the **custom-object creation path** —
`buildDefaultRelationFlatFieldMetadatasForCustomObject`, called when a
user creates a new custom object — destructured only `{
flatFieldMetadatas }` and threw away `indexMetadatas`. So every
`target<CustomObject>Id` column added to the four polymorphic standard
objects has been shipping unindexed since custom morph relations went
in.

## The fix

Three commits.

### 1. `fix(server): index target<CustomObject>Id columns on standard
polymorphic objects`

13 lines across 2 files.

-
`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`
— also destructure `indexMetadatas` from the pair generator and
accumulate them into the returned record (new field
`standardTargetFlatIndexMetadatas`).
-
`from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts`
— append the accumulated indexes to `flatIndexMetadataToCreate`. The
migration pipeline at `object-metadata.service.ts:559–562` already
passes `flatIndexMetadataToCreate` to the migration runner, so no
further wiring is needed.

From now on, creating a custom object also creates the four BTREE
indexes — one per polymorphic standard object's new
`target<CustomObject>Id` column — atomically with the rest of the
migration.

### 2. `feat(server): backfill workspace command for relation join
column indexes`

For existing workspaces whose custom objects were created before the
forward-fix.

`upgrade:2-8:backfill-relation-join-column-indexes` is a
`@RegisteredWorkspaceCommand('2.8.0', 1798100000000)` matching the
pattern from
`2-7-workspace-command-…-drop-connected-account-standard-object.command.ts`.

Per workspace:
1. Load `flatObjectMetadataMaps`, `flatFieldMetadataMaps`,
`flatIndexMaps` from the workspace cache.
2. Resolve the four polymorphic standard object IDs by `nameSingular`
against `DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS`.
3. Collect every field ID that's already covered by any existing index.
4. Filter `flatFieldMetadataMaps` to MORPH_RELATION fields on those four
objects whose `settings.relationType === MANY_TO_ONE` (i.e. owns a join
column) and whose ID isn't in the indexed set.
5. Generate a BTREE `UniversalFlatIndexMetadata` for each via
`generateIndexForFlatFieldMetadata` (same helper the forward-fix uses).
6. Create the indexes in the workspace schema with **CONCURRENTLY** (see
commit 3).
7. Submit the metadata through
`WorkspaceMigrationValidateBuildAndRunService` so it lands in
`indexMetadata` and the cache — same pipeline as a normal metadata
change. The pipeline's own `CREATE INDEX IF NOT EXISTS` no-ops because
the index already exists.

Properties:
- **Idempotent.** Re-running is a no-op once indexes exist.
- **Scoped.** Only the four polymorphic standard objects, only their
MANY_TO_ONE morph relation fields, only those with no covering index.
- **Same code path as the forward-fix.** The backfill produces exactly
the indexes the forward-fix would have created at custom-object creation
time.
- **`--dry-run` supported** via the base
`ActiveOrSuspendedWorkspaceCommandRunner`.

### 3. `feat(server): create index CONCURRENTLY in relation join column
backfill`

Adds an opt-in `concurrently` flag to
`WorkspaceSchemaIndexManagerService.createIndex` (threaded through
`createIndexInWorkspaceSchema`). When `true`, emits `CREATE INDEX
CONCURRENTLY IF NOT EXISTS …`. Defaults to `false` — every existing
caller keeps the current transactional `CREATE INDEX` behavior.

The backfill command opts in. It creates a QueryRunner **without**
`startTransaction()`, issues the CONCURRENTLY indexes one-by-one (each
waits for the previous to finish), then submits the metadata through the
normal migration pipeline whose own `CREATE INDEX IF NOT EXISTS` is now
a no-op.

Why not flip the default for the helper:
- `CREATE INDEX CONCURRENTLY` cannot run inside a transaction — Postgres
errors out. The migration pipeline calls `createIndex` from inside a
transactional schema migration.
- CONCURRENTLY doesn't roll back with the transaction. If the
surrounding migration fails, the index remains and you end up with
metadata/schema drift.
- Failed CONCURRENTLY builds leave an INVALID index behind that needs
manual `DROP`.
- UNIQUE indexes have different failure semantics under CONCURRENTLY
(deferred, not immediate).

So CONCURRENTLY is opt-in, used only where it's the right tool (post-hoc
backfills on populated tables).

## Decisions / tradeoffs

- **Single-column BTREE vs partial `WHERE deletedAt IS NULL` vs
composite.** Twenty's queries always include `deletedAt IS NULL`. A
partial index would be slightly better than a plain BTREE (smaller, no
wasted seeks on soft-deleted rows). This PR ships single-column to match
the existing built-in target index pattern, which already covers >95% of
the available speedup (the 20s→4ms drop the reporter saw comes from
having any index — composite/partial is a second-order effect).
Switching all relation indexes to partial is a separate, broader change.
- **CONCURRENTLY operator caveat.** If a CONCURRENTLY build is
interrupted (kill, connection drop, OOM), Postgres leaves the index as
INVALID. We deliberately don't probe `pg_index` for invalid leftovers on
every create — catalog-table queries can be slow at multi-tenant scale
and the failure mode is rare. Recovery is manual: `DROP INDEX <name>`
and re-run the backfill.
- **Forward-fix is not gated** behind a feature flag. The change is
metadata-pipeline-internal; before, custom-object creation silently
produced a degraded state. After, it produces the correct state. No new
public API, no behavioural change for end users besides the indexes
existing.

## Risk

- Forward-fix: changes only the metadata produced during custom-object
creation. New objects get four extra `FlatIndexMetadata` rows and four
extra `CREATE INDEX` statements during their creation migration. Tables
are empty at that point so the index builds in microseconds.
- Helper change: API-compatible, default behavior unchanged. The new
`concurrently` parameter is optional.
- Backfill: read-only state probe → CONCURRENTLY index creation (no
write blocking) → metadata insert via the normal migration pipeline.
Idempotent. Reverting is `DROP INDEX`.

## Test plan

- [ ] Verify forward-fix: create a custom object, confirm four new BTREE
indexes appear on `timelineActivity`, `attachment`, `noteTarget`,
`taskTarget` for the new `target<CustomObject>Id` columns, and that
`flatIndexMaps` has matching entries.
- [ ] Verify backfill on a workspace that had custom objects created
before the fix: run `--dry-run` first, confirm the expected indexes are
listed; then run for real, confirm the indexes appear in pg (and as
`indisvalid = true` in `pg_index`) and in `flatIndexMaps`. Re-run;
confirm no-op.
- [ ] Verify backfill on a clean workspace: should log "no missing
indexes" and exit.
- [ ] Verify CONCURRENTLY behavior under load: run backfill against a
workspace with active writes on `timelineActivity`; confirm
inserts/updates keep working during index build (no `ShareLock` waits in
`pg_stat_activity`).
- [ ] On the affected reporter-scale workspace, confirm `EXPLAIN
ANALYZE` switches from sequential scan to index scan and timeline
activity timeouts go away.
2026-05-22 11:56:33 +02:00
788d120b71 i18n - docs translations (#20833)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 11:30:11 +02:00
3128157988 i18n - translations (#20832)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-22 11:19:10 +02:00
nitinandGitHub 068d365731 feat(sdk): error on incompatible view filter operand at sync time (#20763)
view filters with mismatched operand + field type now error at sync --
was silently failing before
2026-05-22 09:01:40 +00:00
9b05e7fff4 chore: sync AI model catalog from models.dev (#20830)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-22 09:06:07 +02:00
323e66433e lint: migrate prettier to oxfmt (#20783)
Most changes are `implements` being unwrapped this is not a oxfmt
regression
Prettier in 3.7 (we're on 3.1) changed this behaviour prettier blog
[post](https://prettier.io/blog/2025/11/27/3.7.0#change-18094)

This unifies our linting tooling

---------

Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-22 00:21:33 +02:00
e2ee4ffdff Fix: Added description below application variable (#20781)
Fixes #20757 

Before: 
<img width="655" height="777" alt="Screenshot 2026-05-21 at 12 51 25 AM"
src="https://github.com/user-attachments/assets/d164d2b8-22e6-43b4-9079-a4c33324d7dc"
/>

After:
<img width="655" height="777" alt="Screenshot 2026-05-21 at 12 50 52 AM"
src="https://github.com/user-attachments/assets/0f83062f-7703-43b9-a049-0164f5e7d9cd"
/>

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-05-21 21:14:42 +00:00
martmullandGitHub 9a7f50fcb7 Fix breaking change in install app command (#20825)
add backward compatibility for twenty-sdk install command
2026-05-21 20:58:34 +00:00
c721fa8502 Add editor mode for text field widgets (#20779)
## Summary

Tested ↓

<img width="3456" height="1990" alt="CleanShot 2026-05-20 at 21 14
04@2x"
src="https://github.com/user-attachments/assets/b4e0d3d3-715f-4ad7-bd03-e8e1922b3c6c"
/>


- Enable `FieldDisplayMode.EDITOR` for plain `TEXT` field widgets while
keeping `FIELD` as the default display mode.
- Add a plain multiline text editor renderer for `TEXT + EDITOR` field
widgets with optimistic record-store/cache updates and debounced
persistence.
- Reuse the shared `TextArea` component through a transparent, uncapped
variant so the editor has no input chrome and lets the widget grow.
- Add unit coverage for text display-mode config and a Storybook
scenario for a text field widget in editor mode.

## useEffect cleanup note

`FieldWidgetTextEditor` flushes the debounced persist callback in a
`useEffect` cleanup:

```ts
useEffect(() => () => persistTextDebounced.flush(), [persistTextDebounced]);
```

This follows the existing debounced autosave cleanup pattern already
used in `WorkflowEditActionHttpRequest`. It ensures pending text changes
are persisted when the widget unmounts, while `onBlur` still flushes
immediately for normal editor exits.

## Validation

- `npx nx test twenty-front --testPathPattern=page-layout`
- `npx nx typecheck twenty-front`
- `npx nx lint twenty-front`
- Browser check on
`http://apple.localhost:3001/object/company/20202020-a305-41e7-8c72-ba44072a4c58`
for transparent textarea, no internal max-height/scroll, equal padding,
and widget growth.

Note: lint passes with two unrelated existing warnings in
`NavigationDrawerItem.tsx` and `ConfigVariableEdit.tsx`.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-21 22:29:29 +02:00
b0a1002838 i18n - website translations (#20824)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 21:39:23 +02:00
neo773andGitHub 1e2ae5342b Send Email UI IMAP/SMTP message threading fix (#20784)
**Problem:**

When using Twenty Send Email UI IMAP/SMTP message threading is broken on
Twenty side as well as recipient email client

**Twenty side fix:**

- SMTP has no concept of `externalThreadId` sendEmail resolver always
returns null, this breaks threading
Fix is to pass `parentThreadExternalId` to
`resolveOutboundThreadExternalId` for SMTP/IMAP path

**Recipient email client fix:**

- Fetch associated threads as per RFC spec to write `References` header
 ```
From: johndoe@domain.com
To: janedoe@domain.com
Subject: Test
References: <root@...> <mid1@...> <parent@...>
```
2026-05-21 19:16:27 +00:00
Rashad KaranouhandGitHub 11b9f708d6 feat(twenty-partners): add partners app (#20792)
## Summary

- Adds `twenty-partners`, a Twenty app that manages the partner matching
pipeline: intake partner-eligible deals, assign vetted marketplace
partners, and track the full funnel
- Custom `Partner` object with availability, geo/language coverage,
deployment expertise, and Calendly link
- `matchStatus` SELECT field on Opportunity — 10 non-nullable states
from `TO_BE_MATCHED` through `WON`/`LOST`, replacing a legacy boolean
approach
- Auto-match logic function: when `matchStatus` → `AUTO_MATCH`, assigns
the longest-idle available partner and advances to `MATCHED`; falls back
to `MANUAL_MATCH` with an audit note if no partner is free
- Views: Waiting for match, Matches overview (Kanban by `matchStatus`),
All matched deals, Partners, Opportunities
- Roles: Partner Ops (internal, full CRUD) and Partner (external
placeholder)
- Idempotent seed scripts for demo partners and pipeline data

## Test plan

- [ ] App installs cleanly on a fresh workspace (`yarn twenty dev`)
- [ ] `matchStatus` Kanban grouping renders correctly in Matches
overview
- [ ] Waiting for match view filters to `TO_BE_MATCHED` and
`MANUAL_MATCH` only
- [ ] Auto-match logic assigns a partner and advances status
- [ ] Seed scripts run without errors and are safe to re-run
2026-05-21 19:14:20 +00:00
Rashad KaranouhandGitHub 07a20cba5e [Website] Partners directory (#20632)
## What this PR does

Adds the **Partners Marketplace** page to the Twenty marketing website
(`/partners-marketplace`), built with Next.js App Router. The page
fetches live partner data from the Twenty API and presents it in a
responsive grid with an interactive filter bar.

## Partners grid

- Fetches partners from the `/s/partners` endpoint via a typed
`getPartners()` server-side fetcher
- Responsive 1 → 2 → 3 column grid (mobile / tablet / desktop)
- Each card shows name, region eyebrow, intro text, chip rows (Regions /
Languages / Deploys), and a Calendly CTA
- Stagger entrance animation (700ms cascade, respects
`prefers-reduced-motion`)

## Filter bar

- Three facets: **Region**, **Language**, **Deployment** — multi-select
chips
- **Selection model:** OR within a facet, AND across facets (e.g.
`Europe OR US` AND `French`)
- Filter state lives in URL search params
(`?regions=EUROPE,US&languages=FRENCH`) — filtered views are shareable
and browser-back works correctly
- Client-side filtering — no server round-trip per interaction
- Result count ("Showing 3 of 8 partners") updates live with
`aria-live="polite"`
- "Clear filters" button resets all facets in one URL update, only shown
when filters are active
- Empty state ("No partners match your filters") replaces the grid when
nothing matches
- 200ms opacity fade-out on card removal; initial stagger animation
preserved on first load
- `prefers-reduced-motion: reduce` disables all transitions

## Architecture

- `page.tsx` stays a **Server Component** — fetches partners
server-side, all partner HTML is in the initial response for SEO
- `<MarketplaceClient>` is the client boundary — owns filter state via
`useFilterState()` (backed by `useSearchParams`)
- Canonical URL set in page metadata so `?regions=...` deep-links don't
get indexed as duplicates
- `<Suspense>` wrapper around `MarketplaceClient` for Next.js 15
`useSearchParams` compliance
- No new npm dependencies

## Test coverage

31 tests across three suites:
- `filter-partners.test.ts` — pure filter logic (OR / AND semantics,
empty results)
- `filter-url-helpers.test.ts` — URL param encode / decode / toggle /
round-trip
- `use-filter-state.test.tsx` — hook behaviour with mocked
`next/navigation`

## Screenshot

<img width="1783" height="1196" alt="Screenshot 2026-05-17 at 15 01 54"
src="https://github.com/user-attachments/assets/9dddf827-f440-4cad-8ec3-81ede6d46434"
/>


## Test plan

- [ ] Navigate to `/partners-marketplace` — all live partners render
- [ ] Click a Region chip — URL updates with `?regions=...`, cards
filter, count updates
- [ ] Click the same chip again — selection removed, all cards return
- [ ] Select chips from two different facets — AND behaviour narrows
results correctly
- [ ] Trigger empty state (e.g. filter to a region with no partners) —
empty state shown with "Clear filters" button
- [ ] Click "Clear filters" — all cards return, URL cleared
- [ ] Deep-link to `?regions=EUROPE&languages=FRENCH` — page loads with
filters applied
- [ ] Browser back button restores previous filter state

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-21 19:13:14 +00:00
138eb5a74a Add empty operands to UUID filter type in workflow filter action (#20821)
## Summary
- Adds `IS_EMPTY` and `IS_NOT_EMPTY` operands to the UUID entry in
`getStepFilterOperands`, aligning the workflow filter action with the
find records (search) action which already includes these operands for
ID-type fields.

## Test plan
- [ ] Open a workflow with a filter action, select an ID-type field, and
verify the operand dropdown now includes "Is empty" and "Is not empty"
- [ ] Open a workflow with a find records action, select an ID-type
field, and verify the operand dropdown is consistent with the filter
action

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-21 19:06:10 +00:00
a66fae79ee i18n - docs translations (#20823)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 20:58:59 +02:00
Charles BochetandGitHub edf2bfbf76 feat(server): rotate connectedAccount.connectionParameters via secret-encryption:rotate (#20807)
## Summary

Adds a new `connected-account-connection-parameters` site to the
`secret-encryption:rotate` CLI introduced in #20613, so the nested
password envelopes inside `connectedAccount.connectionParameters` (IMAP
/ SMTP / CALDAV — encrypted at-rest in #20673) are re-encrypted under
the current `ENCRYPTION_KEY` alongside every other at-rest secret site.
Without this, rotating `ENCRYPTION_KEY` on a 2.7+ instance would
silently leave IMAP / SMTP / CalDav passwords on the old key id.

### Why a new handler

A dedicated handler is required (rather than reusing
`ColumnRotationSiteHandler`) because the envelope lives at
`connectionParameters->'<PROTOCOL>'->>'password'`, not in the whole
column, and up to three independent envelopes may need rotating per row.
The handler:

- Uses the same cursor-based, idempotent, online pattern as the existing
handlers, with a SQL predicate that skips rows where every non-null
protocol password is already on the current key id.
- Threads \`workspaceId\` into HKDF, matching how
\`EncryptConnectionParametersSlowInstanceCommand\` backfilled.
- Rebuilds only the protocols whose passwords are not yet current, so a
partial mid-row failure cannot cause unnecessary re-encryption on
resume.
- Guards the UPDATE with jsonb-level deep equality (\`IS NOT DISTINCT
FROM CAST(:json AS jsonb)\`) so optimistic concurrency is unaffected by
Postgres's internal jsonb key ordering vs. JSON.stringify ordering.
- Refuses to rotate plaintext passwords (counted as \`errors\`) —
operators must finish the 2.7 slow instance command
(\`EncryptConnectionParametersSlowInstanceCommand\`) before running
rotation.

### Sites covered (now)

| Site | Location | Scope |
| --- | --- | --- |
| \`connected-account-access-token\` | \`connectedAccount.accessToken\`
| workspace |
| \`connected-account-refresh-token\` |
\`connectedAccount.refreshToken\` | workspace |
| **\`connected-account-connection-parameters\`** (new) |
\`connectedAccount.connectionParameters.{IMAP,SMTP,CALDAV}.password\` |
workspace |
| \`application-variable\` | \`applicationVariable.value\` (isSecret) |
workspace |
| \`application-registration-variable\` |
\`applicationRegistrationVariable.encryptedValue\` | instance |
| \`signing-key-private-key\` | \`signingKey.privateKey\` | instance |
| \`totp-secret\` | \`twoFactorAuthenticationMethod.secret\` | workspace
|
| \`sensitive-config-storage\` | \`keyValuePair.value\` (sensitive
STRING configs) | instance |
2026-05-21 19:31:52 +02:00
64241ed8ae i18n - translations (#20822)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 19:26:28 +02:00
EtienneandGitHub 0edd8d400c fix(billing) - fix orphaned stripe subs (#20814)
Fix sentry issues
https://twenty-v7.sentry.io/issues/7203797925/?environment=prod&project=4507072499810304&query=is%3Aunresolved%20assigned%3Ame&referrer=issue-stream

An orphaned sub is a not "canceled" stripe sub with no matching
workspace

- Clean all orphaned sub (script not included in this PR)
- Ensure to soft delete > cancel stripe sub > check for not active sub >
hard delete in every workspace deletion flow

Bonus : 
- Remove dead code
- Update doc on RLS (to improve AI chat knowledge)
2026-05-21 17:02:54 +00:00
Paul RastoinandGitHub 3c91f3f276 fix(server): encrypt token post refresh (#20819)
# Introduction
Jobs were refreshing token and returning them as plain text, resulting
to underlying code flow failure as expecting encrypted tokens

## Next
We should define a strong typescript signature to avoid such things to
happen again, or least have an explicit naming
2026-05-21 18:53:03 +02:00
0bfcd9a701 fix(api-keys): refresh API keys list after key creation (#20806)
New API keys are created successfully but the API keys table can keep
showing a stale pre-create result, so users think the key vanished. This
blocks key management from the expected UI flow.

Fix: Updated the API key creation mutation to explicitly synchronize
Apollo cache for the API keys list:

- In `SettingsDevelopersApiKeysNew.tsx`, imported `GetApiKeysDocument`.
- Changed `useMutation(CreateApiKeyDocument)` to:
  - `refetchQueries: [GetApiKeysDocument]`
  - `awaitRefetchQueries: true`

Why: the list page (`SettingsApiKeysTable`) reads from
`GetApiKeysDocument`, and creation previously did not invalidate/refetch
that query. With this change, successful creation refreshes the list
query so the new key appears when the user returns to APIs & Webhooks.

Validation attempted:
- `npx nx lint:diff-with-main twenty-front` → failed due missing Nx
modules in this environment.
- `npx nx typecheck twenty-front` → failed due missing Nx modules in
this environment.

Authored by Sonarly by autonomous analysis (run 44851).

Co-authored-by: sonarly-bot <sonarly@sonarly.com>
2026-05-21 16:40:48 +00:00
869f8af4f0 Fix workflow cron trigger cache stuck without TTL (#20812)
## Problem

The cron-trigger cache key (`module:workflow:workflow-cron-triggers`)
can get stuck without a TTL, silently halting **all** cron-triggered
workflows for a whole tenant until the key is manually deleted from
Redis.

Repro path:

1. Cache miss → DB-scan branch runs.
2. Inner loop writes triggers via `hashSet` (creates the key, **no TTL
yet**).
3. Worker crashes / OOMs / gets killed by a deploy between any `hashSet`
and the trailing `expire(1h)` call.
4. Key now exists with TTL = `-1` and a partial set of fields.
5. Next tick: `hashGetValues` returns those fields →
`cachedValues.length > 0` → **cache-hit branch** → `expire` is never
called.
6. Key has no TTL, so it never auto-expires. The DB-scan branch never
runs again. New / missing triggers are never picked up. Workflows go
silent.

Observed in production: cache key with `TTL: no limit` and 121 fields.
Deleting the key restored normal behaviour (next tick rebuilt with TTL
~3600).

## Fix

Set the TTL right after first value is added 

## Monitoring

Added a "Cache miss" log count in workflow dashboard, counted among the
last 6 hours. Turns green if >= 5
<img width="1627" height="721" alt="Screenshot 2026-05-21 at 16 46 13"
src="https://github.com/user-attachments/assets/8262dd5f-fbbd-43c9-aede-c0ce5d6a0f59"
/>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 16:03:09 +00:00
EtienneandGitHub 2fcf3e3c2b fix(ai-chat) - fix browser context injection (#20809)
Move the browsing context out of the system prompt and injecting it
directly into the last user message instead.
Previously (before this PR), browsing context change, update system
prompt then break whole conversation history ... and caching. Now,
browsing context is sent with last message only if changed.

"Benchmark" this PR vs main : 
- same conv with 3-4 turns - 60% -> 85% cache ratio || 0.31 credits ->
0.13
2026-05-21 15:55:07 +00:00
Abdullah.andGitHub 8826d12a18 [Website] Host customer story hero images locally and fix multi-segment redirects (#20790)
Customer story page shows the following error, which I believe leads to
an internal server on the individual customer story pages.

<img width="636" height="75" alt="image"
src="https://github.com/user-attachments/assets/fc9ede75-fd3b-4538-8211-8182f4a99b9b"
/>

This PR replaces remote URLs of those images with local copies to avoid
a 404 issue. Will test once deployed on dev to confirm if the error is
resolved, but locally, I do not see console errors any longer after this
change.

There is some duplicated copy that I found upon audit which can be made
DRY, but I will resolve it in a separate PR to keep this PR
single-responsibility.
2026-05-21 14:10:24 +00:00
Paul RastoinandGitHub abf5902ab5 Connected account deprecation system build (#20810) 2026-05-21 16:14:56 +02:00
a78c4c9fe8 chore: bump version to 2.8.0 (#20813)
## 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

## Checklist

- [ ] Verify version constants are correct

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-05-21 16:10:08 +02:00
a9ff1a9d3c Fix server variable not shown (#20799)
## Before

<img width="1502" height="675" alt="image"
src="https://github.com/user-attachments/assets/b64b24b4-11a5-4f6b-a3aa-c77108f22e9d"
/>

## After

<img width="1262" height="593" alt="image"
src="https://github.com/user-attachments/assets/42d662b4-2ec6-4ad4-9d19-58f25f52475c"
/>

---------

Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-05-21 13:43:05 +00:00
Paul RastoinandGitHub 570c57563a Upload application file resolver exception management and integration coverage (#20803)
# Introduction
Earlier and better exception handling of the upload application file
resolver
+ coverage
2026-05-21 12:45:54 +00:00
Charles BochetandGitHub 4b8c722b41 fix(docker): pin patched curl/nghttp2/postgresql18-client apk versions (#20805)
## Summary

- ECR Inspector still flags `prod-twenty` for the High-severity CVEs
that PR #20603 was meant to fix (8x `postgresql18-18.3-r0`,
`nghttp2-1.68.0-r0`, `curl-8.17.0-r1`, plus the related Medium `curl`
CVE).
- Root cause: PR #20603 pinned the `node:24.15.0-alpine3.23` digest to
invalidate the buildx GHA cache once, but the cache layer was first
repopulated (on the PR branch) before Alpine 3.23 published `18.4-r0` /
`1.69.0-r0` / `8.19.0-r0`. Every build since — including today's prod
v2.6.2 — hits `#26 [twenty-server 2/19] RUN apk add --no-cache curl jq
postgresql-client / #26 CACHED` and ships the stale packages.
- Pinning minimum versions in the `apk add` spec changes the RUN text →
forces a new buildx cache key → apk re-resolves against the current
Alpine mirror. apk also refuses to install anything below the floor, so
the image can't silently regress if a stale layer ever matches the key
again.
2026-05-21 12:42:19 +00:00
WeikoandGitHub d13cc7c349 Drop legacy rolePermissionFlag.flag column + fallback logic (#20730)
## Summary
- **New fast migration**
`2-7-instance-command-fast-1779600000000-finalize-role-permission-flag-cutover.ts`:
  - `DROP CONSTRAINT IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE`
  - `ALTER COLUMN permissionFlagId SET NOT NULL`
  - `DROP COLUMN flag`
- `down()` repopulates `flag` from the catalog via `permissionFlagId`
and restores the old unique.
- **Entity**: `RolePermissionFlagEntity` hides the `flag` column by
using the new decorator + drops old `@Unique` decorator;
`permissionFlagId` and the `permissionFlag` relation become
non-nullable.
- **Deletes** the synthesizer
`synthesize-flat-permission-flag-from-flag.util.ts` and every fallback
branch that used it

(`from-role-permission-flag-entity-to-flat-role-permission-flag.util.ts`,
`from-flat-role-permission-flag-to-role-permission-flag-dto.util.ts`,
`permissions.service.ts`,
`workspace-roles-permissions-cache.service.ts`,
`fromRoleEntityToRoleDto.util.ts`,
`flat-role-permission-flag-validator.service.ts`,
`role-permission-flag.service.ts:getEffectiveUniversalIdentifier`).
- **Write path**: ~~drops `flag` from `CreateRolePermissionFlagInput`,
the create util, and the application-manifest converter.~~
- **Metadata configs**: ~~removes `flag` from
`all-entity-properties-configuration-by-metadata-name.constant.ts`
(rolePermissionFlag block)~~ and flips `permissionFlag.isNullable` to
`false` in `all-many-to-one-metadata-relations.constant.ts`.

### Why the `flag` field stays declared in the entity

The decorator (`@WasRemovedInUpgrade`) is the right tool for the
lifecycle marker, but it's a **reflect-metadata** runtime decorator —
TypeScript can't see it at compile time. So while
the adapter
([`UpgradeAwareEntityMetadataAdapter`](packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts))
now correctly flips
`isSelect`/`isInsert`/`isUpdate` to `false` once the drop migration's
cursor is crossed, the *static* TypeScript types derived from
`RolePermissionFlagEntity`
(`UniversalFlatRolePermissionFlag`, `FlatRolePermissionFlag`,
`MetadataEntityPropertyConfiguration<'rolePermissionFlag'>`, etc.) still
see `flag` as a required scalar property — because the entity declares
it.

That means every producer of one of those derived types must include
`flag`:
-
`from-create-role-permission-flag-input-to-flat-role-permission-flag-to-create.util.ts`
plumbs it through.
- `from-permission-flag-to-universal-flat-role-permission-flag.util.ts`
(the application-manifest converter) sets `flag: permissionFlag.flag`.
- `all-entity-properties-configuration-by-metadata-name.constant.ts` has
a `flag` entry under `rolePermissionFlag`.
- `CreateRolePermissionFlagInput` keeps the `flag` field.
- `RolePermissionFlagService.upsertPermissionFlags` passes `flag:
permissionFlag.key as PermissionFlagType` to the create util.

Explored phantom-brand approach (`RemovedInUpgrade<T>` wrapper on the
field type, key-filter mapped type applied inside `ScalarFlatEntity` /
`UniversalFlatEntityFrom`) but previous commands could have `flag ===
undefined` (downcast from the brand since we can't compare with
UpgradeMigrationName like we do with a decorator). That's a
**silent-read** failure mode: compiles fine, comparisons against `flag`
silently always-false, no error surfaces. Probably worth too much risk
for what's a small amount of plumbing?

The eventual full deletion of `flag` (entity field included) is a future
cleanup once we drop cross-upgrade support for versions ≤ 2.6

Note: Not sure if this PR (and even the decorator) is really needed in
the end, seems we need to keep a lot of code in place to handle legacy.
Maybe a simple noop [At]Deprecated is enough @charlesBochet (and a
migration to set the column nullable if that was not the case before +
remove associated constraints)
2026-05-21 11:56:06 +00:00
d192d2d492 i18n - translations (#20804)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 13:50:56 +02:00
7ea652ddf8 i18n - translations (#20802)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 13:43:27 +02:00
Paul RastoinandGitHub 9b9c97a049 Deprecate and backfill delete ConnectedAccount twenty standard object (#20752)
# Introduction
Following connected account permissions refactor and encryption
Removing the old workspace schema twenty standard application
connectedAccount objects and related standard fields and index

- a lot of deadcode
- instance command backfill cleaning the connected account object from
workspaces
2026-05-21 13:42:09 +02:00
f1b2be041a i18n - translations (#20801)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 13:35:35 +02:00
martmullandGitHub 2963fa9324 Navigate to installed page after app install (#20797)
as title
2026-05-21 11:19:18 +00:00
Charles BochetandGitHub a6a08439f7 chore(deps): bump @xmldom/xmldom to 0.8.13 (security) (#20798)
## Summary
- Re-resolves the transitive `@xmldom/xmldom` dependency to `0.8.13` to
fix four high-severity Dependabot alerts.
- yarn.lock-only change: all four upstream consumers
(`@node-saml/node-saml`, `plist`, `xml-crypto`, `xml-encryption`) accept
`^0.8.x`, so the previous `0.8.10` / `0.8.11` entries collapse onto a
single `0.8.13` resolution. No `package.json` change needed.

## Alerts fixed
-  XML node injection through unvalidated comment serialization (high)
- XML node injection through unvalidated processing instruction
serialization (high)
- XML injection through unvalidated DocumentType serialization (high)
- Uncontrolled recursion in XML serialization leads to DoS (high)

All four advisories are patched in `0.8.13`, the latest release in the
`0.8.x` line.
2026-05-21 11:17:41 +00:00
383edd5871 fix(email): resolve reply account from thread channel (#20755)
## Summary

Fixes the reply account resolution path so email replies use the
connected account associated with the thread's message channel instead
of blindly selecting the first connected account.

This targets the failure mode reported in #20658 where replying can
surface `SMTP is not configured for connected account` even though the
thread's actual IMAP/SMTP account has SMTP configured.

## Changes

- Query `myMessageChannels` alongside `myConnectedAccounts` in
`useEmailThread`
- Resolve the latest message's `messageChannelId` to its
`connectedAccountId`
- Return the matching connected account for reply context instead of
`myConnectedAccounts[0]`
- Add a regression test proving the hook chooses the thread channel's
account when the first account is different

## Validation

- `git diff --check`

I could not run the full frontend test locally in this workspace because
dependency installation failed with `ENOSPC: no space left on device`
while Yarn was cloning a dependency into the Windows temp/cache path.
The code change is intentionally narrow and covered by the added hook
regression test.

---------

Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-21 10:26:59 +00:00
16ad50ea46 Set website default port to 3002 (#20795)
## Summary

- Set `twenty-website`'s `dev` script to run Next.js on port `3002` by
default.
- Set `twenty-website`'s `start` script to use the same default port.

## Why

`twenty-website` previously inherited Next.js' default port `3000`,
which is also Twenty's backend/server default. The main Twenty frontend
already defaults to `3001`, so using `3002` for the website avoids local
port collisions when running the website next to the app server and
frontend.

This keeps the local convention sequential:

- `3000`: Twenty backend/server
- `3001`: Twenty frontend app
- `3002`: Twenty website

## Validation

- Parsed `packages/twenty-website/package.json` successfully with Node.
- Ran `git diff --check` for the changed package file.
- Verified Next.js supports the `--port` option for `next dev`.

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-05-21 09:34:56 +00:00
martmullandGitHub 74fa26f9e5 Fix must wait 3 days to create app in twenty-apps (#20794)
as title
2026-05-21 09:29:47 +00:00
7ca2efeb96 i18n - translations (#20796)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 11:42:46 +02:00
WeikoandGitHub 224376233b fix(front): focus new Field widget and open side panel on add (#20777)
## Context
In the record-page edit mode, the "Add widget" section has three menu
items:
`Fields group`, `Field`, and `More widgets`. `Fields group` creates the
widget, focuses it, and opens its settings side panel. `Field` only
added the widget to the draft — no focus, no panel — which left the user
without visible confirmation or a way to immediately edit the new
widget.

## Change
Updated `useCreateRecordPageFieldWidget` to mirror
`useCreateRecordPageFieldsWidget`:
After appending the new widget to the draft, it now sets
`pageLayoutEditingWidgetIdComponentState` to the new widget's id and
navigates the side panel to `SidePanelPages.RecordPageFieldSettings`
(with `focusTitleInput: true`, `resetNavigationStack: true`).
This matches the behavior of clicking an existing field widget in
`useOpenWidgetSettingsInSidePanel` (`WidgetType.FIELD` branch).
2026-05-21 09:22:21 +00:00
Paul RastoinandGitHub a3c92311e3 Application file storage service (#20793)
# Introduction
Fix unsafe resource path join with expected prefix at file storage
directly
Add early paths transversal detections in metadata validators
2026-05-21 09:21:44 +00:00
1ed347bc17 chore: sync AI model catalog from models.dev (#20791)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-21 09:14:02 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b792f7654b chore(deps): bump tinyglobby from 0.2.15 to 0.2.16 (#20788)
Bumps [tinyglobby](https://github.com/SuperchupuDev/tinyglobby) from
0.2.15 to 0.2.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/SuperchupuDev/tinyglobby/releases">tinyglobby's
releases</a>.</em></p>
<blockquote>
<h2>0.2.16</h2>
<h2>Fixed</h2>
<ul>
<li>Upgraded <code>picomatch</code> to 4.0.4, mitigating any potential
exposure to <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-c2c7-rcm5-vvqj">CVE-2026-33671</a>
and <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-3v7f-55p6-f55p">CVE-2026-33672</a></li>
</ul>
<h2>Changed</h2>
<ul>
<li>Overhauled and optimized most internals by <a
href="https://github.com/Torathion"><code>@​Torathion</code></a></li>
<li>Ignore patterns are no longer compiled twice by <a
href="https://github.com/webpro"><code>@​webpro</code></a></li>
</ul>
<p>Consider <a
href="https://github.com/sponsors/SuperchupuDev">sponsoring</a> if you'd
like to support the development of this project and the goal of reaching
a lighter and faster ecosystem</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/SuperchupuDev/tinyglobby/blob/main/CHANGELOG.md">tinyglobby's
changelog</a>.</em></p>
<blockquote>
<h3><a
href="https://github.com/SuperchupuDev/tinyglobby/compare/0.2.15...0.2.16">0.2.16</a></h3>
<h4>Fixed</h4>
<ul>
<li>Upgraded <code>picomatch</code> to 4.0.4, mitigating any potential
exposure to
<a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-c2c7-rcm5-vvqj">CVE-2026-33671</a>
and <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-3v7f-55p6-f55p">CVE-2026-33672</a></li>
</ul>
<h4>Changed</h4>
<ul>
<li>Overhauled and optimized most internals by <a
href="https://github.com/Torathion">Torathion</a></li>
<li>Ignore patterns are no longer compiled twice by <a
href="https://github.com/webpro">webpro</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/577920259c91f5603fab3dbfa599a83bbb14a27a"><code>5779202</code></a>
release 0.2.16</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/071954f97f4d2b573ecd92aecb48220867b3c776"><code>071954f</code></a>
bump deps once more</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/e541dde000158e69b44e8b5a03789d136d88ae0d"><code>e541dde</code></a>
do not import the whole <code>fs</code> module</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/2381b766d3447e078b3112e533ada07621f60526"><code>2381b76</code></a>
fix root being too broad</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/0addeb9a78cab5fd93ac6dad217f75930cfbfda2"><code>0addeb9</code></a>
chore(deps): update all non-major dependencies (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/191">#191</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/91ac26cc3bda60378d188565ce4f05e884aa4f31"><code>91ac26c</code></a>
chore(deps): update pnpm/action-setup action to v5 (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/192">#192</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/c50558e944bf71dbfeb392f8693fada87f520a70"><code>c50558e</code></a>
upgrade picomatch (and everything else)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/618517544e95f8167f1722902558dd78dcd34fe6"><code>6185175</code></a>
chore(deps): update dependency picomatch to v4.0.4 [security] (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/193">#193</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/49c2b9356c4977f1f531de651f90b420a06c64f6"><code>49c2b93</code></a>
enable pnpm <code>trustPolicy</code></li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/bc825c476a5a429e58e06d11364cd68707b4cdd1"><code>bc825c4</code></a>
chore(deps): update all non-major dependencies (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/181">#181</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/SuperchupuDev/tinyglobby/compare/0.2.15...0.2.16">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tinyglobby&package-manager=npm_and_yarn&previous-version=0.2.15&new-version=0.2.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:47:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
42ad3cbd1a chore(deps): bump linkify-react from 4.3.2 to 4.3.3 (#20789)
Bumps
[linkify-react](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkify-react)
from 4.3.2 to 4.3.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nfrasser/linkifyjs/releases">linkify-react's
releases</a>.</em></p>
<blockquote>
<h2>v4.3.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix parsing bugs with some special encoded URLs</li>
<li>Parsed emails should not include port numbers</li>
<li>Exact version requirement for interfaces and plugins to avoid
incompatibility issues with older versions of linkify core</li>
<li>Support for jQuery 4</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nfrasser/linkifyjs/compare/v4.3.2...v4.3.3">https://github.com/nfrasser/linkifyjs/compare/v4.3.2...v4.3.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md">linkify-react's
changelog</a>.</em></p>
<blockquote>
<h2>v4.3.3</h2>
<ul>
<li>Fix parsing bugs with some special encoded URLs</li>
<li>Parsed emails should not include port numbers</li>
<li>Exact version requirement for interfaces and plugins to avoid
incompatibility issues with older versions of linkify core</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nfrasser/linkifyjs/commit/7fffcc6b48f7dbf8e98fca493e1c997a659fe651"><code>7fffcc6</code></a>
v4.3.3</li>
<li><a
href="https://github.com/nfrasser/linkifyjs/commit/2cb8352d78c7449cd8c7ee489a647d3422640a25"><code>2cb8352</code></a>
Update dependencies (<a
href="https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkify-react/issues/529">#529</a>)</li>
<li>See full diff in <a
href="https://github.com/nfrasser/linkifyjs/commits/v4.3.3/packages/linkify-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for linkify-react since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=linkify-react&package-manager=npm_and_yarn&previous-version=4.3.2&new-version=4.3.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:47:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e1378dd4cf chore(deps): bump @azure/msal-node from 3.8.4 to 3.8.10 (#20787)
Bumps
[@azure/msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js)
from 3.8.4 to 3.8.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/releases">@​azure/msal-node's
releases</a>.</em></p>
<blockquote>
<h2><code>@​azure/msal-node</code> v3.8.10</h2>
<h2>3.8.10</h2>
<p>Wed, 18 Mar 2026 20:48:29 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.17.0 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.9</h2>
<h2>3.8.9</h2>
<p>Fri, 13 Mar 2026 04:32:07 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.16.1 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.8</h2>
<h2>3.8.8</h2>
<p>Mon, 23 Feb 2026 16:28:24 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.15.0 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.7</h2>
<h2>3.8.7</h2>
<p>Tue, 10 Feb 2026 22:19:29 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.14.2 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/74c792ec34cd83a3470c4d878b403af0fa2884f0"><code>74c792e</code></a>
[v4] Add missing client capabilities in platform broker flows (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8429">#8429</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/e096fc85e22fcb941646b3f5d9f264b90dffd0ad"><code>e096fc8</code></a>
[v4] Post-release PR (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8425">#8425</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/23cd31c1fbcfa569dd9135830f5168ec71678a19"><code>23cd31c</code></a>
[v4] Add support for client data telemetry with CLI_DATA parameter (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8378">#8378</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/eb893e565a4f7de5b9c957b37bb4f975302f1713"><code>eb893e5</code></a>
Track online/offline status change (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8410">#8410</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/d221f4e6dbc439e771a90f1648c8f5db09b4d88b"><code>d221f4e</code></a>
[v4] Respect claims of the brokered application (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8409">#8409</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/73df97e5c505010f6b9681749df28a328e4e3894"><code>73df97e</code></a>
Common partial release resolution (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8397">#8397</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/ee5e7abd5d9a6cfb6ada9b3ac36e15a81f7ba3a8"><code>ee5e7ab</code></a>
monitor_window_timeout telemetry (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8380">#8380</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/2be7888051cc12e7447487e84c826159dad3ed42"><code>2be7888</code></a>
Rename dev to v4-lts changes (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8364">#8364</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/4486b7e4bfedc414f7f5f1a7786be30484318206"><code>4486b7e</code></a>
Fix JSON object conversion in PlatformDOMRequest v4 (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8350">#8350</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/3e5d58b71e232b5978edcf0bd4c05f5948b8a747"><code>3e5d58b</code></a>
Post-release PR (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8354">#8354</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/compare/msal-node-v3.8.4...msal-node-v3.8.10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@azure/msal-node&package-manager=npm_and_yarn&previous-version=3.8.4&new-version=3.8.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:06:59 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5e1b23a28b chore(deps): bump @recallai/desktop-sdk from 2.0.8 to 2.0.15 (#20785)
Bumps @recallai/desktop-sdk from 2.0.8 to 2.0.15.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@recallai/desktop-sdk&package-manager=npm_and_yarn&previous-version=2.0.8&new-version=2.0.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:04:13 +00:00
7aef406696 i18n - translations (#20782)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-20 21:33:20 +02:00
923c3beead fix(auth): clarify error when joining a non-active workspace (#20769)
## Summary

When an existing user accepts an invite into a workspace whose
`activationStatus` is not `ACTIVE` (e.g. `SUSPENDED`, `INACTIVE`,
`PENDING_CREATION`), the throw in
`throwIfWorkspaceIsNotReadyForSignInUp` returns:

> User is not part of the workspace

The message describes the symptom (they aren't a member yet) instead of
the cause (the workspace can't accept new members), which makes invitees
assume their invite is broken when the real issue is the target
workspace's state.

The sibling branch a few lines above — for brand-new users hitting the
same non-ACTIVE workspace — already returns `"Workspace is not ready to
welcome new members"`. This PR reuses the same message in the
existing-user branch so both paths give a consistent, accurate
explanation.

Single file, two string literals.

## Test plan

- [ ] Sign in via Google with an existing Twenty account, accepting an
invite to a `SUSPENDED` workspace → confirm the new message is shown
instead of "User is not part of the workspace".
- [ ] Confirm the happy path (sign-in to an `ACTIVE` workspace via
invite) is unchanged — early-return on `ACTIVE` is untouched.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-20 19:16:11 +00:00
Paul RastoinandGitHub 31842f7714 Ci server custom jest reporter (#20765)
# Introduction
Only display failing unit tests trace in the ci-server server-test jobs
So it's possible to identify what unit test are failing without having
to re run them locally
2026-05-20 17:58:27 +00:00
Charles BochetandGitHub 9988f98577 feat(server): idempotent CLI to rotate ENCRYPTION_KEY across enc:v2 rows (#20613)
## Summary
Adds the \`secret-encryption:rotate\` CLI command, which re-encrypts
every at-rest secret stored in an \`enc:v2:\` envelope under the current
\`ENCRYPTION_KEY\`. The command is **online** and **resumable**: a SQL
filter skips rows already on the current keyId, so interrupting it
(Ctrl-C, container restart, …) and re-running picks up where it left off
without re-rotating earlier rows.

### Sites covered (one handler each)
| Site | Table.column | Scope |
| --- | --- | --- |
| \`connected-account-tokens\` | \`connectedAccount.{accessToken,
refreshToken}\` | workspace |
| \`application-variable\` | \`applicationVariable.value\` (isSecret
only) | workspace |
| \`application-registration-variable\` |
\`applicationRegistrationVariable.encryptedValue\` | instance |
| \`signing-key-private-keys\` | \`signingKey.privateKey\` | instance |
| \`sensitive-config-storage\` | \`keyValuePair.value\` (isSensitive +
STRING configs) | instance |
| \`totp-secrets\` | \`twoFactorAuthenticationMethod.secret\` |
workspace |

Each handler:
- Filters at SQL level on \`value LIKE 'enc:v2:%' AND value NOT LIKE
'enc:v2:<primaryKeyId>:%'\` to enforce idempotency without re-decrypting
already-rotated rows.
- Uses cursor-based batching (default **200**, capped **5000**).
- Threads \`workspaceId\` into HKDF for workspace-scoped sites; runs
instance-scoped for the rest.

### CLI flags
| Flag | Description |
| --- | --- |
| \`-s, --site <site>\` | Limit to a single site. |
| \`-b, --batch-size <n>\` | Override per-batch row count. |
| \`-d, --dry-run\` | Decrypt + re-encrypt in memory, skip the
\`UPDATE\`. |

The runner logs progress via Nest \`Logger\` (per-site start,
completion, final summary) and exits non-zero when any site reports
\`errors > 0\`. \`FALLBACK_ENCRYPTION_KEY\` must be set to the previous
\`ENCRYPTION_KEY\` during rotation; the runner warns when it is unset.

Operator documentation lives in #20611 (docs PR).
2026-05-20 17:51:29 +00:00
3c458ce4ca i18n - docs translations (#20778)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-20 19:28:51 +02:00
b869107a22 fix(messaging): preserve all gmail to/cc/bcc recipients as participants (#20491)
As title but I also refactored it a little to match our current file and
code conventions since the code was very old

Reported by a cloud customer

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-20 15:40:29 +00:00
EtienneandGitHub 4c4dc4cb21 fix(ai-chat)-preference models import (#20776) 2026-05-20 15:20:09 +00:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
237a943947 Update twenty sdk commands (#20735)
Performs twenty-sdk cli command migration:

Summary

``` ┌─────┬──────────────────────────┬────────────────────────────┬───────────────────────┐
 │  #  │       Old command        │        New command         │        Status         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 1   │ twenty dev [appPath]     │ twenty dev [appPath]       │ Unchanged (now also   │
 │     │                          │                            │ DEFAULT)              │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 2   │ twenty dev --once        │ twenty dev --once          │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 3   │ twenty dev --watch       │ twenty dev [appPath]       │ --watch flag removed  │
 │     │ [appPath]                │                            │ (was default)         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 4   │ twenty dev --verbose     │ twenty dev --verbose       │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 5   │ twenty dev --debug       │ twenty dev --debug         │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 6   │ twenty dev --debounceMs  │ twenty dev --debounceMs    │ Unchanged             │
 │     │ <ms> [appPath]           │ <ms> [appPath]             │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 7   │ twenty build [appPath]   │ twenty dev:build [appPath] │ Deprecated → colon    │
 │     │                          │                            │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 8   │ twenty build --tarball   │ twenty dev:build --tarball │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 9   │ twenty typecheck         │ twenty dev:typecheck       │ Deprecated → colon    │
 │     │ [appPath]                │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 10  │ twenty logs [appPath]    │ twenty dev:fn-logs         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 11  │ twenty logs -n <name>    │ twenty dev:fn-logs -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 12  │ twenty logs -u <id>      │ twenty dev:fn-logs -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 13  │ twenty exec [appPath]    │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 14  │ twenty exec -n <name>    │ twenty dev:fn-exec -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 15  │ twenty exec -u <id>      │ twenty dev:fn-exec -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 16  │ twenty exec -p <json>    │ twenty dev:fn-exec -p      │ Deprecated → colon    │
 │     │ [appPath]                │ <json> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 17  │ twenty exec              │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │ --postInstall [appPath]  │ --postInstall [appPath]    │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 18  │ twenty exec --preInstall │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │  [appPath]               │ --preInstall [appPath]     │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 19  │ twenty add [entityType]  │ twenty dev:add             │ Deprecated → colon    │
 │     │                          │ [entityType]               │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 20  │ twenty add --path <path> │ twenty dev:add --path      │ Deprecated → colon    │
 │     │  [entityType]            │ <path> [entityType]        │ command               │
 └─────┴──────────────────────────┴────────────────────────────┴───────────────────────┘

 App lifecycle commands

 ┌─────┬────────────────────────┬────────────────────────────┬─────────────────────────┐
 │  #  │      Old command       │        New command         │         Status          │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 21  │ twenty publish         │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 22  │ twenty publish --tag   │ twenty app:publish --tag   │ Deprecated → colon      │
 │     │ <tag> [appPath]        │ <tag> [appPath]            │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 23  │ twenty deploy          │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ --private [appPath]        │ command + --private     │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 24  │ twenty install         │ twenty app:install         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 25  │ twenty uninstall       │ twenty app:uninstall       │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 26  │ twenty uninstall -y    │ twenty app:uninstall -y    │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 └─────┴────────────────────────┴────────────────────────────┴─────────────────────────┘

 Server commands

 ┌─────┬─────────────────────────┬─────────────────────────────┬──────────────────────┐
 │  #  │       Old command       │         New command         │        Status        │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 27  │ twenty server start     │ twenty docker:start         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 28  │ twenty server start -p  │ twenty docker:start -p      │ Deprecated → colon   │
 │     │ <port>                  │ <port>                      │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 29  │ twenty server start     │ twenty docker:start --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 30  │ twenty server stop      │ twenty docker:stop          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 31  │ twenty server stop      │ twenty docker:stop --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 32  │ twenty server status    │ twenty docker:status        │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 33  │ twenty server status    │ twenty docker:status --test │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 34  │ twenty server logs      │ twenty docker:logs          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 35  │ twenty server logs -n   │ twenty docker:logs -n       │ Deprecated → colon   │
 │     │ <lines>                 │ <lines>                     │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 36  │ twenty server logs      │ twenty docker:logs --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 37  │ twenty server reset     │ twenty docker:reset         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 38  │ twenty server reset     │ twenty docker:reset --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 39  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ [version]               │ [version]                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 40  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ --test [version]        │ --test [version]            │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 41  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync            │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 42  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync -r <name>  │ -r <name>                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 43  │ twenty catalog-sync     │ (removed)                   │ Removed (was already │
 │     │                         │                             │  deprecated)         │
 └─────┴─────────────────────────┴─────────────────────────────┴──────────────────────┘

 Remote commands

 ┌─────┬────────────────────────┬──────────────────────────┬──────────────────────────┐
 │  #  │      Old command       │       New command        │          Status          │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 44  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 45  │ twenty remote add --as │ twenty remote:add --as   │ Deprecated → colon       │
 │     │  <name>                │ <name>                   │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 46  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-key <key>        │ --api-key <key>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 47  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-url <url>        │ --api-url <url>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 48  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --local                │ --local                  │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 49  │ twenty remote add      │ twenty remote:add --test │ Deprecated → colon       │
 │     │ --test                 │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 50  │ twenty remote list     │ twenty remote:list       │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 51  │ twenty remote switch   │ twenty remote:use [name] │ Deprecated → colon       │
 │     │ [name]                 │                          │ syntax + renamed         │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 52  │ twenty remote status   │ twenty remote:status     │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 53  │ twenty remote remove   │ twenty remote:remove     │ Deprecated → colon       │
 │     │ <name>                 │ <name>                   │ syntax                   │
 └─────┴────────────────────────┴──────────────────────────┴──────────────────────────┘
```

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-20 15:12:39 +00:00
WeikoandGitHub 6e5e7963b5 fix(server): map PermissionsException to proper HTTP status on REST API (#20739)
## Summary

`PermissionsException` thrown by `SettingsPermissionGuard` (and other
permission code paths) was bubbling up through every typed REST
exception filter and landing in the global `UnhandledExceptionFilter`,
which falls back to **500** for anything that isn't an `HttpException`.
So a forbidden user (e.g. an API key whose role doesn't have
`DATA_MODEL`) calling `GET /rest/metadata/objects` got:
```
HTTP/1.1 500 Internal Server Error "Entity performing the request does not have permission"
```


GraphQL already had the right plumbing via
`permissionGraphqlApiExceptionHandler` (`ForbiddenError` → 403,
`UserInputError` → 400, `NotFoundError` → 404). This PR mirrors it on
the REST side.

## What

- New util `permissionRestApiExceptionCodeToHttpStatus` mapping every
`PermissionsExceptionCode` → HTTP status, with `assertUnreachable` to
force explicit handling of future codes.
- New filter `PermissionsRestApiExceptionFilter`
(`@Catch(PermissionsException)`) that delegates to
`HttpExceptionHandlerService.handleError(...)` with the resolved status.
- Wired `PermissionsRestApiExceptionFilter` (placed first, so the typed
filter wins over any sibling catch-all) into `@UseFilters(...)` of every
REST controller that uses `SettingsPermissionGuard` or whose service can
throw `PermissionsException`:
  - `object-metadata`, `field-metadata`, `webhook`, `api-key`
- `view`, `view-sort`, `view-group`, `view-filter`, `view-filter-group`,
`view-field`
  - `page-layout`, `page-layout-widget`, `page-layout-tab`
  - `front-component`, `ai-generate-text`
- Unit tests covering 403 / 400 / 404 / 500 mappings.

## Mapping

| Code | Status |
|------|--------|
| `PERMISSION_DENIED`, `NO_AUTHENTICATION_CONTEXT`,
`ROLE_LABEL_ALREADY_EXISTS`, `CANNOT_UNASSIGN_LAST_ADMIN`,
`CANNOT_UPDATE_SELF_ROLE`, `CANNOT_DELETE_LAST_ADMIN_USER`,
`ROLE_NOT_EDITABLE`, `CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT`,
`CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT` | **403** |
| `INVALID_ARG`, `INVALID_SETTING`,
`CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT`,
`CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION`,
`ONLY_FIELD_RESTRICTION_ALLOWED`,
`FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT`,
`FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT`,
`EMPTY_FIELD_PERMISSION_NOT_ALLOWED`,
`ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET`, `ROLE_CANNOT_BE_ASSIGNED_TO_USERS`
| **400** |
| `ROLE_NOT_FOUND`, `OBJECT_METADATA_NOT_FOUND`,
`FIELD_METADATA_NOT_FOUND`, `FIELD_PERMISSION_NOT_FOUND`,
`PERMISSION_NOT_FOUND` | **404** |
| All remaining "internal" codes (rethrown as-is in GraphQL) | **500** |

## Before
<img width="507" height="216" alt="Screenshot 2026-05-19 at 19 26 07"
src="https://github.com/user-attachments/assets/21d633aa-7ee8-4923-94e4-7ad57258a29e"
/>

## After
<img width="610" height="385" alt="Screenshot 2026-05-19 at 19 26 01"
src="https://github.com/user-attachments/assets/0103b7ee-7df7-4aef-999a-73c22901afd2"
/>
2026-05-20 13:02:58 +00:00
Félix MalfaitandGitHub a2acf88a57 feat(website): per-PR preview deploys via Worker versions (#20762)
## Summary
Adds review apps for the marketing site. Every PR that touches
`packages/twenty-website/**` or `packages/twenty-shared/**` gets a
per-version Worker preview URL, sticky-commented on the PR, auto-cleaned
up when the PR closes.

Same Cloudflare machinery skew protection rides on, just used for
previews — no extra plan, no extra services. Cleaner than the
GitHub-Actions-runner + Cloudflare-tunnel pattern: previews persist for
the life of the version, accessible from anywhere, no warm-up.

## Files
- **`.github/workflows/website-pr-preview.yaml`** — on PR
open/sync/reopen: builds the Worker with a per-PR `DEPLOYMENT_ID`, runs
`wrangler versions upload --tag pr-<N>` (no production traffic),
sticky-comments the preview URL. Skipped on fork PRs because GitHub
doesn't pass secrets to forks anyway.
- **`.github/workflows/website-pr-preview-cleanup.yaml`** — on PR close:
walks the Worker version list via the CF API, deletes anything tagged
`pr-<N>` (with message-based fallback if the annotation key changes),
updates the sticky comment.
- **`open-next.config.ts`** — `maxNumberOfVersions: 10 → 50` to leave
room for PR previews on top of skew protection's prod-version retention.

## How it looks on a PR

The bot leaves a sticky comment like:

> 🔍 **Website preview** is up at
**https://abc12345-twenty-website-dev.twentyhq.workers.dev**
>
> | | |
> |---|---|
> | Version | `abc12345-...` |
> | Commit | `<sha>` |
> | Bindings | shared with the `dev` Worker (R2 cache + secrets) |
>
> Updates on every push. Auto-deleted when the PR closes.

On close it becomes:

> 🧹 Website preview for this PR was cleaned up after close.

## Twenty repo credentials already provisioned
- `secret CLOUDFLARE_API_TOKEN` — same scoped token the `twenty-infra`
workflow uses
- `var CLOUDFLARE_ACCOUNT_ID` = `67b2bbe4381006564d2b0aa6ce6177be`
- `var CF_PREVIEW_DOMAIN` = `twentyhq` (no `.workers.dev` suffix —
OpenNext appends it;
[opennextjs-cloudflare#811](https://github.com/opennextjs/opennextjs-cloudflare/issues/811))

## Known limitations
- **Shared dev bindings**: PR previews use the dev Worker's R2 bucket +
secrets (Stripe test key, JWT private key). Fine for a read-mostly
marketing site; if two simultaneous PRs ever fight over ISR cache state
we can prefix R2 keys per-PR later.
- **Fork PRs don't get previews**. GitHub Actions doesn't pass
`secrets.*` to fork-PR runs (security), and the wrangler upload requires
the CF token. To enable forks, would need to switch to
`pull_request_target` and gate on a maintainer label — not done here
because the security tradeoff isn't worth it for a marketing-site
preview.
- **Version cap**: 50 versions is the new ceiling, and
`maxVersionAgeDays: 14` auto-prunes anything older. Cleanup-on-close
should keep us well under in steady state.

## Test plan
- [ ] CI on this PR triggers the preview workflow itself; check that the
sticky comment appears with a working URL
- [ ] Hit the URL, click around — should look like a fresh
marketing-site build with this PR's changes
- [ ] Close (don't merge) → cleanup workflow should run; sticky comment
switches to the "cleaned up" message; the version is gone from `wrangler
versions list --name twenty-website-dev`
2026-05-20 14:59:42 +02:00
Félix MalfaitandGitHub c002bc52bd fix(ci): repair preview-environment dispatch (use PAT, not GITHUB_TOKEN) (#20773)
## What
One-line token swap on the same-repo dispatch step in
[`preview-env-dispatch.yaml`](.github/workflows/preview-env-dispatch.yaml#L40):
`secrets.GITHUB_TOKEN` → `secrets.CI_PRIVILEGED_DISPATCH_TOKEN`.

## Why
Regression from [#20476](https://github.com/twentyhq/twenty/pull/20476)
("security: harden CI against supply-chain attacks"), merged 2026-05-12.
That PR replaced

```yaml
uses: peter-evans/repository-dispatch@v2
with:
  token: ${{ secrets.GITHUB_TOKEN }}
  ...
```

with a raw `gh api` call but kept `GITHUB_TOKEN`:

```yaml
env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
  gh api repos/"$REPOSITORY"/dispatches -f event_type=preview-environment ...
```

The auto-provisioned `GITHUB_TOKEN` can't fire `repository_dispatch` via
`gh api` even when the workflow declares `permissions: contents: write`.
The action used a different code path that worked; the CLI requires a
token with `repo` scope. So every dispatch from this workflow has
returned `403 Resource not accessible by integration` since that PR
merged — except for runs the `author_association` / `preview-app` label
gate skips entirely (which then show "success" because no jobs ran).

Recent failed example:
https://github.com/twentyhq/twenty/actions/runs/26162974597/job/76959379235?pr=20769

## The fix
`secrets.CI_PRIVILEGED_DISPATCH_TOKEN` already exists in repo secrets
and is **already used** by the immediately-following cross-repo dispatch
step in the same file. Using it for the same-repo dispatch too matches
the surrounding code and is consistent with the original hardening
intent (use a scoped PAT, not the auto-provisioned token).

## Test plan
- [ ] Merge this PR
- [ ] Next PR open / sync / reopen on a member's branch → check that
`Preview Environment Dispatch` succeeds (no 403)
- [ ] Confirm `Preview Environment Keep Alive` workflow gets triggered
(the downstream effect of the dispatch)
- [ ] Confirm the tunnel URL sticky comment lands on the PR

Discovered while testing an unrelated PR
([#20762](https://github.com/twentyhq/twenty/pull/20762)). Independent
fix.
2026-05-20 14:57:57 +02:00
martmullandGitHub 127fb2a470 Increase size of tarball upload (#20767)
- check size while reading stream instead of checking after reading all
stream
- move MAX_TARBALL_UPLOAD_SIZE_BYTES to config variables
- increase MAX_TARBALL_UPLOAD_SIZE_BYTES default from 50Mb to 100Mb
2026-05-20 12:39:11 +00:00
Paul RastoinandGitHub 3d49c17e34 [CONNECTED_ACCOUNT_BREAKING_CHANGE] Unify connected account permissions (#20732)
# Introduction
This PR is a followup of https://github.com/twentyhq/twenty/pull/20673

It aims to unify the authentication/permissions layer with all the
connectedAccount interactions across the application

## Deprecate
- findAll
- findById

## Email sync
An user can only sync the message of his own connected account

## Workflow email
- Related https://github.com/twentyhq/private-issues/issues/478
- Only reauthorize owned account
2026-05-20 11:36:58 +00:00
b454ad2aea fix(workflow): restore initial input fields on code step creation (#20756)
## Summary

- Fixes a regression from #20208 where creating a new CODE workflow step
shows no input fields
- The split-triggers PR removed `SEED_LOGIC_FUNCTION_INPUT_SCHEMA` and
replaced `toolInputSchema` with `workflowActionTriggerSettings`, but
`CodeStepBuildService.createCodeStepLogicFunction` was not updated to
pass the seed schema — causing `logicFunctionInput` to default to `{}`
and no fields to render
- Adds `SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS` constant (matching the
seed template's `{ a: string, b: number }` params) and passes it when
creating the seed logic function

## Test plan

- [x] Unit test updated to assert `logicFunctionInput` contains `{ a:
null, b: null }` on code step creation
- [x] Create a new CODE step in the workflow builder and verify input
fields `a` and `b` appear immediately

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 11:21:17 +00:00
fb47e4497a i18n - docs translations (#20764)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-20 13:21:44 +02:00
Félix MalfaitandGitHub 00fad657f4 feat(website): enable OpenNext skew protection + tune CF cache (#20760)
## Summary
The original goal of this whole migration: **cross-deployment skew is
now handled by OpenNext's per-version routing instead of by users having
to refresh.**

A client holding a stale tab from deployment X requests assets with
`?dpl=X` — the Worker compares X to the current `DEPLOYMENT_ID`, looks
it up in `CF_DEPLOYMENT_MAPPING`, and routes to the matching old Worker
version via its per-version preview URL
(`<old-version>-twenty-website-<env>.twentyhq.workers.dev`). The old
version serves the old assets / RSC payloads / Server Actions
consistently.

**Verified end-to-end on dev**:
| | Marker in HTML |
|---|---|
| Current Worker (`twenty-main.com/`) | `9npeiytir8EPOtW71cqDZ` |
| Stale request (`twenty-main.com/?dpl=<previous-deploy-id>`) |
`B9OC_TNl1vaGcJ5oUUty6` |
| Direct hit on old preview URL | `B9OC_TNl1vaGcJ5oUUty6` ← matches the
skew-routed response |

## Changes

**`open-next.config.ts`** — enable skew protection
```ts
const baseConfig = defineCloudflareConfig({ incrementalCache: r2IncrementalCache });
export default {
  ...baseConfig,
  cloudflare: {
    ...baseConfig.cloudflare,
    skewProtection: {
      enabled: true,
      maxNumberOfVersions: 10,
      maxVersionAgeDays: 14,
    },
  },
};
```
(`defineCloudflareConfig` doesn't accept `skewProtection` directly — has
to be merged in)

**`next.config.ts`** — `deploymentId: process.env.DEPLOYMENT_ID`. CI
sets `DEPLOYMENT_ID` per-build; Next bakes it into prerendered HTML,
`?dpl=…` on asset URLs, Server Actions, and RSC fetch headers.

**`wrangler.jsonc`**:
- `compatibility_date: 2026-04-15` (was `2025-01-15`; build was warning)
- `assets.run_worker_first: true` — Worker must intercept asset requests
so the skew handler can route stale `/_next/static/*` to the old
version. CF edge cache absorbs hot paths so this isn't a 5×
billable-invocation tax
- `preview_urls: true` — required; skew routes via the per-version
preview URL which only exists when previews are enabled
- Per-env `services: [{ binding: WORKER_SELF_REFERENCE, service:
twenty-website-<env> }]` — OpenNext's recommended setup for
fire-and-forget ISR revalidation
- Per-env `vars`: `CF_WORKER_NAME` + `CF_PREVIEW_DOMAIN` (bare
`twentyhq`, *not* `twentyhq.workers.dev` — OpenNext appends
`.workers.dev` itself, see
[opennextjs-cloudflare#811](https://github.com/opennextjs/opennextjs-cloudflare/issues/811))
- Kept `global_fetch_strictly_public` in compat flags — without it, CF's
optimised intra-account routing self-loops the cross-version fetch and
522s out. With it, the fetch takes the public-Internet path which routes
correctly.

**`public/_headers`** — deleted (with `run_worker_first: true` the
assets pipeline doesn't process it; Next sets the same `Cache-Control:
immutable` on `/_next/static/*` anyway).

## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/__ — wires the four CF env
vars (`DEPLOYMENT_ID`, `CF_WORKER_NAME`, `CF_PREVIEW_DOMAIN`,
`CF_ACCOUNT_ID`, `CF_WORKERS_SCRIPTS_API_TOKEN`) into the deploy
workflow.

## Known limitation
Skew routing only works for Worker versions deployed AFTER this PR
(older versions don't have `preview_urls: true` and don't have
`DEPLOYMENT_ID` bindings OpenNext can read). Users on tabs older than
the first post-merge deploy still fall through to the current Worker
(same behaviour as today).

OpenNext marks `skewProtection` as **experimental** in their type docs
("might break on minor releases") — worth keeping an eye on.
2026-05-20 12:47:12 +02:00
c800eccc65 Slack workflow connector (#20427)
https://github.com/user-attachments/assets/5a746414-988b-473c-9401-b8863a3e1c15


https://github.com/user-attachments/assets/0cdebdb1-f7c8-43cb-beef-f279387b6ce9



https://github.com/user-attachments/assets/df31c631-0781-42d8-8e6e-e5a16573ee3b



https://github.com/user-attachments/assets/6adaeae4-f3c9-4a5f-b0df-50c1f9a78428

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-20 10:21:22 +00:00
nitinandGitHub a26fe3bb65 docs(sdk): document DatabaseEventPayload and simplify its type (#20754)
closes
https://discord.com/channels/1130383047699738754/1505967920163983502

Update logic-function docs to match the real `DatabaseEventPayload`
shape.

The docs now show database event payloads as record-level events with
`recordId` and `properties.before/after/diff/updatedFields`, including
compact examples for created, updated, and destroyed events. Route
payload type imports now use the preferred `twenty-sdk/logic-function`
surface.

Also clean up the shared payload type wrapper so it models event
metadata without over-promising actor fields; `userId`,
`userWorkspaceId`, and `workspaceMemberId` remain optional through the
underlying event type.
2026-05-20 09:22:58 +00:00
EtienneandGitHub 8d81496625 fix(ai-chat) - fixes on cost display (#20750)
- total cost conversion
- update metrics at message end
2026-05-20 08:50:23 +00:00
martmullandGitHub f1b7c21b6c Update create twenty app scaffolded front component (#20733)
- Replace inline SVG icons with proper Avatar and icon components
(IconBox, IconHierarchy, IconLayout, IconSettingsAutomation) from
twenty-sdk/ui in the scaffolded front component
  template
- Strip trailing slashes from workspace/API URLs in both
create-twenty-app CLI and twenty-sdk remote commands to prevent
malformed requests
  - Fix the application settings link to navigate to #installed anchor
- Bump twenty-sdk, twenty-client-sdk, and create-twenty-app versions to
2.6.0

<img width="1512" height="824" alt="image"
src="https://github.com/user-attachments/assets/8561d7bb-3458-46c4-b01e-664321634b4c"
/>
2026-05-20 08:23:10 +00:00
Félix MalfaitandGitHub f630ce34fe feat(website): mirror prod hostname pattern on dev (apex + www) (#20753)
## Summary
Drops the `website.` subdomain on dev entirely and serves the marketing
site from the bare zone + `www`, mirroring how prod is served at
`twenty.com` + `www.twenty.com`.

Also fixes a latent root-path substitution bug in the existing www→apex
redirect that was masked on prod by a CF-level redirect.

## What changes
- `wrangler.jsonc` env.dev routes: `twenty-main.com` (apex) +
`www.twenty-main.com` (was `website.twenty-main.com`)
- `next.config.ts`: extends host-based www→apex redirect to also cover
`www.twenty-main.com`, and adds explicit `source: '/'` rules for both
prod + dev before the catch-all `source: '/:path*'` (the `:path*`
parameter doesn't substitute properly when it matches the empty root
path against an absolute destination URL — Next.js leaves the literal
`:path*` in the `Location` header)

## Live verification (after redeploy)
| URL | Status | Notes |
|---|---|---|
| `https://twenty-main.com/` | 200 | `x-opennext: 1`, `x-nextjs-cache:
HIT` |
| `https://twenty-main.com/pricing` | 200 | Worker SSR |
| `https://www.twenty-main.com/` | 308 → `https://twenty-main.com/` |
Root-redirect fix applied |
| `https://www.twenty-main.com/pricing` | 308 →
`https://twenty-main.com/pricing` | Path preserved |
| `https://twenty.com/` | 200 | Unchanged |
| `https://www.twenty.com/` | 301 → `https://twenty.com/` | Still routed
via CF-level redirect, now also covered by the new explicit Next rule as
a defense-in-depth |
| `https://website.twenty-main.com/` | 503 | DNS record removed by
wrangler when route was deleted; hostname effectively retired |

## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/__ —
`cloudflare/website/dev.env` + `docs/4-environments.md` updated to the
new URL; also bundles the CI fix that should have landed in #683 (was
pushed too late).
2026-05-20 09:54:44 +02:00
martmullandGitHub 7ebcbe8801 Unify oAuth success and failure screen with autorize page (#20746)
Goal, matching authorize page design

<img width="2062" height="1376" alt="image"
src="https://github.com/user-attachments/assets/93d0f77a-c769-4a32-b41b-16459378314f"
/>


## Before

<img width="1540" height="942" alt="image"
src="https://github.com/user-attachments/assets/4de64f37-8519-4fdc-9388-70d98a69663e"
/>


## After

<img width="962" height="657" alt="image"
src="https://github.com/user-attachments/assets/c4fd19aa-13e3-452e-b6b7-c10202cb1edf"
/>

<img width="705" height="437" alt="image"
src="https://github.com/user-attachments/assets/e1f743f1-a862-4629-a3d0-c8e62e429e04"
/>
2026-05-20 07:43:34 +00:00
b821061526 fix(create-twenty-app): preserve .yarnrc.yml in template (#20623)
**Source:** https://sonarly.com/issue/37981?type=bug

## Summary

New apps created with `create-twenty-app@2.5.0` can fail at `yarn twenty
dev` with `Could not resolve "twenty-sdk/define"`, blocking onboarding
for app developers.

## Root cause

Proximate cause: manifest module loading in the SDK fails to resolve
`twenty-sdk/define` when the generated app uses Yarn PnP (no
`node_modules` tree), and esbuild is invoked with normal Node-style
resolution.

- The failing path is `extractManifestFromFile()` → `loadModule()` in
`packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts`,
which calls `esbuild.build({ bundle: true, ... })` and does not
stub/mock `twenty-sdk/define` [ref: read
`manifest-extract-config-from-file.ts`].
- The scaffolded template imports `twenty-sdk/define` in
`src/application-config.ts` and `src/default-role.ts` [ref: grep in
`packages/create-twenty-app/src/constants/template/src/*`].
- If esbuild cannot resolve that import from the app environment, the
exact error matches the issue: `src/application-config.ts:1:34: ERROR:
Could not resolve "twenty-sdk/define"`.

Triggering cause (why now): `create-twenty-app@2.5.0` (the current npm
`latest`) ships a template tarball without `.yarnrc.yml`, so new
projects silently default to Yarn PnP instead of `node-modules`.

Evidence:
- Source template contains `.yarnrc.yml` with `nodeLinker: node-modules`
[ref: read
`packages/create-twenty-app/src/constants/template/.yarnrc.yml`].
- Published npm tarball for `create-twenty-app-2.5.0.tgz` does **not**
contain `template/.yarnrc.yml` (but does contain renamed
`gitignore`/`github`) [ref: tarball listing command output: `hasYarnrc:
false`].
- Dotfile-preservation logic in `copyBaseApplicationProject()` only
renames `gitignore` and `github`; it does not preserve `.yarnrc.yml`
[ref: read `packages/create-twenty-app/src/utils/app-template.ts`].
- `npm dist-tags` shows `latest: 2.5.0`, so users following docs with
`@latest` receive this broken scaffold path now [ref: npm registry
query].

Why this is attributable to a specific change:
- Commit `15eb3e7edccdf4e9770a00a07bfbd026420f7c3b` introduced
dotfile-preservation mechanics for template publish
(`gitignore`/`github`) but left out `.yarnrc.yml`, creating the
regression window for newly scaffolded apps [ref: `git show --stat
15eb3e7...`, `git blame` on `renameDotfiles()`].

## Fix

Implemented a targeted fix in `create-twenty-app` so `.yarnrc.yml` is
preserved through npm packaging the same way `.gitignore` and `.github`
are handled.

What changed:
1) Template dotfile preservation
- Removed
`packages/create-twenty-app/src/constants/template/.yarnrc.yml`
- Added `packages/create-twenty-app/src/constants/template/yarnrc.yml`
with identical content:
  - `nodeLinker: node-modules`

This avoids npm stripping the file from the published tarball.

2) Scaffold rename logic
- Updated `packages/create-twenty-app/src/utils/app-template.ts`:
- Added `{ from: 'yarnrc.yml', to: '.yarnrc.yml' }` in
`renameDotfiles()`
  - Updated progress text and inline comment to include `.yarnrc.yml`

So generated apps reliably restore `.yarnrc.yml` after template copy.

3) Regression test
- Updated
`packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts`:
  - Added a test asserting `yarnrc.yml` is renamed to `.yarnrc.yml`
  - Added a small constant for the test path

This locks the behavior and prevents reintroducing the publish omission
regression.

Validation notes:
- Attempted to run the focused Jest test, but execution failed due
missing workspace dependency state (`@nx/jest/preset` / node_modules
state not installed in this environment).

## Original request

fix(create-twenty-app): preserve .yarnrc.yml in template

_Created by Sonarly by autonomous analysis (run 43375)._

---------

Co-authored-by: sonarly-bot <sonarly@sonarly.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-20 07:39:53 +00:00
f3aadbbb66 chore(server): drop leftover favorite and favoriteFolder workspace objects (#20744)
## Summary

- Adds a 2.7.0 workspace upgrade command
`upgrade:2-7:drop-favorite-objects` that removes the legacy `favorite`
and `favoriteFolder` object metadata (and their workspace tables) from
every active or suspended workspace.
- The records were migrated to `navigationMenuItem` in the 1.17/1.18
upgrades and the entity code was deleted in #19536, but the
per-workspace metadata rows were never cleaned up — so they still
surface in the "Existing objects" settings list and expose stale CRUD
tools to the AI/MCP layer (e.g. the model can hallucinate
`create_favorite_folder` against a real-looking schema).

## Implementation notes

- Modeled on
[`upgrade:2-3:drop-message-direction-field`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777400000000-drop-message-direction-field.command.ts),
but at object granularity.
- Uses `ObjectMetadataService.deleteOneObject({ isSystemBuild: true })`
so all cascading is handled by the existing pipeline: field metadata,
indexes, relation fields on other workspace entities, command menu
items, and the workspace data tables. Views and orphaned
`navigationMenuItem` rows pointing at favorite views are removed by the
existing `onDelete: 'CASCADE'` FKs.
- Deletion order: `favorite` first (holds a relation to
`favoriteFolder`), then `favoriteFolder`.
- Both objects are flagged `isSystem: true`, hence `isSystemBuild: true`
on the call.
- Idempotent: workspaces where the object is already absent are logged
and skipped.
- Honors `--dry-run`.
- Universal identifiers are hard-coded because the matching
`STANDARD_OBJECTS` entries were deleted in #19536.

## Test plan

- [ ] Run on a workspace that still has `favorite` / `favoriteFolder` in
`core.objectMetadata` (verify in prod-like DB beforehand) and confirm
both objects, their fields, indexes, relation fields on linked objects,
views, and the workspace data tables are gone after running.
- [ ] Re-run on the same workspace — confirm it logs "already absent"
and exits clean (idempotency).
- [ ] Run on a workspace where the objects don't exist (e.g. fresh
local) — confirm clean no-op.
- [ ] Run with \`--dry-run\` first — confirm log output and no DB
mutations.
- [ ] Confirm the "Existing objects" settings page no longer lists
Favorites / Favorite Folders after the migration.

## Safety check before rollout

Before running in prod, verify no workspace has live (non-soft-deleted)
favorite data that didn't make it to \`navigationMenuItem\`:

\`\`\`sql
-- Per workspace
SELECT count(*) FROM workspace_xxx.favorite WHERE "deletedAt" IS NULL;
\`\`\`

Should be ~0 in workspaces that ran the 1.17 / 1.18 migrations.

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-20 07:21:52 +00:00
e494bc7006 chore: sync AI model catalog from models.dev (#20751)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-20 09:06:14 +02:00
martmullandGitHub 91e0e07af4 fix: render PAGE_LAYOUT nav items with standard icon tile and compute… (#20743)
## Before

<img width="434" height="583" alt="image"
src="https://github.com/user-attachments/assets/a336620f-3f85-4d20-ba58-b20d779ba3b9"
/>


## after

<img width="539" height="600" alt="image"
src="https://github.com/user-attachments/assets/f88e4793-e1a1-4215-baf1-289c413d9dac"
/>
2026-05-20 06:17:39 +00:00
martmullandGitHub 7ab6f5719f Update default widget gridPosition (#20740)
move DEFAULT_WIDGET_SIZE to twenty-shared and use it in sync application
manifest
2026-05-20 06:15:53 +00:00
Félix MalfaitandGitHub d17393e91d feat(website): migrate dev hostname website-new.twenty-main.com → website.twenty-main.com (#20747)
## Summary
Last "-new" trace in the source repo, follow-up to the rename in #20745.
The dev Worker custom domain swaps from `website-new.twenty-main.com` to
`website.twenty-main.com`, matching the prod pattern (no "-new"
anywhere).

## Live operations already performed
- Deleted the legacy CNAME at `website.twenty-main.com` that pointed at
the dev EKS NLB (record id `52b4a4174dfd382ecf38111b7f08e642`, was the
Docusaurus dev deploy that had been 503'ing)
- Redeployed `twenty-website-dev` Worker — Wrangler provisioned the new
custom domain via the CF API
- Verified `https://website.twenty-main.com/` returns 200 with
`x-opennext: 1` and `x-nextjs-cache: HIT`

The old hostname `website-new.twenty-main.com` is now unbound; Wrangler
removed its DNS record when the route disappeared from this file.
Visitors get 522, which is the desired state for a retired hostname.

## Companion infra PR
https://github.com/twentyhq/twenty-infra/pull/683 — removes
`charts/dev/apps/website` Helm chart (the legacy Docusaurus deploy) +
ArgoCD app, updates `cloudflare/website/dev.env` and
`docs/4-environments.md`.

## Out of scope
- Legacy `website.twenty-staging.com` and `website.twenty.com` are still
alive serving Docusaurus content. Decommissioning those is a separate
decision (those URLs may still be linked externally).
2026-05-19 23:55:08 +02:00
Félix MalfaitandGitHub 658bdf3e57 chore(website): rename twenty-website-new → twenty-website (#20745)
## Summary
Follow-up to the Cloudflare/OpenNext migration (#20741). Now that the
legacy `twenty-website` package was already removed in #20270, the
`-new` suffix on the marketing site package is no longer meaningful.

## What changes
- **Directory rename**: `git mv packages/twenty-website-new
packages/twenty-website` (1213 files moved, no content change)
- **Package + nx config**: `package.json` and `project.json` name fields
updated, `sourceRoot` repointed
- **Source refs**: `load-local-articles.ts` and
`load-local-release-notes.ts` had a hardcoded `'twenty-website-new'`
segment in their monorepo-root fallback path;
`app/[locale]/releases/page.tsx` had display strings showing where to
add content
- **External refs**: root `package.json` workspaces, root `CLAUDE.md` /
`README.md`, `twenty-sdk` + `create-twenty-app` READMEs,
`.vscode/twenty.code-workspace`, `.cursor/rules/changelog-process.mdc`,
Crowdin config + the three `website-i18n-*` CI workflows +
`ci-website.yaml`
- **Docker cleanup**:
`packages/twenty-docker/twenty-website-new/Dockerfile` deleted; the two
Makefile targets (`prod-website-new-build` / `prod-website-new-run`)
that referenced it removed — EKS deploy was retired in the Cloudflare
migration
- **`yarn.lock`** regenerated against the new workspace path

## What's deliberately not in this PR
The dev hostname `website-new.twenty-main.com` in `wrangler.jsonc` stays
for now. Migrating it to `website.twenty-main.com` needs coordinated DNS
deletion (current CNAME points at the legacy Docusaurus NLB and serves
503s) and removal of the matching legacy `website` Helm chart in
`twenty-infra`. Flagged as a separate cleanup.

Companion infra PR: https://github.com/twentyhq/twenty-infra/pull/682
(workflow paths + Terraform ECR + docs)

## Test plan
- [x] `yarn install --immutable` resolves clean against the new path
- [x] `npx nx typecheck twenty-website` passes
- [x] `npx nx lint twenty-website` passes
- [ ] CI on this PR confirms the same on a fresh checkout
- [ ] After merge: trigger `Deploy Website` workflow against
`environment=dev` to confirm the renamed working-directory deploys
correctly
2026-05-19 23:42:09 +02:00
Félix MalfaitandGitHub 24a836cc7d feat(website-new): add Cloudflare Workers deployment via OpenNext (#20741)
## Summary
- Adds `@opennextjs/cloudflare` adapter so `packages/twenty-website-new`
can deploy to Cloudflare Workers
- Two environments (`dev` / `prod`) wired via `wrangler.jsonc` env
blocks
- Existing Docker / EKS build path is untouched in this PR — the cutover
happens in the paired infra PR

Pairs with: https://github.com/twentyhq/twenty-infra/pull/__ (to be
opened, will swap CI + decommission Helm/ArgoCD)

## Files added
- `packages/twenty-website-new/wrangler.jsonc` — Worker config,
`nodejs_compat` flag, R2 incremental cache, Cloudflare `IMAGES` binding,
env-specific routes (`website-new.twenty-main.com` for dev; `twenty.com`
+ `www.twenty.com` for prod)
- `packages/twenty-website-new/open-next.config.ts` — minimal config
using `r2IncrementalCache`
- `packages/twenty-website-new/.dev.vars.example` — local secrets
template (`STRIPE_SECRET_KEY`, `ENTERPRISE_JWT_PRIVATE_KEY`)
- `packages/twenty-website-new/public/_headers` — immutable cache
headers for `/_next/static/*`

## Files modified
- `packages/twenty-website-new/package.json` — adds
`@opennextjs/cloudflare`, `wrangler` to devDeps; adds `preview`,
`deploy:dev`, `deploy:prod`, `cf-typegen` scripts
- `packages/twenty-website-new/next.config.ts` — calls
`initOpenNextCloudflareForDev()` (no-op outside `next dev`); preserves
Linaria CommonJS export
- `packages/twenty-website-new/.gitignore` — ignores `.open-next/`,
`.wrangler/`, `.dev.vars`, generated `cloudflare-env.d.ts`

## Compatibility notes
- `enterprise-jwt.ts` uses Node `crypto` + `Buffer` — works on Workers
with the `nodejs_compat` flag (compat date 2025-01-15, well past the
2024-09-23 minimum)
- `sharp` stays as a build-time dep (Next/Image asset processing);
runtime image optimization routes through the Cloudflare `IMAGES`
binding
- Linaria runs at build time, unaffected
- Stripe SDK is HTTP-based, fine on Workers

## One-time CF setup required before this PR is useful
The infra PR adds GitHub Actions wiring, but the Cloudflare account
itself needs:
- R2 buckets: `twenty-website-cache-dev`, `twenty-website-cache-prod`
- Worker secrets per env (via `wrangler secret put --env <dev|prod>`):
`STRIPE_SECRET_KEY`, `ENTERPRISE_JWT_PRIVATE_KEY`
- An API token with `Workers Scripts:Edit`, `Workers R2 Storage:Edit`,
`Zone DNS:Edit` on the `twenty.com` zone — stored as
`CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` in the infra repo's
GitHub secrets
- The Cloudflare Images subscription enabled on the account (binding is
configured; \$5/mo + per-transformation pricing)

## Follow-up (out of scope)
- Rename `packages/twenty-website-new` → `packages/twenty-website` and
delete the legacy `packages/twenty-website` (mechanical, separate PR to
keep this diff reviewable)
- Remove `packages/twenty-docker/twenty-website-new/` once the EKS
deploy is fully retired

## Test plan
- [ ] `yarn install` resolves new devDeps cleanly
- [ ] `cd packages/twenty-website-new && npx next build` still succeeds
(Linaria path untouched)
- [ ] `yarn preview` builds the Worker locally and serves on
http://localhost:8788
- [ ] Smoke: `/`, `/pricing`, an enterprise-key-signing flow (needs
`.dev.vars` populated)
- [ ] After CF resources are provisioned: `yarn deploy:dev` succeeds and
`website-new.twenty-main.com` serves the new Worker
2026-05-19 23:23:54 +02:00
Charles BochetandGitHub 1a9f786e42 refactor(filters): pass fieldMetadataItems array to dispatcher (#20737)
## Summary

Alternative to #20717. Same goal (clean up the filter dispatcher API
after #20670) but smaller and follows the codebase's "pass data, not
behavior" style.

The dispatcher takes a `fieldMetadataItems: FieldShared[]` array
directly instead of a `findFieldMetadataItemById: (id) => FieldShared |
undefined` callback. The util builds the id lookup internally — once per
call, used for both source-field and relation-target-field lookups. No
new types, no separate hydration step.

## What changes

**`twenty-shared`**
- `computeRecordGqlOperationFilter` /
`turnRecordFilterIntoRecordGqlOperationFilter` /
`turnRecordFilterGroupsIntoGqlOperationFilter`: replace
`findFieldMetadataItemById` param with `fieldMetadataItems` /
`fieldMetadataItemById` (internal Map).
- Remove the exported `FindFieldMetadataItemById` type.
- `turnAnyFieldFilterIntoRecordGqlFilter`: rename its internal
`fieldById` Map for consistency.
- Tests updated to pass arrays.

**Frontend (15 call sites)**
- Switch from `fieldMetadataItemByIdMapSelector` to
`flattenedFieldMetadataItemsSelector`.
- Pass `fieldMetadataItems: flattenedFieldMetadataItems` to the
dispatcher.
- `useFindManyRecordsSelectedInContextStore` keeps the Map selector
because it still does a per-filter lookup for the soft-delete check.

**Server (5 call sites)**
- Pass
`Object.values(flatFieldMetadataMaps.byUniversalIdentifier).filter(isDefined)`.

## Why this over #20717

#20717 moves resolution into a separate hydration step + introduces a
`HydratedRecordFilter` type. The bug that #20717 originally surfaced was
Sentry catching 4 critical runtime errors during review
(`fieldMetadataItemByIdMap` declared but not passed). The added type and
the explicit hydration boundary are extra surface area for not much
benefit — the existing API was a callback wrapping a Map at every call
site, and the natural simplification is to just pass the Map (or its
array) directly.

Net diff: **196 insertions, 203 deletions** (~7 lines net removed). 32
files.

## Test plan
- [x] Shared filter unit tests pass (461 tests)
- [x] Frontend filter/context-store tests pass (13 tests)
- [x] Frontend typecheck passes
- [x] Server typecheck passes
- [x] Lint passes (frontend + server)
- [ ] Integration tests on #20670 still pass — workflow find-records +
chart-data with relation-traversal filter still work end-to-end through
the new array param
2026-05-19 22:49:53 +02:00
WeikoandGitHub 265d2edc83 Fix QueryRunnerAlreadyReleasedError in sign-in-up service (#20734)
## Context

When signing up on a new workspace,
`SignInUpService.signUpOnNewWorkspace`
manually drove a transaction with `createQueryRunner` /
`startTransaction` /
`commitTransaction` / `rollbackTransaction` / `release`.

If the underlying Postgres connection dropped mid-transaction
(`idle_in_transaction_session_timeout`, server-side termination), the
`pg`
client's `'error'` event fires. TypeORM's connect-time listener responds
by
calling `release()` on the `QueryRunner`, which sets `isReleased = true`
but
deliberately does **not** touch `isTransactionActive`.

The `catch` branch then hit:

```ts
if (queryRunner.isTransactionActive) {
  await queryRunner.rollbackTransaction(); // throws QueryRunnerAlreadyReleasedError
}
throw error;
```
Error from Sentry
```typescript
QueryRunnerAlreadyReleasedError: Query runner already released. Cannot run queries anymore.
    at PostgresQueryRunner.query (.../PostgresQueryRunner.js:177)
    at PostgresQueryRunner.rollbackTransaction (.../PostgresQueryRunner.js:167)
    at SignInUpService.signUpOnNewWorkspace (.../sign-in-up.service.js:370)
```

## Changes

Replaced the hand-rolled transaction with
this.dataSource.transaction(...).
TypeORM's built-in wrapper already does what we need:

- starts/commits/rolls back the transaction
- wraps rollback in try { ... } catch { /* ignore */ }, so a connection
drop no longer masks the real error
- releases the QueryRunner unconditionally

## Note
Other fix would have been to do this
```typescript
  if (queryRunner.isTransactionActive && **!queryRunner.isReleased**) {
     try {
       await queryRunner.rollbackTransaction();
     } catch {
```
2026-05-19 15:32:36 +00:00
d345a6b2d6 Twenty fireflies integration (#20618)
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-19 15:23:36 +00:00
f8605763dd i18n - docs translations (#20736)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 17:25:12 +02:00
WeikoandGitHub ac432d3195 Add @WasRemovedInUpgrade decorator (#20729)
## Summary

Adds the symmetric counterpart to `@WasIntroducedInUpgrade` for the
upgrade-aware ORM. Today the framework can describe "this column will
exist once upgrade X applies" but not "this
column will stop existing once upgrade X applies". Plain field deletion
only works when nothing writes to the table during the mid-state window
between the binary booting and the drop
migration completing for a given workspace — fine for sparse tables
(`DropWorkspaceVersionColumn`, `DropPostgresCredentialsTable`), risky
for hot-write tables.

This PR ships the primitive on its own so the upcoming
`rolePermissionFlag.flag` drop has the framework support it needs. No
in-tree consumer yet — coverage is via unit tests against
synthetic entities.

### What's in it

- **New `@WasRemovedInUpgrade({ upgradeCommandName })` decorator**
(class- or property-scope) — mirrors `@WasIntroducedInUpgrade`, uses the
shared
`defineUpgradeMetadataOnClassOrProperty` helper, exposes class +
property getters.
- **`resolveEntityShapeAtUpgradeCursor`** now folds applied-removals
into the existing `hiddenPropertyNames` set. Intro-pending and
removal-applied share one hide bucket — both ask
TypeORM for the same thing.
- **`UpgradeAwareEntityMetadataAdapter`** now disables `isSelect`,
`isInsert`, **and** `isUpdate` for any hidden column, restoring
canonical values when the column comes back.
Previously only `isSelect` was flipped, which left an
INSERT-into-nonexistent-column hole the intro path was tacitly relying
on application code to avoid; this PR closes that hole for
both directions.
- **`validateUpgradeAwareEntityDecorators`** validates
`@WasRemovedInUpgrade` `upgradeCommandName` references, and surfaces a
new `removal-before-introduction` problem when a property
has both decorators with the removal step preceding the introduction
step.
2026-05-19 14:57:05 +00:00
e463a09e17 chore(server): remove unused CommandLogger from command module (#20638)
## Summary

This PR removes the unused `CommandLogger` implementation located at:

```
/commands/command-logger.ts
```


The Command application context is bootstrapped using `LoggerService`
from:

```ts
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
...
const loggerService = app.get(LoggerService);
...
// Inject our logger
app.useLogger(loggerService);
...

```

So `CommandLogger` is not imported, injected, or referenced anywhere in
the Command execution flow and is safe to remove.

## Note
There is another `CommandLogger` class at:
```
/database/commands/logger.ts
```

This one is only used within `database-command` module and is unrelated
to the Command module logger being removed in this PR.

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-05-19 14:44:34 +00:00
nitinandGitHub 83b10ad698 fix(server): sync command menu item availability expressions on existing workspaces (#20719)
Two fixes via one workspace command:

1. Gates 5 standard command menu items behind `pageType == "INDEX_PAGE"`
--
   `importRecords`, `exportView`, `seeDeletedRecords`, `createNewView`, 
`hideDeletedRecords`. They currently appear (and crash or do nothing) on
   RECORD_PAGE.

2. Fixes Edit Layout missing from older workspaces -- root cause is 
`conditionalAvailabilityExpression` drift between source-of-truth
constants
and the workspace DB (e.g. #20556 removed a feature flag from the
expression
   without syncing existing workspaces).

The 2-6 workspace command iterates all `STANDARD_COMMAND_MENU_ITEMS` and
reconciles any `conditionalAvailabilityExpression` that differs from the
constant. Idempotent -- already-correct rows are skipped.

Deferred: `deleteRecords` doesn't refetch the current record after
deletion
on RECORD_PAGE (mutation fires but UI shows stale state until refresh)
--
different fix shape (frontend handler), separate PR.
2026-05-19 14:42:40 +00:00
Abdul RahmanandGitHub 08e7e4819b use declared outputSchema for logic-function steps (#20679)
When a logic function declares
`workflowActionTriggerSettings.outputSchema`, use it as the step's
initial output schema so downstream steps can pick variables without
first running the Test tab. A successful test run still overrides the
schema with the inferred shape, preserving "test wins" behavior. Falls
back to the existing "Generate Function Output" LINK placeholder when no
schema is declared (custom code steps, older functions).


https://github.com/user-attachments/assets/af9c45ed-d623-4234-be9f-46812fd06e2e
2026-05-19 14:41:00 +00:00
EtienneandGitHub 827f24df2b fix(ai) - add ai model preferences fallback (#20704)
**Problem** 
AI_MODEL_PREFERENCES, JSON env var is not supported +
IS_CONFIG_VARIABLES_IN_DB_ENABLED=false in twenty cloud server
-> No option to set AI_MODEL_PREFERENCES

**Solution** 
AI_MODEL_PREFERENCES supports three override sources beyond the
hardcoded code defaults, in priority order:

- DB (IS_CONFIG_VARIABLES_IN_DB_ENABLED=true), the only writable source;
admin-panel mutations persist here
- ENV not usable in Twenty Cloud, which does not handle JSON-format env
vars
- **Introduced in this PR** --> File
(AI_MODEL_PREFERENCES_STORAGE_PATH), a read-only startup fallback, the
only viable override in Cloud/self-managed deployments where DB config
is disabled and JSON env vars are unsupported.
2026-05-19 13:19:24 +00:00
martmullGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
d5ff9eb515 Create twenty app improvements (#20688)
create-twenty-app updates:
- remove --example option
- sync --once when scaffolding an applicaiton
- rename --api-url option to --workspace-url
- create a standalone page when scaffolding an app
<img width="1494" height="765" alt="image"
src="https://github.com/user-attachments/assets/0e35ed0c-b0aa-466c-9f56-7939294fd2cf"
/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-19 13:12:47 +00:00
71a7a3c42d i18n - website translations (#20724)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 15:17:23 +02:00
cbac2ba0bf i18n - translations (#20725)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 15:15:56 +02:00
4452b0f03d i18n - website translations (#20723)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 15:09:49 +02:00
Paul RastoinandGitHub 57f13c9b92 [CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction
Prevent any cross user `connectedAccount` `connectionParamaters` leak
Also encrypt in db all `connectionParameters` password
Never return any password through `DTO` anymore
The settings now allow update mutation without providing the password in
edition mode

Verified all `connectionParameters.password` interaction

## Integration tests
- Added more coverage for both failing and successful paths
- Introduced a new env var that allow bypass the provider connection
test

## Legacy connected Account decryption support
Stop allowing non encrypted decryption on `accessToken` and
`refreshToken`, only allow legacy decryption on refactored
`connectionParameters`

## Upsert ownership
Completely got rid of the connected workspace schema context which is
legacy
Also now a user can only upsert a connected account for him only..

## New UI
<img width="1770" height="1852" alt="image"
src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b"
/>
If in edition the password is by default disabled
It needs to be selected as being edited to be enabled

## Next
- Refactor tool permissions flag not to include connected accounts
- Remove the legacy connected standard object
- Refactor and improve connected account resolver auth
2026-05-19 12:56:44 +00:00
neo773andGitHub 72c0c36db5 fix(twenty-front): prevent connected account row overflow on long status label (#20713)
Reproducible on German language

Before

<img width="638" height="262" alt="SCR-20260519-ofhi"
src="https://github.com/user-attachments/assets/633ddd9a-203d-472a-bf29-379d6f088e80"
/>


After

<img width="806" height="434" alt="SCR-20260519-oesa"
src="https://github.com/user-attachments/assets/ca37c06b-e439-4156-8447-0e75a5f3fe9c"
/>


/closes #20594
2026-05-19 12:39:00 +00:00
Charles BochetandGitHub 77514ad14a fix(server): backport relationTargetFieldMetadataId column-add to 2.4 and 2.5 fast instance (#20721)
## Summary

Cross-version upgrade from a **v2.3 or v2.4 baseline** to v2.6.x
currently fails at the 2.5 workspace command
`NormalizeCompositeFieldDefaults`:

```
[QueryFailedError] column ViewFilterEntity.relationTargetFieldMetadataId does not exist
  at WorkspaceFlatViewFilterMapCacheService.computeForCache
```

Reproduced locally via Docker cross-version upgrade (v2.6.1 against
`twentycrm/twenty:v2.3` and `:v2.4` images on a freshly-seeded DB).

### Root cause

The column-add is already declared in two places:
-
`2-3/.../1747234300000-add-relation-target-field-metadata-id-to-view-filter`
(backport from #20664)
-
`2-6/.../1798000005000-add-relation-target-field-metadata-id-to-view-filter`

But the runner's `resolveStartCursor`
(`upgrade-sequence-runner.service.ts`) advances forward from
`lastAttemptedCommandName` and never re-runs commands inserted *behind*
the cursor:

- **fresh install through 1.23 → 2.6.x**: cursor < 2.3 → 2.3 backport
runs → column added before 2.5 workspace ✓
- **v2.3 baseline → 2.6.x**: cursor past 2.3 → 2.3 backport skipped →
2.5 workspace `NormalizeCompositeFieldDefaults` crashes ✗
- **v2.4 baseline → 2.6.x**: cursor past 2.4 → 2.3 backport skipped →
same crash ✗
- **v2.5 baseline → 2.6.x**: cursor past 2.5 → 2.5 workspace already
applied (ran against v2.5 source's older entity without the column) →
2.6 fast adds the column ✓

The 2.6 fast `1798000005000` runs *after* the 2.5 workspace command, too
late to help v2.3 / v2.4 baselines.

### Fix

Mirror the existing 2.3 Early backport at two more versions:

-
`2-4/.../1747234400000-add-relation-target-field-metadata-id-to-view-filter`
— covers v2.3 baseline (runs in 2.4 fast, before any 2.4/2.5 workspace
command)
-
`2-5/.../1747234500000-add-relation-target-field-metadata-id-to-view-filter`
— covers v2.4 baseline (runs in 2.5 fast, before
`NormalizeCompositeFieldDefaults`)

Both use `ADD COLUMN IF NOT EXISTS` (idempotent) and `DROP COLUMN IF
EXISTS` for the down. No FK / index — those still live in the 2.6 file,
which runs as a no-op for the column on already-fixed DBs.

Pre-2.6 codebases can't use `@WasIntroducedInUpgrade` (#20686 only lands
in 2.6), so this "ladder of backports" remains the operative pattern.

## Audit context

Locally walked `v1.23 / v2.0 / v2.1 / v2.2 / v2.3 / v2.4 / v2.5 →
v2.6.1`:

| Baseline | Result |
|---|---|
| v1.23 | PASS |
| v2.0 | PASS |
| v2.1 | PASS |
| v2.2 | PASS |
| **v2.3** | **FAIL** (this PR) |
| **v2.4** | **FAIL** (this PR) |
| v2.5 | PASS |
2026-05-19 14:46:56 +02:00
3d5c6cb0e5 i18n - website translations (#20722)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 14:34:21 +02:00
Abdullah.andGitHub 4b12fda3f5 [Website] Hide Product and Articles from navigation and remove language switcher. (#20718)
Restore "Why" as the top-level nav item, remove Product and Articles
from menu and footer, and hide the language switcher in the footer for
this release. Pages remain accessible via direct URL and stay indexed.

Will re-add once the release is out.
2026-05-19 12:14:55 +00:00
MarieandGitHub 9fddaf53d5 Fix BUILDER_INTERNAL_SERVER_ERROR message (#20720)
The throw site was passing (code, message) to a constructor whose
signature is (message, code), so exception.message ended up as the
literal string "BUILDER_INTERNAL_SERVER_ERROR" and the real
error.message was stored in exception.code where nothing reads it.
Swapping the two args puts the real error message back into
exception.message, which is the field Yoga's error handler copies into
the GraphQL response's top-level message — and that's the field the CLI
prints.
2026-05-19 12:09:31 +00:00
Charles BochetandGitHub 3512849004 refactor(server): drop logo select workaround in flat-application cache (#20708)
## Summary

Replaces the temporary `select: { ... }` workaround in
`WorkspaceFlatApplicationMapCacheService` (introduced by #20159) with a
property-level `@WasIntroducedInUpgrade` decorator on
`ApplicationEntity.logo`.

#20159's own description called itself out: *"This is a temporary fix
for cross-version upgrade process, a better fix would be to expose an
hasInstanceCommandBeenRun() util (and later a decorator)"*. The
decorator now exists, courtesy of #20686.

## Root cause recap

`ApplicationEntity.logo` is added by
`2-2-instance-command-fast-1777539664664-add-logo-to-application.ts`.
The column is declared on the entity class, so before that instance
command runs (i.e. on a cross-version upgrade from a 2.1 or older
baseline), TypeORM's bare `repository.find()` emits `SELECT \"logo\" …`
against a table that doesn't have the column yet → upgrade aborts.
#20159 worked around this by listing every column **except** `logo` in
an explicit `select`, with an `as unknown as
FindOptionsSelect<ApplicationEntity>` cast.
2026-05-19 10:41:22 +00:00
Charles BochetandGitHub 72ce77864e feat(server): Enterprise cron that rotates the current JWT signing key (#20612)
## Summary
Adds a daily Enterprise-only cron that rotates the current ES256 JWT
signing key once it has been current for `SIGNING_KEY_ROTATION_DAYS`.
Manual rotation from the admin panel is unaffected.

### Behaviour
- `SIGNING_KEY_ROTATION_DAYS` is **opt-in**: when unset, the cron is a
no-op.
- Rotation flips `isCurrent` and clears the previous key's `privateKey`
in the same transaction, then inserts the new `isCurrent=true` row.
- The previous key's row is kept (`revokedAt` stays `null`) so its
`publicKey` can keep verifying tokens it signed until they expire; only
the encrypted `privateKey` is wiped since it can no longer be used to
sign.
- **No auto-revocation** — revoking a key remains a manual admin action,
reserved for leak / emergency response.
- The cron is also a no-op when `EnterprisePlanService.isValid()` is
`false`.

### Wiring
- `JwtKeyManagerService.rotateCurrent()`
- `SigningKeyRotationService.rotateIfDue()` (reads
`SIGNING_KEY_ROTATION_DAYS`, skips when unset)
- `RotateSigningKeysCronJob` (Enterprise-gated, rethrows on failure)
registered in `JwtModule`
- `RotateSigningKeysCronCommand` registered with `cron:register:all`
- `ROTATE_SIGNING_KEYS_CRON_PATTERN = '15 3 * * *'` (daily, no-op until
threshold)

Operator documentation lives in #20611 (docs PR).
2026-05-19 10:41:04 +00:00
neo773andGitHub 6cd069ce40 messaging minor perf improvement (#20687)
This PR adds two changes

1. Pass `lite:true` to `ExecuteInWorkspaceContextOptions` introduced in
https://github.com/twentyhq/twenty/pull/18376

2. Remove redundant gmail alias call, it adds 300ms every cron job, we
only do it once now when user connects, realistically I don't see people
changing their aliases every day you only set it up once

actual real diff is small, it's just prettier format contributing to
diff

Objective decrease total time take per job
2026-05-19 10:38:34 +00:00
db4a05301a i18n - website translations (#20712)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 12:27:49 +02:00
Abdullah.andGitHub f9e3683518 [Website] Change product hero to reveal tabs on scroll. (#20707)
Here's the video - it still needs refinement, but we want to hide
articles and product pages to push out a release today. Merging this as
a checkpoint so the next PR can hide these pages from navigation for the
release.


https://github.com/user-attachments/assets/eb5048b2-d3df-4920-a62a-5b2617d11e4a
2026-05-19 10:12:44 +00:00
fecea1bfae i18n - translations (#20710)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 12:13:33 +02:00
3281d37bdf Fix(twenty-front): BlockNote slash command shows empty state when no match (#20689)
Fixes: #20625
Original PR: #20626

New changes:
- Now the text says "Close menu" instead of "No command found".
- "Close menu" is interactive like other commands ( With keyboard and
with mouse ).
- It closes automatically when user type 3 extra character past the
point of no result.


https://github.com/user-attachments/assets/9c1315b4-5a63-424d-8da8-dc1283535725

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-19 09:47:18 +00:00
Félix MalfaitandGitHub 291ce5ccdb fix(filters): make filter dispatcher own relation-target resolution (#20670)
## Summary

Two relation-traversal bugs surfaced post-merge of #20533, both rooted
in the same architectural smell: the GraphQL filter dispatcher took a
flat `fields: FieldShared[]` array and silently dropped any filter whose
`relationTargetFieldMetadataId` wasn't in that array. Callers had to
remember to pre-augment the list with relation targets — and 16+ call
sites did not all know this.

This PR fixes both bugs and removes the smell.

### Bug 1 — Save as new view loses the relation target

`useCreateViewFromCurrentView` built the create-filter input without
`relationTargetFieldMetadataId`. The saved view's filter persisted
without the traversal — on reload the chip showed "Company contains
'air'" instead of "Company → Name contains 'air'". Discarded at save
time, not at read time.

Fix: include `relationTargetFieldMetadataId` in the create input.
(Commit 1.)

### Bug 2 — Workflow Search Records drops one-hop traversals

`FindRecordsWorkflowAction` built its fields list from
`flatObjectMetadata.fieldIds` only (source object's fields). The shared
dispatcher then couldn't resolve the relation target field on the
related object and silently dropped the filter — a configured "People
where Company → Name Contains 'Airbnb'" came through as `{ and: [] }`.

This was the same shape as bugs already fixed in 5 other call sites
(chart filters, view filters, record table, etc.). The pattern was:
caller forgets to augment fields → dispatcher silently drops the filter.

Fix (commit 2): change the dispatcher to take a
`findFieldMetadataItemById: (id) => FieldShared | undefined` resolver
callback. Both source-field and relation-target-field lookups go through
the same resolver, so callers no longer need to know about the
augmentation requirement. Frontend callers pass a workspace-wide
resolver built from `flattenedFieldMetadataItemsSelector`; server
callers wrap `findFlatEntityByIdInFlatEntityMaps` on
`flatFieldMetadataMaps`. In both cases relation-target lookups just
work, because the resolver can see fields on related objects.

## Why this matters

Before: "if you call the dispatcher, pre-augment your fields list with
relation targets, or filters get silently dropped." An invariant only
enforceable by code review, broken often enough to ship two user-visible
bugs in one week.

After: the dispatcher resolves field ids itself. There's no list to
forget to augment. The failure mode (filter silently dropped) becomes
structurally impossible at the dispatcher boundary.

Net diff: 240 insertions, 319 deletions. Removed
`augmentFieldsWithRelationTargets` (frontend) and the workflow
whack-a-mole code (server).

## Test plan
- [ ] Save view: create an advanced filter using a one-hop relation
traversal, click "Save as new view", reload, confirm the chip still
reads "Source → Target operator value"
- [ ] Workflow: configure a Search Records action with a
relation-traversal filter, run the workflow, confirm the filter is
actually applied
- [ ] Dashboard chart: configure a chart with a relation-traversal
filter, confirm the chart data respects it
- [ ] Record table, group-by, calendar, total count, footer aggregates:
all continue to work with both plain and relation-traversal filters
2026-05-19 11:55:22 +02:00
martmullandGitHub a9634b027e Stop bundling twenty-ui react cjs runtime code (#20703)
For this front component using Avatar from `twenty-sdk/ui`

```typescript
import { defineFrontComponent } from 'twenty-sdk/define';
import { Avatar } from 'twenty-sdk/ui';

// React component - implement your UI here
const Component = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <Avatar avatarUrl={null} placeholder={'test'} />
      <h1>My new component!</h1>
      <p>This is your front component: fc-test</p>
    </div>
  );
};

export default defineFrontComponent({ ... });
```

## Before
<img width="3024" height="1964" alt="image"
src="https://github.com/user-attachments/assets/64479d9a-2316-4782-9552-c3982d85f97c"
/>

## After

<img width="1150" height="567" alt="image"
src="https://github.com/user-attachments/assets/67f34da2-a546-40e3-9f5d-62a5de6e4146"
/>
2026-05-19 09:26:41 +00:00
ae41751a8c i18n - docs translations (#20705)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 11:34:14 +02:00
11d8679f65 i18n - docs translations (#20702)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 09:40:04 +02:00
05f31c1837 docs(self-host): document ENCRYPTION_KEY, FALLBACK_ENCRYPTION_KEY and key rotation procedures (#20611)
## Summary
- Documents the new at-rest encryption envelope (`ENCRYPTION_KEY` /
`FALLBACK_ENCRYPTION_KEY`) introduced in v2.5+ and clarifies its
relationship to the legacy `APP_SECRET`-as-encryption-key path.
- Adds a new dedicated **Key rotation** guide covering manual /
Enterprise-cron JWT signing-key rotation, signing-key revocation, and
the online `ENCRYPTION_KEY` rotation procedure (including the new
\`secret-encryption:rotate\` CLI shipped in a follow-up PR).
- Updates the docker-compose quickstart to generate a dedicated
\`ENCRYPTION_KEY\` from day 1.
- Mentions the v2.5+ enc:v2 backfill in the upgrade guide.

English-only — the localized mirrors will be picked up by i18n CI.

## Test plan
- [ ] Mintlify build passes locally / in CI
- [ ] Sidebar entry renders under **Self-Host → Key rotation**
- [ ] Internal links to /developers/self-host/capabilities/key-rotation
resolve from setup.mdx, docker-compose.mdx and upgrade-guide.mdx

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 06:45:40 +00:00
2a92f34d06 chore: bump version to 2.7.0 (#20693)
## 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

## Checklist

- [ ] Verify version constants are correct

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-05-19 08:10:57 +02:00
d9b125efc5 i18n - website translations (#20694)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 08:10:35 +02:00
Charles BochetandGitHub e6b7f31fea fix(front): prevent standalone page layout crash from useTargetRecord (#20698)
## Context

Reported in production on `engineering.twenty.com` — standalone page
layouts (e.g. "Release overview") crash with the React error boundary
fallback ("Sorry, something went wrong"). The console shows:

```
Error: useTargetRecord must be used within a record page context (targetRecordIdentifier is required)
```

The minified stack trace points at `SidePanelToggleButto…`, but that's
just the bundle chunk name — the actual call site is
`PageLayoutTabsRenderer`.

## Root cause

#19296 added an unconditional `useTargetRecord()` call inside
`PageLayoutTabsRenderer` so it could read the target object's metadata
and hide tabs whose widgets reference deactivated relations:

```ts
const targetRecord = useTargetRecord();

const { objectMetadataItem } = useObjectMetadataItem({
  objectNameSingular: targetRecord.targetObjectNameSingular,
});
```

But `PageLayoutTabsRenderer` runs on **both** record pages and
standalone pages. On standalone pages, `StandalonePageLayoutPage`
intentionally sets `targetRecordIdentifier: undefined` in
`LayoutRenderingProvider`, which makes `useTargetRecord()` throw — and
the follow-up `useObjectMetadataItem()` would also throw on miss.
2026-05-19 00:40:37 +02:00
Charles BochetandGitHub bad1f20012 fix(server): handle legacy PK name in 2.6 rename-permission-flag upgrade (#20697)
## Summary

The 2.6 `RenamePermissionFlagToRolePermissionFlag` upgrade command
failed on staging and dev with:

```
[QueryFailedError] constraint "PK_a02789db60620a1e9f90147b50f" for table "rolePermissionFlag" does not exist
in RenamePermissionFlagToRolePermissionFlag1778235340020 (2.6.0) (instance fast)
```

### Root cause

TypeORM names PKs as `PK_<sha1(tableName_sortedColumnNames)[:27]>`. So:
- `permissionFlag_id` → `PK_a02789db60620a1e9f90147b50f`
- `settingPermission_id` → `PK_8c144a021030d7e3326835a04c8`
- `rolePermissionFlag_id` → `PK_76591adc8035c2e7b0cd6115136`

On databases initially migrated before the v1.5.5 migration squash
(#15183), the table was renamed `settingPermission` → `permissionFlag`
via the pre-squash migration
`1753149175945-renameSettingPermissionToPermissionFlag.ts`. That
migration renamed the table, the column, the unique index, and the role
FK, but **never renamed the PK constraint** — and Postgres does not
auto-rename constraints on `ALTER TABLE ... RENAME TO`. Those instances
therefore still carry the legacy PK name
`PK_8c144a021030d7e3326835a04c8`.

Fresh installs (squashed `setupMetadataTables` migration) instead have
the expected `PK_a02789db60620a1e9f90147b50f`.

The 2.6 upgrade only handled the fresh-install name, so it broke for any
DB that went through the historical rename chain.

### Fix

Replace the brittle `RENAME CONSTRAINT` with `DROP CONSTRAINT IF EXISTS`
for both historical PK names, followed by `ADD CONSTRAINT ... PRIMARY
KEY ("id")` with the canonical new name. The migration now converges to
the same PK name regardless of the DB's history.

The same pattern is applied symmetrically in `down()`.

### Why this is safe

- The whole instance command runs in a transaction
(`InstanceCommandRunnerService.runFastInstanceCommand`).
- The first statement (`ALTER TABLE ... RENAME TO`) takes `ACCESS
EXCLUSIVE` on the table, so the drop/add window for the PK is invisible
to any concurrent writer — they queue on the lock until commit.
- No FK references `rolePermissionFlag.id` at this point in the sequence
(migration 22 introduces an FK pointing at the new `permissionFlag`
catalog created in migration 21, not at the renamed grant table), so
dropping the PK does not cascade or block.
- `NOT NULL` and the `uuid_generate_v4()` default on `id` are
column-level and remain in place when the PK is dropped.

## Test plan

- [ ] Run 2.6 upgrade against a fresh-install database (PK =
`PK_a02789db60620a1e9f90147b50f`) — should succeed.
- [ ] Run 2.6 upgrade against a pre-squash database (PK =
`PK_8c144a021030d7e3326835a04c8`, reproducible on current staging/dev) —
should now succeed.
- [ ] Verify post-migration: `rolePermissionFlag` exists, PK is named
`PK_76591adc8035c2e7b0cd6115136`, all FKs and indexes named as expected.
- [ ] Run `down()` and verify table returns to `permissionFlag` with PK
`PK_a02789db60620a1e9f90147b50f`.
- [ ] Subsequent migrations (`1778235340021` permission-flag catalog,
`1778235340022` link, `1778235340023` backfill) still apply cleanly.
2026-05-18 23:04:15 +02:00
ce8ef261c1 Update pricing plan cards (#20614)
## Summary
- Update pricing top-card bullets to use workflow credits, keep full
customization, and show Organization-only features accurately.
- Make custom AI models self-host Organization-only and move custom
domain into the Cloud Organization card.
- Align pricing comparison rows for row-level permissions, encryption
key rotation, API call limits, custom domain availability, and self-host
custom objects/fields.

## Validation
- `lingui extract --overwrite --clean`
- `lingui compile --typescript`
- `node scripts/check-section-shape.mjs`
- `git diff --check`
- Playwright snapshot of `http://localhost:3002/pricing`

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-18 19:43:15 +00:00
Charles BochetandGitHub 1d3d3999e2 feat(server): upgrade-aware entity decorators for cross-version upgrades (#20686)
## What

When the same PR introduces a new core entity *and* adds a cache
provider that queries it, every workspace step from older versions that
runs before the introducing instance step hits `relation … does not
exist` — the cause of the failed [v2.6.0 staging-ci
run](https://github.com/twentyhq/twenty-infra/actions/runs/26042742000).
Same class of failure for renamed core entities and for new FK columns
hidden inside relation loads.

This PR adds **upgrade-aware entity decorators** + a runtime that adapts
TypeORM's view of the schema to the current `core.upgradeMigration`
cursor.

## Strategy

```
                    ┌────────────────────────────────┐
                    │  @Entity classes (final shape) │
                    │   + @WasIntroducedInUpgrade    │
                    │   + @WasRenamedInUpgrade       │
                    └───────────────┬────────────────┘
                                    │
              UpgradeSequenceRunner.run()
              ┌─────────────────────┴─────────────────────┐
              ▼                                           ▼
       step N+1 begins                          step N just completed
              │                                           │
              └────────► adapter.refresh() ◄──────────────┘
                            │
            reads core.upgradeMigration via
            UpgradeMigrationService.getLastAttemptedInstanceCommand
                            │
                            ▼
       ┌────────────────────────────────────────────────────────┐
       │  UpgradeAwareEntityMetadataAdapter                     │
       │  • mutates EntityMetadata.tableName / tablePath        │
       │     -> historical name for renames not yet applied     │
       │  • flips column.isSelect = false for not-yet-introduced│
       │     columns                                            │
       │  • tracks per-entity availability sidecar              │
       └─────────────────┬──────────────────────────────────────┘
                         │
                         ▼
       DataSource.getRepository wrapped at TypeOrmModule.forRoot:
       repo.find() / findOne() / count() / …
       ┌─────────────────────────────────────────┐
       │  wrapRepositoryWithUpgradeAwareProxy    │
       │  • entity unavailable -> short-circuit  │
       │     (find -> [], count -> 0,            │
       │      findOneOrFail -> EntityNotFound)   │
       │  • write -> Promise.reject(             │
       │      UpgradeUnavailableEntityWriteEx)   │
       │  • find({ relations: ['X'] }) with X    │
       │     unavailable -> X stripped           │
       └─────────────────────────────────────────┘
```

The decorator strings reference real `core.upgradeMigration.name` values
(`${version}_${className}_${timestamp}`). A boot-time validator walks
the actual `UpgradeSequenceReaderService.getUpgradeSequence()` and fails
fast on typos.

## Files

- New decorators:
`engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator.ts`,
`was-renamed-in-upgrade.decorator.ts`
- Runtime: `engine/twenty-orm/upgrade-aware/` (adapter, proxy, install
hook, state singleton, exceptions)
- Wired into `UpgradeSequenceRunnerService` (`refresh()` between steps)
and `TypeOrmModule.forRoot` (proxy install)
- 2-6 entity decorations: `RolePermissionFlagEntity` (rename history +
new `permissionFlagId` column), `PermissionFlagEntity` (new catalog)

## Validation

End-to-end local cross-version upgrade (v1.22 → HEAD): `28 workspace(s)
succeeded, 0 failed`; `upgrade:status → Instance: Up to date, 4 up to
date, 0 behind, 0 failed`. Full log excerpts and the
second-failure-found-and-fixed (`WorkspaceRolesPermissionsCacheService`
relation load) in [this
comment](https://github.com/twentyhq/twenty/pull/20686#issuecomment-4480036816).

## Test plan

- [x] Adapter spec covers rename mutation; proxy spec covers `find()`
short-circuit on unavailable entity. Resolver + validator + decorators
are covered by `resolve-entity-shape-at-upgrade-cursor.util.spec.ts`
(integration-level via real decorator application).
- [x] `nx lint:diff-with-main twenty-server` + `nx typecheck
twenty-server` clean
- [x] All 82 affected tests passing
- [ ] Cross-version-upgrade CI re-runs after this lands; v2.6.0 retag
once green

## Follow-ups deferred

- v2.7 `connectionProvider` rename repro as a permanent end-to-end test
artifact
- Extending the proxy to also cover `EntityManager.getRepository` and
`createQueryBuilder` if a non-`find()` upgrade-time consumer surfaces
2026-05-18 21:38:20 +02:00
EtienneandGitHub 89579f5225 fix(ai-chat) - upload files (#20681)
closes https://github.com/twentyhq/twenty/issues/20437

bonus : persist file filename for UI display
2026-05-18 15:50:02 +00:00
132d997474 i18n - translations (#20685)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-18 17:42:30 +02:00
7c252ff233 Fix 19026 deactivated relation unassignable (#19296)
PR to fix the bug #19026 

This PR will ensure that if an object has some relation deactivated, the
relation will not be visible in the side panel tab and will not be
assignable in the deactivated relation.

## Notes deactivated in People
<img width="1068" height="1106" alt="image"
src="https://github.com/user-attachments/assets/e8c2dbf3-5391-4dbc-8e40-79fcc44e8158"
/>

## Notes not visible in the side panel of people
<img width="2390" height="892" alt="image"
src="https://github.com/user-attachments/assets/308a78aa-2c6d-4d3d-b67c-b49795839aae"
/>

---------

Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
2026-05-18 15:25:51 +00:00
nitinandGitHub 4ba9c0ca0b [Navigation Drawer] Multiple fixes in settings and app drawer (#20634)
closes -
https://discord.com/channels/1130383047699738754/1487720717192527942



https://github.com/user-attachments/assets/6db2df8b-be01-4b5f-a958-575d87b41559

~~waiting on @Bonapara 's feedback!~~
2026-05-18 15:22:03 +00:00
Charles BochetandGitHub d03480472c perf(server): index messageChannel/calendarChannel for per-workspace sync crons (#20678)
## Summary

The messaging/calendar import crons each iterate every active workspace
and execute one `find` per workspace against `core."messageChannel"` /
`core."calendarChannel"` with the shape:

```
WHERE "workspaceId" = $1 AND "isSyncEnabled" = true AND "syncStage" = $2 [AND "type" <> $3]
```

There is currently no index supporting that shape, so the planner does a
seq scan on each table for every iteration. On prod-eu (RDS Performance
Insights, `rds-prod-eu-one`), these two queries are the top two by load
— together ~12 AAS, ~12 calls/sec — and have been the primary
contributor to the sustained 100% CPU since active workspace count grew.

This PR adds composite indexes on `(workspaceId, isSyncEnabled,
syncStage)` for both tables as an instance migration in 2.6.0.
2026-05-18 14:31:03 +00:00
8f3c336e62 i18n - docs translations (#20680)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-18 15:37:33 +02:00
Thomas des FrancsandGitHub e7a1448414 Add OpenAI Apps domain challenge file (#20677)
## Summary
- Add the OpenAI Apps domain verification token as a static well-known
file on the Twenty website.

## Why
The OpenAI Apps submission form allows a challenge base URL on the MCP
hostname or a parent hostname. Since the MCP hostname is
`api.twenty.com`, the parent origin `https://twenty.com` can serve the
challenge at `/.well-known/openai-apps-challenge` without adding an API
route.

## Validation
- `curl -I -L https://twenty.com/.well-known/openai-apps-challenge`
currently returns 404, confirming the file is not already live.
- `git diff --check origin/main...HEAD`
- Verified the PR diff is a single static file:
`packages/twenty-website-new/public/.well-known/openai-apps-challenge`.

## Submission setting
Use `https://twenty.com` as the Challenge Base URL after this is
deployed.
2026-05-18 12:29:53 +00:00
EtienneandGitHub fc74938d7b fix(billing) - query timeout (#20669)
Sonarly context : https://sonarly.com/issue/33412
Sentry issue :
https://twenty-v7.sentry.io/issues/7454613767/?project=4507072499810304
2026-05-18 12:29:47 +00:00
Thomas des FrancsandGitHub d5e65c563e Add MCP tool annotations (#20672)
## Summary

Adds explicit MCP tool annotations for the Twenty MCP server so ChatGPT
app submission review can inspect the exposed tools without relying on
protocol defaults.

## Changes

- Adds one-export annotation constants for closed-world read-only tools,
open-world read-only tools, and `execute_tool`.
- Attaches annotations to the five exposed MCP tools:
`search_help_center`, `get_tool_catalog`, `learn_tools`, `execute_tool`,
and `load_skills`.
- Marks `search_help_center` as read-only and open-world because it
performs outbound help-center HTTP requests.
- Keeps `get_tool_catalog`, `learn_tools`, and `load_skills` read-only
and closed-world.
- Keeps `execute_tool` non-read-only, open-world, and destructive
because it can route to tools that create/update/delete records or send
email.
- Returns annotations through `tools/list` and updates MCP tests to
cover them.

No output schemas are included in this PR.

## Validation

- `git diff --check origin/main...HEAD`
- `jest --config packages/twenty-server/jest.config.mjs
packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-tool-executor.service.spec.ts
packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts
--runInBand`

Note: the Jest command was run with arm64 Node because the available
shared `node_modules` install contains the arm64 SWC native binding.
2026-05-18 12:14:34 +00:00
5c8ddb0c12 i18n - translations (#20674)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-18 14:15:45 +02:00
db0547f503 [1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377.

## Summary

This PR separates available permission flags from per-role permission
flag grants.

Previously, `core.permissionFlag` stored the role assignment directly:
`roleId + flag`. This PR renames that legacy grant table to
`core.rolePermissionFlag`, then recreates `core.permissionFlag` as the
catalog of available permission flags.

## What changed

- Rename the existing `core.permissionFlag` grant table to
`core.rolePermissionFlag`.
- Add the new syncable `core.permissionFlag` catalog entity with key,
label, description, icon, permission type, relevance flags, and
custom/standard metadata.
- Add stable `SystemPermissionFlag` universal identifiers for the
built-in `PermissionFlagType` values.
- Seed the standard permission flags for every workspace under the
Twenty standard application.
- Backfill existing role grants:
  - create missing catalog rows for existing grant keys,
  - add `rolePermissionFlag.permissionFlagId`,
- migrate grants from the old string `flag` column to the new catalog
FK,
- replace the old `(flag, roleId)` uniqueness with `(permissionFlagId,
roleId)`.
- Rewire role permission flag caches, permission checks, role DTO
mapping, and `upsertPermissionFlags` to resolve through the catalog.
- Keep the existing public role permission API shape: product/app
surfaces still talk about `permissionFlags` and return `{ id, roleId,
flag }`.
- Update metadata flat-entity machinery, migration builders, validators,
action handlers, snapshots, generated schemas, docs, and app fixtures
for the new `permissionFlag` / `rolePermissionFlag` split.

## Behavior after this PR

- Existing permission flag grants keep working.
- Existing GraphQL role permission flows keep the same public naming.
- Standard permission flags are represented as catalog rows.
- Permission checks now compare grants through catalog universal
identifiers instead of the legacy `flag` column.
- Workspace deletion cleanup now verifies both `permissionFlag` and
`rolePermissionFlag`.

## What is not in this PR

- Public GraphQL CRUD for custom permission flags.
- App manifest support for declaring new custom permission flags.
- Frontend UI for creating or assigning custom permission flags beyond
the existing role permission flow.

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-05-18 11:57:47 +00:00
01535a3b3e fix(server): handle network errors in RestApiService catch block (#20644)
## Summary
- Added safe null check for `err.response?.data?.errors` in
`RestApiService.call()` catch block
- When the internal HTTP client fails with a network-level error
(ECONNREFUSED, timeout), `err.response` is `undefined` — accessing
`.data.errors` on it throws a `TypeError` which gets silently swallowed,
returning an empty 500
- Now falls back to throwing the raw error message for network failures
instead of crashing

## Changes
- `packages/twenty-server/src/engine/api/rest/rest-api.service.ts`

Fixes #20136

---------

Co-authored-by: Marie Stoppa <marie@twenty.com>
2026-05-18 09:51:42 +00:00
Shubham SinghandGitHub 45ac3e8218 fix(front): align currency icon vertically with amount text (#20646)
## Summary
- Replaced inline `<span>` wrapping the currency icon with a Linaria
styled component using `display: flex` and `align-items: center`
- The icon was misaligned with the amount text in table views and
settings because the inline span didn't vertically center the SVG icon

## Changes
-
`packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx`

Fixes #20640
2026-05-18 08:48:54 +00:00
cd09690d5d fix(server): correct OpenAPI schema for phones.additionalPhones (#20631)
Fixes #20629

Problem

The OpenAPI schema for PHONES composite fields documented
additionalPhones as string[], but the actual runtime type (defined in
phones.composite-type.ts) is Array<{ number: string, countryCode:
string, callingCode: string }>. This caused generated SDK types and API
docs for create/update payloads to be incorrect.

Root cause

A hardcoded mistake in
convert-object-metadata-to-schema-properties.util.ts — the
FieldMetadataType.PHONES branch set additionalPhones.items to { type:
'string' } instead of an object schema.

Changes


packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts
- Changed additionalPhones.items from { type: 'string' } to { type:
'object', properties: { number, countryCode, callingCode } }, matching
AdditionalPhoneMetadata.


packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
- Updated all three inline snapshot occurrences (for ObjectName,
ObjectNameForResponse, ObjectNameForUpdate) to expect the correct object
shape instead of string.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 08:41:59 +00:00
Charles BochetandGitHub 6b3064e2ba fix(server): add relationTargetFieldMetadataId column early in upgrade sequence (#20664)
## Summary

Cross-version upgrade fails at the 2.3
`DropMessageDirectionFieldCommand` stage:

```
[QueryFailedError] column ViewFilterEntity.relationTargetFieldMetadataId does not exist
  at WorkspaceFlatViewFilterMapCacheService.computeForCache
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25929264129/job/76219964380)

Same shape as #20584 (subFieldName), one column over.

### Root cause

1. The 2.3 `DropMessageDirectionFieldCommand` builds a workspace
migration that deletes a `fieldMetadata` (the `direction` field).
2. `WorkspaceMigrationRunnerService.run` walks the metadata cascade
graph and pulls `viewFilter` into the dependency set because
`viewFilter` is the inverse one-to-many of `fieldMetadata`.
3. That maps to cache keys → `flatViewFilterMaps` gets requested →
`WorkspaceFlatViewFilterMapCacheService.computeForCache` runs.
4. `computeForCache` does `viewFilterRepository.find({ where: {
workspaceId }, withDeleted: true })` with no `select`, so TypeORM emits
a SELECT that includes `relationTargetFieldMetadataId` — column only
added by the 2.6 fast instance command `1798000005000`, not yet run at
the 2.3 stage. 💥

### Why v2.5.0 / v2.5.1 passed

They didn't include #20527 (one-hop relation filters, May 14), which
added `relationTargetFieldMetadataId` to `ViewFilterEntity` and the 2.6
instance command. The CI base image (v1.22) seeded the DB, then the
v2.5.0/v2.5.1 container ran upgrade commands against an entity that
didn't yet know about this column.
2026-05-18 10:42:40 +02:00
Charles BochetandGitHub a321e24839 fix(server): scope workspace findOne in incrementMetadataVersion (#20660)
## Summary

Cross-version upgrade fails at the 2.1
`GateExportImportCommandMenuItemsByPermissionFlagCommand` stage:

```
[GateExportImportCommandMenuItemsByPermissionFlagCommand] Found 3 command menu item(s) to update for workspace ...
error: column WorkspaceEntity.isInternalMessagesImportEnabled does not exist
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25929264129/job/76219964380)

### Root cause

Same class of bug as #20581 and #20583, one layer deeper in the call
graph.

1. The 2.1 workspace command emits a `commandMenuItem` migration (3
items differ from the current standard expressions).
2. After the migration commits,
`WorkspaceMigrationRunnerService.invalidateCache` walks the
related-for-validation metadata for `commandMenuItem`, which includes
`objectMetadata`. That puts `flatObjectMetadataMaps` in the keys set.
3. `getLegacyCacheInvalidationPromises` sees `flatObjectMetadataMaps` in
the keys and calls
`WorkspaceMetadataVersionService.incrementMetadataVersion(workspaceId)`.
4. `incrementMetadataVersion` did a bare `findOne` on `WorkspaceEntity`
with no `select` → TypeORM emits a SELECT for every column declared on
the entity → hits `isInternalMessagesImportEnabled` (added by #20457),
whose DB column is only created by the 2.5 fast instance command
`1778525104406-add-is-internal-messages-import-enabled`, which has not
run yet at the 2.1 stage. 💥

### Fix

The function only reads `workspace.metadataVersion`, so narrow the
`select` to `['id', 'metadataVersion']`. No behavior change.

```diff
 async incrementMetadataVersion(workspaceId: string): Promise<void> {
   const workspace = await this.workspaceRepository.findOne({
+    select: ['id', 'metadataVersion'],
     where: { id: workspaceId },
     withDeleted: true,
   });
```
2026-05-18 10:36:33 +02:00
nitinandGitHub 3717df34be fix(twenty-front): anchor body text color to theme var (#20622)
Fixes #20607.
also fixes https://github.com/twentyhq/twenty/issues/20627


Front Components rendered via Remote DOM produced black-on-dark text in
dark mode for any unstyled element. `body` already anchored `background`
to a theme var; the matching `color` rule was missing, so unstyled
subtrees fell through to browser default `#000`.

before - 

<img width="850" height="526" alt="CleanShot 2026-05-16 at 16 31 49@2x"
src="https://github.com/user-attachments/assets/ca21359c-d1d1-4367-831e-f694673757e5"
/>

<img width="840" height="1694" alt="CleanShot 2026-05-16 at 16 32 02@2x"
src="https://github.com/user-attachments/assets/ed0891b5-1b97-4499-bb52-9e31c4cabf11"
/>

after - 

<img width="828" height="514" alt="CleanShot 2026-05-16 at 16 30 50@2x"
src="https://github.com/user-attachments/assets/a22d1f1b-a79b-454c-8c05-5c7c00157b2c"
/>

<img width="852" height="1674" alt="CleanShot 2026-05-16 at 16 31 07@2x"
src="https://github.com/user-attachments/assets/9b1dc19e-ccc5-472b-9184-59fa9b8832f8"
/>
2026-05-18 10:29:22 +02:00
Shubham SinghandGitHub 140dceebd1 fix(front): use theme-aware color for side panel title (#20645)
## Summary
- Added `color: ${themeCssVariables.font.color.primary}` to
`StyledPageInfoTitleContainer` in `SidePanelPageInfoLayout.tsx`
- The "Update records" title had no explicit color, so it didn't adapt
to dark mode and was nearly invisible against the dark background
- Now correctly uses the theme-aware primary font color

## Changes
-
`packages/twenty-front/src/modules/side-panel/components/SidePanelPageInfoLayout.tsx`

Fixes #20627
2026-05-18 10:25:34 +02:00
4d2ceaf70a i18n - translations (#20661)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-18 10:20:30 +02:00
Félix MalfaitandGitHub 6b49a14b9f feat(auth): set 50-character maximum length on passwords (#20655)
## Summary
- Cap password length at 50 characters in the shared regex used by
sign-up, password reset, and password change (both `twenty-front` and
`twenty-server`).
- Update the user-facing validation message on sign-up and password
reset to mention both the 8 min and 50 max bounds.
- Extend the `PASSWORD_REGEX` unit test to cover the new upper bound.

The cap also prevents unbounded inputs from reaching bcrypt, which
silently truncates passwords above 72 bytes and can mask user-visible
bugs.

## Test plan
- [x] `npx jest src/modules/auth/utils/__tests__/passwordRegex.test.ts`
passes (8-char min and 50-char max).
- [ ] Sign up with a 51-character password — form rejects with "Password
must be between 8 and 50 characters".
- [ ] Sign up with an 8–50 character password — succeeds.
- [ ] Password reset rejects a 51-character password with the same
message.
- [ ] Existing users with longer passwords (if any pre-exist) can still
sign in (the regex only gates write paths: sign-up, change, reset).
2026-05-18 10:12:19 +02:00
62b347fc74 chore: sync AI model catalog from models.dev (#20620)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-16 08:45:07 +02:00
Charles BochetandGitHub cf4b4455d3 fix(server): normalize composite defaultValues in manifest converter (unblock app re-install on 2.5-normalized workspaces) (#20615)
## Context

The runtime create-field path and the v2.5
`NormalizeCompositeFieldDefaultsCommand` workspace upgrade both run
composite `defaultValue`s through `nullifyEmptyCompositeDefaultValue`.
The manifest install/sync path was the only write path that skipped it:
[`fromFieldManifestToUniversalFlatFieldMetadata`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util.ts)
passed `fieldManifest.defaultValue` through verbatim.

For the SDK-emitted ACTOR system fields (`createdBy` / `updatedBy`),
`twenty-sdk` ships `{ name: "''", source: "'MANUAL'" }`. After the
runtime or the 2.5 normalize command stores them, the workspace row
holds the canonical four-key form `{ context: null, name: null, source:
"'MANUAL'", workspaceMemberId: null }`. The next install computes its TO
map from the manifest, still gets the raw two-key shape, and diffs it
against the normalized FROM. The dispatcher emits a `defaultValue`
update on each system actor field; the flat-field-metadata validator
rejects it with `FIELD_MUTATION_NOT_ALLOWED`, blocking every re-install
of any application that defines a custom object on a v2.5-normalized
workspace.


## Fix

Normalize composite `defaultValue`s inside the converter, reusing the
same `nullifyEmptyCompositeDefaultValue` helper the three other write
paths already share:

-
[`get-default-flat-field-metadata-from-create-field-input.util.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts)
— `createOneObject` and `createOneField` GraphQL paths.
-
[`sanitize-raw-update-field-input.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts)
— `updateOneField` GraphQL path.
-
[`2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/database/commands/upgrade-version-command/2-5/2-5-workspace-command-1778000001000-normalize-composite-field-defaults.command.ts)
— the upgrade backfill that introduced the divergence.

After the fix, the four write paths agree on the canonical shape, so
re-installs are no-ops on system actor fields regardless of when the 2.5
normalize command ran. Non-composite types pass through unchanged.

## Test

New spec
`from-field-manifest-to-universal-flat-field-metadata.util.spec.ts`
covers:

- Empty-name actor defaults are normalized to the four-key canonical
shape.
- The converter is idempotent: feeding its own output back in produces
the same result (so two consecutive syncs of the same manifest never
emit a `defaultValue` update).
- When the manifest omits `defaultValue`, the converter falls back to
`generateDefaultValue` and normalizes the result.
- Non-composite defaults pass through unchanged.

```
PASS  src/engine/core-modules/application/application-manifest/converters/__tests__/from-field-manifest-to-universal-flat-field-metadata.util.spec.ts
  fromFieldManifestToUniversalFlatFieldMetadata
    composite defaultValue normalization
      ✓ normalizes empty-name actor defaults to the canonical four-key shape
      ✓ is idempotent: re-running the converter on its own output yields the same defaultValue
      ✓ falls back to the generated default and normalizes it when defaultValue is omitted
      ✓ leaves non-composite defaults untouched
Tests: 4 passed
```

## CI gap that let this through

The integration suites covering manifest install (`appDevOnce` against
the test workspace) never re-installed an existing app on a workspace
whose composite fields had already been put through the 2.5 normalize
command. They synced once, then ran assertions on the resulting state;
the second sync that would have re-triggered the `defaultValue` diff was
never exercised.

If we want to catch this class of regression at the integration level
too, we'd add a test that (1) syncs an app whose manifest includes an
ACTOR system field with the raw SDK shape, (2) invokes
`NormalizeCompositeFieldDefaultsCommand` directly on the test workspace,
(3) re-syncs the same manifest, and (4) asserts no
`FIELD_MUTATION_NOT_ALLOWED` errors. The unit-level idempotency check in
this PR is the minimal version of that same coverage. Happy to ship that
integration spec in a follow-up if it'd help.
2026-05-15 18:31:48 +02:00
268fccca29 i18n - translations (#20609)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-15 16:51:02 +02:00
c938fbf4d6 feat(twenty-front): relation traversal in filter dropdown (stacked) (#20533)
**Stacked on #20527** 




https://github.com/user-attachments/assets/48995655-401a-4c35-8094-e88da8408bdd





## Summary

Surfaces the one-hop relation traversal added in #20527 through the
existing **composite sub-field dropdown pattern**. Clicking a
MANY_TO_ONE relation field in the "+ Filter" picker now opens the same
second-level dropdown that composite fields (FULL_NAME, ADDRESS,
CURRENCY, etc.) already use — populated with the target object's
filterable fields. Picking one (e.g. `Company → Name`) builds a filter
that serializes to the nested GraphQL filter the backend now accepts: `{
company: { name: { ilike: "%X%" } } }`.

No new components. The whole feature reuses
`AdvancedFilterSubFieldSelectMenu` + the existing
`subFieldNameUsedInDropdownComponentState` + the existing `MenuItem
hasSubMenu` indicator. Only the conditions that gate the sub-menu (and
the sub-menu's content for relations) were broadened.

## What landed

| File | Change |
|---|---|
| `ObjectFilterDropdownFilterSelectMenuItem` | Sub-menu chevron now
shows on MANY_TO_ONE relations (`isManyToOneRelationField` util). |
| `AdvancedFilterFieldSelectMenu` | Relation clicks open the sub-menu
alongside composite clicks. |
| `AdvancedFilterSubFieldSelectMenu` | New branch: when the sub-menu
type is `'RELATION'`, render the target object's filterable fields via
`useFilterableFieldMetadataItems(targetObjectMetadataId)`. Composite
logic untouched. |
| `objectFilterDropdownSubMenuFieldType` state | Widened to accept a
`'RELATION'` sentinel. Role-permissions sub-field menu narrows it back
out (it doesn't traverse relations). |
| `useSelectFieldUsedInAdvancedFilterDropdown` | New optional
`targetFieldMetadataItem` arg. When present, the stored RecordFilter's
`type` is the target field's type so the operand picker and value input
render the target's operands (`'TEXT'` operators when filtering
`company.name`, etc.). |
| `turnRecordFilterIntoGqlOperationFilter` (shared) | When the filter
targets a `RELATION` field with a `subFieldName`, synthesize a
field-metadata for the target, recurse to build the inner filter, then
wrap it under the relation field's name → `{ relationName: {
targetFieldName: { ...operator } } }`. |

`RecordFilter.subFieldName` stays narrowly typed as
`CompositeFieldSubFieldName` so the wide downstream consumers
(`shouldShowFilterTextInput`, composite handlers in the serializer,
etc.) don't change. The relation target field's name is stored through a
narrowly-scoped cast at the dropdown's storage point — the serializer
checks `filter.type === 'RELATION'` before interpreting it as a target
field name, so the cast can't be mis-read by composite-only code paths.

## Test plan

- [ ] Open a table view on People, click "+ Filter", click "Company" →
sub-menu opens with Company's filterable fields
- [ ] Pick "Name" → operand picker shows TEXT operators (Contains,
Equals, …)
- [ ] Type "Airbnb" → filter applies, table shows people whose company
name contains "Airbnb"
- [ ] Verify network tab: the GraphQL filter variable is `{ company: {
name: { ilike: "%Airbnb%" } } }`
- [ ] Same flow with a composite target field (e.g. `Company →
annualRecurringRevenue → amountMicros`) — should work end-to-end
(backend supports composite-within-relation; #20527 has an integration
test covering this)
- [ ] Composite fields (FULL_NAME, ADDRESS) still open their normal
sub-menu and filter correctly — no regression
- [ ] Role-permissions field-select sub-field menu is unaffected (it
bails out early on the RELATION sentinel)

## Out of scope

- ONE_TO_MANY traversal (no backend support yet)
- Aggregates (`people.count > 5`)
- Persisting relation-traversal filters into a saved view (ViewFilter
has no `relationPath` column yet; that's a separate slice)
- REST API DSL changes
- AI Tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 16:42:47 +02:00
Charles BochetandGitHub eca92ca559 fix(server): rebuild unique phone indexes drops legacy non-empty partial WHERE clause (#20606)
## Summary

`RebuildUniquePhoneIndexesCommand` reuses each index's existing
`indexWhereClause` when recreating the physical index. For workspaces
whose unique phone indexes have a legacy clause like
`"primaryPhoneNumber" != ''` (created before PR #18024 hardened the
validator allowlist), the recreate path fails at
`validateAndReturnIndexWhereClause` because the clause isn't in
`ALLOWED_INDEX_WHERE_CLAUSES`.

Two workspaces are hitting this on the 2.5 upgrade:
- `3a797122-…` — `"companyPhonePrimaryPhoneNumber" != ''`
- `ea74716f-…` — `"phonesPrimaryPhoneNumber" != ''`

## Fix

Detect the legacy `"<col>" != ''` shape via a strict regex. When it's
there, before the existing drop+create, do three things inside the
workspace transaction:

1. **Normalize the data** that the legacy partial clause was masking —
`UPDATE "<schema>"."<table>" SET "<col>" = NULL WHERE "<col>" = ''` for
every column the index covers. Without this the next step would fail
because the new plain-unique index would see duplicate `''` values
across the rows the old partial clause was excluding.
2. **Null out `core."indexMetadata".indexWhereClause`** so the metadata
row matches what the UI would have created (`indexWhereClause: null`)
and doesn't carry the validator-rejected clause forward to any future
re-emit. Uses the same workspace `queryRunner` (Postgres lets one
connection write across schemas).
3. **Recreate** with an overridden flat index where `indexWhereClause:
null`. `createIndexInWorkspaceSchema` → `indexManager.createIndex` →
`validateAndReturnIndexWhereClause` short-circuits on null, no allowlist
check.

End state matches the shape a fresh "toggle unique in Settings UI"
creates: plain unique index, no `WHERE`, NULL semantics doing the
"exclude empty phones" work via PG's default NULL-distinct behaviour.

For indexes whose clause is already allowlisted (`"deletedAt" IS NULL`)
or null, behaviour is unchanged — just the column-list widening this
command already does.
2026-05-15 16:39:34 +02:00
0c20b8bc88 i18n - translations (#20605)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-15 13:08:34 +02:00
Charles BochetandGitHub 75b9b2fe5d feat(admin-panel): signing keys management tab with usage tracking (#20586)
## Summary
- Adds a new admin-only **Security** tab to the Admin Panel (alongside
General/Apps/AI/Config/Health) containing a **Signing Keys** section.
The tab is intentionally introduced now so the upcoming **Encryption
rotation** work can land as a sibling section.
- Lists every JWT signing key with key id, `createdAt`, `revokedAt`,
current/active/revoked status, and a **7-day verification count** read
from Redis. A trailing row aggregates **legacy HS256** verifications so
it is clear when the deprecated path is still in use.
- Lets an admin **revoke** a public key. Revoking the current key drops
`isCurrent`, sets `revokedAt`, nulls the encrypted `privateKey` and
clears the in-process cached current key; the existing lazy path in
`JwtKeyManagerService.getCurrentSigningKey()` then mints a fresh current
key on the next sign.

## Backend
- `SigningKeyVerifyCounterService` — bucketed Redis counter under the
existing `EngineMetrics` namespace. 1-day UTC-aligned buckets, 8-day TTL
refreshed on every increment, batched read via `mget`. Failures are
swallowed and logged at `warn` so a Redis hiccup cannot break auth.
- `JwtWrapperService.verifyJwtToken` records verifies **after success**
for both ES256 (`kid` as identifier) and HS256 (the literal `legacy`
identifier).
- `JwtKeyManagerService.listSigningKeys()` and `revokeSigningKey(id)`:
list ordered by `isCurrent DESC, createdAt DESC`; revoke is idempotent,
validates the UUID, invalidates the public-key cache, and resets the
cached current-key promise.
- `AdminPanelResolver.getSigningKeys` (query) and `revokeSigningKey`
(mutation) are both decorated with `@UseGuards(AdminPanelGuard)` so they
are admin-only, like the 35 existing admin-only methods on this
resolver. `privateKey` is never returned over GraphQL.

## Frontend
- New `SECURITY` tab id wired into `SettingsAdminContent` and
`SettingsAdminTabContent` (gated by `canAccessFullAdminPanel`).
- `SettingsAdminSecurity` / `SettingsAdminSigningKeysTable` strictly
reuse existing admin-panel components: `Section`, `H2Title`,
`Table`/`TableRow`/`TableCell`/`TableHeader` from `@/ui/layout/table`,
`Tag`/`Button` from `twenty-ui`, and `ConfirmationModal` mirroring the
queue retry/delete modals. Only one minimal styled helper for the
monospaced UUID rendering.
- `useRevokeSigningKey` uses `useApolloAdminClient`, refetches
`GetSigningKeys`, shows success/error snackbars (same pattern as
`useRetryJobs`/`useDeleteJobs`).

<img width="1293" height="881" alt="image"
src="https://github.com/user-attachments/assets/7cf98664-950b-4451-af85-27781a8e9a9c"
/>
2026-05-15 10:49:18 +00:00
AriqhermawanandGitHub 218799636f fix(docs): replace removed Mintlify build command (#20578)
## Summary
Closes #20565.

The Twenty docs package still pointed contributors at the removed
`mintlify build` command. This switches the docs workflow to a
`validate` command, which matches the supported Mintlify CLI command for
validating the documentation build, and updates the README wording to
match.

## Changes
- Replaced the `twenty-docs` package `build` script with a `validate`
script.
- Renamed the Nx docs target from `build` to `validate` and kept it
wired to `mintlify validate`.
- Updated the README validation command to `npx nx run
twenty-docs:validate`.

## Verification
```bash
$ npx -y mintlify validate --help
usage: mintlify validate [options]

Options:
  -t, --telemetry        Enable or disable anonymous usage telemetry   [boolean]
      --groups           Mock user groups for validation                 [array]
      --disable-openapi  Disable OpenAPI file generation
                                                      [boolean] [default: false]
  -h, --help             Show help                                     [boolean]
  -v, --version          Show version number                           [boolean]

Examples:
  mintlify validate  validate the build
```

```bash
$ npx -y mintlify build
Unknown command: build
```

I also started `npx -y mintlify validate --disable-openapi`; the CLI
recognized the command and began validating, but this Windows
environment could not finish Mintlify framework extraction because it
hit an EPERM symlink error inside the local `.mintlify` cache.
2026-05-15 09:40:11 +00:00
Charles BochetandGitHub 14acd77626 fix(docker): pin node:24-alpine to 24.15.0-alpine3.23 digest (#20603)
## Summary

- ECR Inspector flagged 9 CVEs on the `prod-twenty` image — 8 PostgreSQL
CVEs on `postgresql18-18.3-r0` (pulled in transitively by `apk add
postgresql-client`) and CVE-2026-27135 on `nghttp2-1.68.0-r0` (pulled in
by `curl` / `aws-cli`).
- Alpine 3.23 already ships patched `postgresql18-18.4-r0` and
`nghttp2-1.69.0-r0`, but the GHA buildx cache was reusing the stale `apk
add` layer because `FROM node:24-alpine` had not moved.
- Pinning the base image to `node:24.15.0-alpine3.23@sha256:8e2c930f…`
forces a layer cache miss, picks up the patched apk packages, and gives
Dependabot/Renovate a stable target for future digest bumps.

Applied to both
[packages/twenty-docker/twenty/Dockerfile](https://github.com/twentyhq/twenty/blob/charles/trusting-solomon-259ec8/packages/twenty-docker/twenty/Dockerfile)
(4 stages → ECR `prod-twenty`) and
[packages/twenty-docker/twenty-website-new/Dockerfile](https://github.com/twentyhq/twenty/blob/charles/trusting-solomon-259ec8/packages/twenty-docker/twenty-website-new/Dockerfile)
(2 stages).

## Test plan

- [ ] CI builds both images successfully on amd64 + arm64
- [ ] After merge + deploy, re-run ECR Inspector on the new
`prod-twenty` image and confirm the 9 CVEs
(CVE-2026-6473/6474/6475/6476/6477/6478/6479/6637 + CVE-2026-27135) are
gone
- [ ] Smoke-test the staging deployment (server boot, DB migrations via
`psql` in the entrypoint)
2026-05-15 09:27:23 +00:00
Abdullah.andGitHub d94d2eb67c [Website] Make product stepper visuals interactive. (#20602)
We had low-res screenshots for each step in the stepper. Replaced them
with interactive components.


https://github.com/user-attachments/assets/d03ff924-a1dd-467f-ba19-cece0ecb3486
2026-05-15 08:58:22 +00:00
Charles BochetandGitHub 45bea6f991 feat(secret-encryption): drop APP_SECRET from approved-access-domain validation and session cookies (#20580)
## Summary

Continues retiring `APP_SECRET` as a hot signing secret (after the TOTP
migration in #20577). This PR moves the last two cryptographic uses of
`APP_SECRET` off it:

1. **Approved-access-domain validation tokens** — was a one-shot
`sha256(JSON.stringify({id, domain, key: APP_SECRET}))` HMAC with no
built-in expiry. Now a JWT signed by the workspace `signingKey` with a
7-day expiry and claims bound to `approvedAccessDomainId`,
`workspaceId`, and `domain`.
2. **Express-session cookie signing** — was `sha256(APP_SECRET ||
'SESSION_STORE_SECRET')`. Now `HKDF(ENCRYPTION_KEY,
info='twenty:hmac:v1:session-cookie')` with `FALLBACK_ENCRYPTION_KEY`
supported for rotation.

### Approved-access-domain — strict cutover

- `ApprovedAccessDomainService.mintValidationToken` issues a JWT via
`JwtWrapperService.signAsyncOrThrow` (workspace `signingKey`, asymmetric
ES256 with kid-based rotation built in).
- `validateApprovedAccessDomain` verifies the JWT, asserts `type ===
APPROVED_ACCESS_DOMAIN`, cross-checks `claim.approvedAccessDomainId`
against the URL's `approvedAccessDomainId`, then re-checks `domain` and
`workspaceId` against the stored row. Any failure maps to
`APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID`.
- **No legacy fallback:** any pending invitation link minted with the
old SHA hash will fail validation and must be re-sent. Volume is small
and admins can re-issue from settings — this is the cleanest cutover.

### Session cookies — bridged cutover

- `resolveSessionCookieSecretsOrThrow` returns an array
`[HKDF(ENCRYPTION_KEY), HKDF(FALLBACK_ENCRYPTION_KEY)?,
sha256(APP_SECRET || 'SESSION_STORE_SECRET')?]`.
- `express-session` signs new cookies with the first secret and verifies
against any entry, so in-flight cookies signed under the legacy SHA keep
verifying until `maxAge` (30 min) expires.
- New `deriveInstanceHmacKey` HKDF utility uses a dedicated
`twenty:hmac:v1:` info prefix — distinct from the AEAD subkey prefix
`twenty:enc:v2:` — so HMAC and encryption subkeys can never collide for
the same raw `ENCRYPTION_KEY`.
- TODO comment marks the legacy slot for removal post-2.5.

### Notes on rotation behaviour

- Rotating `ENCRYPTION_KEY` while keeping the old value in
`FALLBACK_ENCRYPTION_KEY` keeps cookies signed under either key
verifying. New cookies sign under the new key. After all in-flight
cookies expire (≤30 min), the fallback slot can be dropped from env.
- Rotating the workspace `signingKey` (already supported by
`JwtKeyManagerService`) keeps already-issued approved-access-domain JWTs
verifying via `kid` until their 7-day expiry.

## Test plan

- [x] Unit tests for `ApprovedAccessDomainService` cover: happy path,
JWT verify failure, wrong token type, JWT id ≠ input id, JWT-claimed
domain ≠ row, missing row, already-validated row.
- [x] Unit tests for `resolveSessionCookieSecretsOrThrow` cover: throws
without keys, primary order (`ENCRYPTION_KEY` → APP_SECRET fallback),
`FALLBACK_ENCRYPTION_KEY` placement, empty-string vars treated as unset,
legacy slot omitted when `APP_SECRET` missing, HKDF domain separation
across purposes.
- [x] `nx lint:diff-with-main twenty-server` — clean.
- [x] Full test surface across approved-access-domain,
secret-encryption, session-storage — 78/78 pass.
- [ ] CI green.
- [ ] Manual smoke: boot with a dummy `ENCRYPTION_KEY`, confirm sign-in
succeeds (session cookie works), create + validate an
approved-access-domain end-to-end through the UI.
2026-05-15 08:47:46 +00:00
dd9027680e chore: sync AI model catalog from models.dev (#20601)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-15 09:00:29 +02:00
Charles BochetandGitHub ca1571676c fix(server): treat plaintext-under-isSecret rows as plaintext in app variable encryption migration (#20590)
## Summary

Prod 2.5 upgrade failed on the slow instance command
`EncryptApplicationVariableSlowInstanceCommand`:

```
[Nest] LOG  [InstanceCommandRunnerService] 2.5.0_EncryptApplicationVariableSlowInstanceCommand_1798000005000 starting data migration...
[Nest] WARN [SecretEncryptionService] Decrypted a legacy unprefixed AES-CTR ciphertext...
[Nest] ERROR [InstanceCommandRunnerService] data migration failed
TypeError: Invalid initialization vector
```

### Root cause

The migration assumes every row matching `isSecret = true AND value <>
'' AND value NOT LIKE 'enc:v2:%'` is legacy AES-CTR ciphertext. In prod
we found multiple `isSecret = true` rows whose `value` is plaintext
(e.g. `SLACK_HOOK_URL = 'https://hooks.slack.com/services/...'`) — most
likely the result of `isSecret` being flipped to true on a row that
already held a plaintext value, or a write path that bypassed
`ApplicationVariableEntityService.update`. Those values can't decode
into the 16-byte IV that AES-CTR needs, so `Buffer.from(value,
'base64')` truncates at the first non-base64 char (`:`), the buffer is <
16 bytes, and `createDecipheriv` throws.

### Fix

Follow the same policy as
`EncryptConnectedAccountTokensSlowInstanceCommand`: anything that isn't
already in the `enc:v2:` envelope is plaintext. Concretely:

1. Try `decryptVersioned` — legacy CTR rows decrypt fine.
2. If it throws (mis-classified plaintext), log a warning naming the row
id and fall back to treating `row.value` as plaintext.
3. Encrypt the resulting plaintext into the `enc:v2:` envelope and
update the row.

In-loop `isSecret` guard is kept (alongside the SQL filter) so
non-secret rows are never touched even if the SQL filter is ever
loosened.

### Integration test coverage

Added one new case alongside the existing ones in
`…encrypt-application-variable.integration-spec.ts`:

- `treats plaintext-under-isSecret=true as plaintext and re-encrypts as
v2` — seeds a row with `isSecret = true` and a URL value (`:` and `/`
are not base64, so this is the exact failure shape from prod), runs the
migration, and asserts the value is now `enc:v2:...` and decrypts back
to the original URL.

Existing cases unchanged: legacy CTR happy path, non-secret rows
untouched, idempotent across re-runs, `up()` adds the CHECK constraint,
`down()` removes it.

### Why this is a 2-5 edit

`TWENTY_CURRENT_VERSION` is now 2.6.0, so editing a 2-5 file trips the
`server-previous-version-upgrade-mutation-guard` —
`ci:allow-previous-version-upgrade-mutation` label is on the PR. `up()`
and `down()` are unchanged; only `runDataMigration` is modified.

## Test plan

- [ ] Re-deploy 2.5 to prod and confirm
`EncryptApplicationVariableSlowInstanceCommand` completes
- [ ] Inspect warning log to count rows that went through the plaintext
fallback
- [ ] Verify resulting secret rows all satisfy `value = '' OR value LIKE
'enc:v2:%'` and the CHECK constraint is in place
2026-05-14 18:40:41 +02:00
Charles BochetandGitHub a5880bd8d0 fix(server): drop correlated subquery in getWorkspaceLastAttemptedCommandName (#20591)
## Summary
- The upgrade runner calls `getWorkspaceLastAttemptedCommandName` twice
per workspace step. Grafana showed it averaging ~4.4s and trending
upward as the `core.upgradeMigration` table grows during an in-flight
upgrade.
- The old query joined every outer row against a correlated subquery
(`attempt = (SELECT MAX(sub.attempt) ... WHERE sub.name = m.name AND
sub."workspaceId" = m."workspaceId")`). Even with the `(workspaceId,
name, attempt)` index added in 2.3, each outer row triggers an index
lookup — fine for a few rows, painful at production scale.
- Replaced with a two-level `DISTINCT ON`:
- Inner `DISTINCT ON ("workspaceId", name) ORDER BY "workspaceId", name,
attempt DESC` walks `IDX_UPGRADE_MIGRATION_WORKSPACE_ID_NAME_ATTEMPT`
directly and yields one row per `(workspaceId, name)` at max attempt.
- Outer `DISTINCT ON ("workspaceId") ORDER BY "workspaceId", "createdAt"
DESC` picks the most recent row per workspace.
- Semantically identical; planner now does a single index walk + one
sort instead of N correlated lookups.

The same correlated-subquery shape exists in
`getLastAttemptedCommandNameOrThrow`, `areAllWorkspacesAtCommand`, and
`getLastAttemptedInstanceCommand`. They run far less often during an
upgrade (per instance step, not per workspace step), so they're out of
scope for this hotfix — happy to follow up if we want them too.

## Benchmark (prod)

Run over all distinct workspaceIds in `core."upgradeMigration"`:

| Variant | Execution Time |
| --- | --- |
| Before (correlated subquery) | **2979.659 ms** |
| After (two-level DISTINCT ON) | **1225.690 ms** |

~2.4× faster, and the gap widens as the table grows over the course of
an upgrade.

Equivalence confirmed: the diff query below returned `0` divergent
workspaces on prod.

### Variant A — original (correlated subquery)

```sql
SELECT DISTINCT ON (m."workspaceId")
  m."workspaceId", m.name, m.status, m."executedByVersion",
  m."errorMessage", m."createdAt", m."isInitial"
FROM core."upgradeMigration" m
WHERE m."workspaceId" IN ($1, $2, ...)
  AND m.attempt = (
    SELECT MAX(sub.attempt)
    FROM core."upgradeMigration" sub
    WHERE sub.name = m.name
      AND sub."workspaceId" = m."workspaceId"
  )
ORDER BY m."workspaceId", m."createdAt" DESC;
```

### Variant B — new (two-level DISTINCT ON)

```sql
SELECT DISTINCT ON (latest_per_name."workspaceId")
  latest_per_name."workspaceId",
  latest_per_name.name,
  latest_per_name.status,
  latest_per_name."executedByVersion",
  latest_per_name."errorMessage",
  latest_per_name."createdAt",
  latest_per_name."isInitial"
FROM (
  SELECT DISTINCT ON ("workspaceId", name)
    "workspaceId", name, status, "executedByVersion",
    "errorMessage", "createdAt", "isInitial"
  FROM core."upgradeMigration"
  WHERE "workspaceId" = ANY($1)
  ORDER BY "workspaceId", name, attempt DESC
) latest_per_name
ORDER BY latest_per_name."workspaceId", latest_per_name."createdAt" DESC;
```

### Equivalence check (returned 0 on prod)

```sql
WITH target_ids AS (
  SELECT DISTINCT "workspaceId"
  FROM core."upgradeMigration"
  WHERE "workspaceId" IS NOT NULL
),
old_result AS (
  SELECT DISTINCT ON (m."workspaceId")
    m."workspaceId", m.name, m.status, m."executedByVersion",
    m."errorMessage", m."createdAt", m."isInitial"
  FROM core."upgradeMigration" m
  WHERE m."workspaceId" IN (SELECT "workspaceId" FROM target_ids)
    AND m.attempt = (
      SELECT MAX(sub.attempt)
      FROM core."upgradeMigration" sub
      WHERE sub.name = m.name
        AND sub."workspaceId" = m."workspaceId"
    )
  ORDER BY m."workspaceId", m."createdAt" DESC
),
new_result AS (
  SELECT DISTINCT ON (latest_per_name."workspaceId")
    latest_per_name."workspaceId", latest_per_name.name, latest_per_name.status,
    latest_per_name."executedByVersion", latest_per_name."errorMessage",
    latest_per_name."createdAt", latest_per_name."isInitial"
  FROM (
    SELECT DISTINCT ON ("workspaceId", name)
      "workspaceId", name, status, "executedByVersion",
      "errorMessage", "createdAt", "isInitial"
    FROM core."upgradeMigration"
    WHERE "workspaceId" IN (SELECT "workspaceId" FROM target_ids)
    ORDER BY "workspaceId", name, attempt DESC
  ) latest_per_name
  ORDER BY latest_per_name."workspaceId", latest_per_name."createdAt" DESC
),
diffs AS (
  SELECT 'only_in_old' AS bucket, o."workspaceId", o.name, o.status, o."createdAt"
  FROM old_result o
  LEFT JOIN new_result n ON n."workspaceId" = o."workspaceId"
  WHERE n."workspaceId" IS NULL OR n.name <> o.name OR n.status <> o.status
  UNION ALL
  SELECT 'only_in_new', n."workspaceId", n.name, n.status, n."createdAt"
  FROM new_result n
  LEFT JOIN old_result o ON o."workspaceId" = n."workspaceId"
  WHERE o."workspaceId" IS NULL OR o.name <> n.name OR o.status <> n.status
)
SELECT COUNT(*) AS divergent_workspaces FROM diffs;
```

## Test plan
- [ ] `npx nx test twenty-server --testPathPattern upgrade-migration`
- [ ] Integration tests: `npx nx run
twenty-server:test:integration:with-db-reset --testPathPattern
sequence-runner`
- [ ] Verify on staging that the slow query disappears from the
PostgreSQL Grafana board during the next upgrade run
2026-05-14 18:39:34 +02:00
Charles BochetandGitHub 78b3092886 fix(server): batch upgrade migration inserts to stay under PG param limit (#20588)
## Summary

Prod deploy of v2.5.0 fails with a query failure inserting into
`core.upgradeMigration`:

```
query failed: INSERT INTO "core"."upgradeMigration" ("id", "name", "status", "attempt", "executedByVersion", "errorMessage", "isInitial", "workspaceId", "createdAt")
VALUES (DEFAULT, $1, $2, $3, $4, $5, DEFAULT, $6, DEFAULT),
       (DEFAULT, $7, $8, $9, $10, $11, DEFAULT, $12, DEFAULT),
       ... (continues past $2515) ...
```

### Root cause

`UpgradeMigrationService.recordUpgradeMigration` writes one row per
workspace via a single `repository.save([...rows])` call.
`UpgradeMigrationEntity` has **6 user-provided columns** per row
(`name`, `status`, `attempt`, `executedByVersion`, `errorMessage`,
`workspaceId`), so the multi-row INSERT binds `6 * (1 + N_workspaces)`
parameters.

Postgres' wire protocol caps a single statement at **65,535 bind
parameters** (16-bit count). That gives a hard ceiling of ~10,920 rows
per call. Production has enough workspaces to overflow.
2026-05-14 18:30:38 +02:00
Charles BochetandGitHub 5a1d3841f4 Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.5.0 (#20587)
## Summary

- Bumps `twenty-sdk` from `2.4.2` to `2.5.0`.
- Bumps `twenty-client-sdk` from `2.4.2` to `2.5.0`.
- Bumps `create-twenty-app` from `2.4.2` to `2.5.0`.
2026-05-14 17:14:31 +02:00
94748b7042 chore: bump version to 2.6.0 (#20585)
## 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

## Checklist

- [ ] Verify version constants are correct

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
2026-05-14 15:02:38 +00:00
Félix MalfaitandGitHub 663ef332ad feat(auth): resume workspace selection on /welcome with valid tokenPair cookie (#20575)
## Summary

After a user completes a multi-workspace social-SSO sign-in,
[auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011)
issues a **workspace-agnostic** access + refresh token pair and lands
them on `app.twenty.com/welcome?tokenPair=…`.
[SignInUpGlobalScopeFormEffect.tsx](packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx)
reads the URL param, writes the cookie, pushes them to
`SignInUpStep.WorkspaceSelection`.

The problem: if the user revisits `app.twenty.com/welcome` later (e.g.
ChatGPT pings `/authorize` and the global page-change effect redirects
them to `/welcome` with `returnToPath=/authorize?…`), the existing
branch is a no-op — the URL param is gone. The user sees the regular
email/SSO form and has to re-authenticate, even though the
workspace-agnostic cookie is still valid.

This PR adds a second branch in the same `useEffect` that handles the
"valid cookie, no URL param" case:

```ts
if (signInUpStep !== SignInUpStep.Init) return;
if (!hasAccessTokenPair) return;
loadCurrentUser();
setSignInUpStep(SignInUpStep.WorkspaceSelection);
```

Single `useEffect`, no `useRef`, no async then/catch. The synchronous
`setSignInUpStep(WorkspaceSelection)` is the gate — once the step
transitions, subsequent effect runs early-return. Mirrors the existing
URL-param branch's pattern exactly.

If the cookie is stale, `loadCurrentUser` triggers Apollo's renewal
middleware. Renewal of a workspace-agnostic refresh token is supported
end-to-end (verified in audit, see below) — if it succeeds the user sees
their workspaces; if both tokens are expired, `onUnauthenticatedError`
clears the cookie and the next render lands them on the regular sign-in
form. Same fallback as if the cookie had never been there.

## Behavior matrix

| State on /welcome mount | Before | After |
|---|---|---|
| No tokenPair anywhere | Show sign-in form | Show sign-in form |
| tokenPair in URL (just bounced from SSO) | Set tokens →
WorkspaceSelection | (unchanged) Set tokens → WorkspaceSelection |
| tokenPair in cookie, access valid | Show sign-in form  | **→
WorkspaceSelection ✓** |
| tokenPair in cookie, access expired, refresh valid | Show sign-in form
(Apollo eventually 401s on a query) | Renewal succeeds silently →
WorkspaceSelection ✓ |
| tokenPair in cookie, both expired | Show sign-in form |
`onUnauthenticatedError` clears cookie → fall back to sign-in form |

## Workspace-agnostic renewal: confirmed working end-to-end

Audit summary:
- **Refresh token carries the type**:
[refresh-token.service.ts:104](packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.ts)
preserves `targetedTokenType` in the JWT payload and returns it from
`verifyRefreshToken`.
- **Renewal branches on type**
([renew-token.service.ts:70-87](packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts)):
  ```ts
  const accessToken =
    isDefined(authProvider) &&
    targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC &&
    !isDefined(workspaceId)
? await
this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken({...})
      : await this.accessTokenService.generateAccessToken({...});
  ```
  Renewed refresh token preserves `targetedTokenType` (line 93).
- **Resolver is workspace-agnostic**: `@UseGuards(PublicEndpointGuard,
NoPermissionGuard)` on `renewToken`
([auth.resolver.ts:796-804](packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts))
— no `@AuthWorkspace()` requirement, callable from `app.twenty.com`.
- **Frontend middleware is type-agnostic**:
[apollo.factory.ts:180-209](packages/twenty-front/src/modules/apollo/services/apollo.factory.ts)
just passes the refresh token blob.

Net: no backend change needed. The full workspace-agnostic lifecycle
(issue → cookie → renew → re-issue) already works.

## Test plan

- [x] `npx oxlint` + `prettier --check` — clean.
- [x] `npx nx typecheck twenty-front` — clean.
- [ ] Manual: complete one full SSO flow ending on a workspace
subdomain. Visit `https://app.twenty.com/welcome` directly — expect the
workspace picker, not the sign-in form.
- [ ] Manual: same but with tokenPair cookie cleared — expect the
regular sign-in form (no regression).
- [ ] Manual: sign-out from a workspace, then visit
`app.twenty.com/welcome` — expect the regular form (sign-out clears the
cookie via full page reload).
- [ ] Manual: stale/expired tokenPair cookie — Apollo renewal kicks in
transparently; if renewal fails, regular form (no infinite loop, no
crash).
- [ ] Manual: pair with #20572 — visit `app.twenty.com/authorize?…` with
a stale workspace-agnostic cookie. Expected chain: `/authorize` renders
→ `PageChangeEffect` redirects to `/welcome?returnToPath=/authorize?…` →
this effect lands the user on WorkspaceSelection → picking a workspace
bounces to `<workspace>/authorize?…` where consent renders.

## Out of scope

- Fixing `lastAuthenticatedWorkspaceDomain` for custom-domain users
(separate cookie-scoping issue, tracked separately).
2026-05-14 15:02:07 +00:00
Charles BochetandGitHub 09daccc3f9 fix(server): add subFieldName column early in upgrade sequence (#20584)
## Summary

Cross-version upgrades from pre-2.3 still fail after #20581 / #20583 —
different column, structurally similar problem:

```
column ViewSortEntity.subFieldName does not exist
  at WorkspaceFlatViewSortMapCacheService.computeForCache (...flat-view-sort/services/workspace-flat-view-sort-map-cache.service.js:40)
  ... triggered indirectly by DropMessageDirectionFieldCommand (2.3 workspace command)
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25862573418/job/75997337604)

### Why narrowing the `select` doesn't fit here

In the previous two PRs the offender was a bare `findOne` on
`WorkspaceEntity` — easy to narrow. Here the chain is:

1. The 2.3 `DropMessageDirectionFieldCommand` builds a workspace
migration that deletes a `fieldMetadata` (the `direction` field).
2. `WorkspaceMigrationRunnerService.run` walks the metadata cascade
graph (`getMetadataRelatedMetadataNames`) and pulls `viewSort` into the
dependency set because `viewSort` is the inverse one-to-many of
`fieldMetadata` (deleting a field cascades to view sorts that reference
it).
3. That maps to cache keys → `flatViewSortMaps` gets requested →
`WorkspaceFlatViewSortMapCacheService.computeForCache` runs.
4. `computeForCache` does `viewSortRepository.find({ where: {
workspaceId }, withDeleted: true })` with no `select`, so TypeORM emits
a SELECT that includes `subFieldName` — the column doesn't exist in DB
yet (added by a 2.5 instance command much later in the sequence). 💥

Narrowing the cache provider's select would silently drop `subFieldName`
from the cache for runtime use too, until something invalidates it.
Brittle, and would re-break the next time anyone adds a `viewSort`
column.

### Structural fix

Ensure the column exists in DB before any 2.3 workspace command can
trigger that cascade. Within a version, the upgrade runner sorts: fast
instance → slow instance → workspace, so a new 2.3 fast instance command
lands before `DropMessageDirectionFieldCommand`.

- **Add**
`2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort.ts`
— `ALTER TABLE ... ADD COLUMN IF NOT EXISTS "subFieldName"`. Comment in
the file explains the cascade and why this lives in 2.3 instead of 2.5.
- **Make idempotent** the existing
`2-5/...-add-sub-field-name-to-view-sort.ts` — switched to `ADD COLUMN
IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so it's a no-op on
cross-upgrade paths while still creating the column on fresh-from-2.5
installs.
- Register the new command in `instance-commands.constant.ts`.

The 2.5 command body change is semantically preserving (idempotent), and
v2.5.0 hasn't shipped to any production DB yet — so this doesn't violate
the "never rewrite committed instance commands" rule in spirit.

### Note on the previous two PRs

#20581 and #20583 narrowed `select` on `WorkspaceEntity` for
`isInternalMessagesImportEnabled`. That's a band-aid that works because
there's a small, enumerable set of bare `workspaceRepository.findOne`
call sites. It could in principle be replaced with the same pattern as
this PR (early 2.x instance command that adds the workspace column). Not
doing that here to keep the diff tight, but happy to follow up if
preferred.

## Test plan

- [ ] Re-run twenty-infra cross-version-upgrade CI and confirm 2.3
workspace commands complete
- [ ] Verify the new 2.3 instance command and the modified 2.5 instance
command are both idempotent (running upgrade twice should not error)
- [ ] Verify a fresh install path still ends with `subFieldName` present
on `core.viewSort`
2026-05-14 16:02:53 +02:00
Charles BochetandGitHub 484037c179 fix(server): scope workspace findOne in ApplicationService (#20583)
## Summary

Cross-version upgrade still fails after #20581:

```
column WorkspaceEntity.isInternalMessagesImportEnabled does not exist
  at ApplicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow (application.service.ts:84)
  at UpdateGlobalObjectContextCommandMenuItemsCommand.runOnWorkspace (1-23-…)
  at BackfillRecordPageLayoutsCommand.runOnWorkspace (1-23-…)
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25861366732/job/75993012161)

### Root cause

Same class of bug as #20581, different location.
`ApplicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow`
does:

```ts
await this.workspaceRepository.findOne({
  where: { id: workspaceId },
  withDeleted: true,
});
```

No `select`, so TypeORM emits a SELECT for every column declared on
`WorkspaceEntity`. PR #20457 added `isInternalMessagesImportEnabled` to
the entity; its DB column is only created by the 2-5 fast instance
command `1778525104406-add-is-internal-messages-import-enabled`. Many
workspace commands across versions 1-21 → 2-3 call this service (notably
the 1-23 commands shown in the stack), and they all run before the 2-5
instance command — so the bare findOne hits a column that doesn't exist
yet and the upgrade aborts.

### Fix

The function only reads `workspace.id` (passed to cache) and
`workspace.workspaceCustomApplicationId`. Narrow the select to just
those.

The `workspace: WorkspaceEntity` input variant of the function is
unchanged — only the path where we fetch the workspace ourselves is
narrowed. Callers don't see the workspace entity (the function only
returns `{ twentyStandardFlatApplication, workspaceCustomFlatApplication
}`).

### Why not edit the committed 1-23 workspace commands

Same reasoning as #20581: the fix lives in the service that does the
read, so future column additions to `WorkspaceEntity` don't risk
re-breaking every caller. Per `CLAUDE.md`, instance command `up`/`down`
is immutable; this isn't an instance command.

## Test plan

- [ ] Re-run the failing cross-version-upgrade job and confirm it gets
past 1-23
- [ ] Verify the function still resolves the standard + custom
applications correctly for a workspace (no behavior change in returned
shape)
2026-05-14 15:24:59 +02:00
Charles BochetandGitHub a5982b644c fix(server): scope workspace findOne in 1-21 backfill-datasource command (#20581)
## Summary

Cross-version upgrades from pre-1-21 instances currently fail with:

```
error: column WorkspaceEntity.isInternalMessagesImportEnabled does not exist
```

(see
https://github.com/twentyhq/twenty-infra/actions/runs/25857499266/job/75979993686)

### Root cause

The 1-21 workspace command `backfill-datasource-to-workspace` does:

```ts
const workspace = await this.workspaceRepository.findOne({
  where: { id: workspaceId },
});
```

No `select`, so TypeORM emits a SELECT for every column declared on
`WorkspaceEntity`. PR #20457 added `isInternalMessagesImportEnabled` to
the entity, but its DB column is only created by the 2-5 fast instance
command `1778525104406-add-is-internal-messages-import-enabled`. On a
fresh cross-version upgrade, the runner reaches the 1-21 workspace
segment before that 2-5 instance command runs, the bare `findOne` issues
SELECT on a column that doesn't exist yet, and the upgrade aborts.

### Fix

Narrow the select to just the columns this command actually reads (`id`,
`databaseSchema`). The query now ignores entity columns added later in
the upgrade sequence.

### Why edit a committed workspace command

Per `CLAUDE.md`, committed *instance* command `up`/`down` logic is
immutable. Workspace commands are idempotent backfills — adding a
`select` narrows the read but doesn't change behavior, so it's safe.

### Audit

Verified this is the only unguarded `workspaceRepository.find*` across
the entire upgrade subtree:
- `WorkspaceIteratorService.iterate` uses `select: ['databaseSchema']`
- `WorkspaceVersionService.getActiveOrSuspendedWorkspaceIds` uses
`select: ['id']`
- `UpgradeStatusService.loadActiveOrSuspendedWorkspaces` uses `select:
['id', 'displayName']`

## Test plan

- [ ] Re-run the failing cross-version upgrade job and confirm it gets
past 1-21
- [ ] Verify the 1-21 backfill still correctly skips workspaces with a
non-empty `databaseSchema` and backfills those without
2026-05-14 14:57:26 +02:00
4054ede5bb i18n - translations (#20582)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-14 14:52:45 +02:00
af4765effe feat(twenty-server): one-hop relation filters in GraphQL API (#20527)
## Summary

Adds support for filtering records by fields on a related MANY_TO_ONE
object via the GraphQL API. Backend only — no frontend, no REST, no
view-filter persistence yet.

```graphql
{
  people(filter: { company: { name: { like: "%Airbnb%" } } }) {
    edges { node { id } }
  }
}
```

### Where the work lands

- **Schema** — `relation-field-metadata-gql-type.generator.ts` now emits
`{relationName}: TargetFilterInput` alongside the existing
`{joinColumnName}: UUIDFilter` for MANY_TO_ONE relations. Mirrors the
order-by generator that already does this for sort. Lazy thunks in
`object-metadata-filter-gql-input-type.generator.ts` handle the cycle
between filter inputs.
- **Arg processor** — `FilterArgProcessorService` no longer hard-rejects
accessing a relation by its name. When the value is a nested object on a
MANY_TO_ONE field, it recurses into the target object's metadata so each
leaf still gets validated and coerced. Depth-capped at 1.
- **Query parser** — new `parseRelationSubFilter` branch in
`graphql-query-filter-field.parser.ts`. When triggered: looks up the
target object metadata, calls `ensureRelationJoin` against the outer
query builder, and recurses via a child
`GraphqlQueryFilterConditionParser` scoped to the target.
`and`/`or`/`not` inside the relation filter keep working because the
child dispatches through the same `parseKeyFilter`.
- **Shared join utility** — `ensureRelationJoin.util.ts` is a single
function that inspects `queryBuilder.expressionMap.joinAttributes` for
the alias before adding a `LEFT JOIN`. Rewired the existing inline
`qb.leftJoin` calls in the order parser and group-by service to use it,
so filter-driven joins no longer collide with sort-driven joins on the
same relation.

### Out of scope (explicit)

- ONE_TO_MANY reverse traversal (needs EXISTS subqueries)
- Aggregates (`company.people.count > 5` — needs HAVING)
- View-filter storage (no `relationPath` column on `ViewFilterEntity`)
- REST DSL changes
- Frontend filter-picker UX
- Nesting deeper than one hop (parser and arg-processor both reject)

### Open question for review

Permissions. The order-by-on-relation code path already lets users sort
People by Company.name without a Company read-permission check, and this
PR matches that behavior for filters — felt wrong to add a stricter gate
only on the filter side. If we want object-permission gating on the
relation target, it should be a follow-up that covers both paths
consistently. The only attack surface today is existence inference via
timing, identical to what sort already exposes.

## Test plan

- [x] `tsc --noEmit` — clean for changed files (5 unrelated pre-existing
errors on main untouched)
- [x] `oxlint --type-aware` + `prettier --check` — 0 errors on all 17
changed/new files
- [x] `jest filter-arg-processor.service.spec` — 229 tests pass (the new
optional `flatObjectMetadataMaps` arg is backwards-compatible)
- [x] Integration test (`filter-by-relation-field.integration-spec.ts`,
6 cases) — needs to be verified against a seeded test DB. Could not
exercise the happy path in my isolated worktree; depth-2 rejection
passed there.
- [ ] EXPLAIN ANALYZE on the integration test query to confirm the FK on
`person.companyId` is indexed for both standard and custom MANY_TO_ONE
relations.

### Integration test cases

1. Filter People by `company.name = "Airbnb"` (exact match)
2. Filter People by `company.name like "%irbnb%"`
3. Non-matching filter returns empty
4. Combined with a scalar filter at root via `and`
5. **Combined with `orderBy` on the same relation** — proves the
join-dedupe works (without `ensureRelationJoin`, TypeORM throws
"duplicate alias")
6. Depth-2 nesting (`company.accountOwner.name`) returns
`INVALID_ARGS_FILTER`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 14:45:32 +02:00
Charles BochetandGitHub a941f6fe01 feat(server): migrate TOTP secret encryption to SecretEncryptionService (#20577)
## Summary

Removes the last `APP_SECRET`-derived at-rest encryption site by
migrating `core.twoFactorAuthenticationMethod.secret` from
`SimpleSecretEncryptionUtil` (AES-256-CBC with key derived from
`sha256(APP_SECRET + userId + workspaceId + 'otp-secret' +
'KEY_ENCRYPTION_KEY')`) to the versioned `enc:v2:` envelope
(ENCRYPTION_KEY → HKDF-SHA256 bound to `workspaceId` → AES-256-GCM).

- New `decrypt-legacy-aes-cbc.util.ts` faithfully reproduces the
pre-migration CBC derivation byte-for-byte;
`SecretEncryptionService.decryptVersioned` dispatches to it when callers
pass `legacyAesCbcPurpose`, with a dedicated one-shot WARN log family.
- `TwoFactorAuthenticationService` now uses `encryptVersioned` /
`decryptVersioned` (passing the legacy purpose so existing rows still
decrypt). `SimpleSecretEncryptionUtil` and its spec are deleted;
`TwoFactorAuthenticationModule` imports `SecretEncryptionModule` in
their place.
- `TwoFactorAuthenticationMethodEntity` gets a `@Check` decorator
(`CHK_twoFactorAuthenticationMethod_secret_encrypted`) restricting
`secret` to the `enc:v2:` envelope; the matching 2.5 slow instance
command (`1798000009000-encrypt-totp-secrets`) cursor-paginates `JOIN`ed
`userWorkspace` rows to recover the legacy `userId`, re-encrypts to
`enc:v2`, and applies the CHECK constraint in `up()`.

### Deviation note

The plan suggested wiring a workspace-only legacy derivation directly
into `decryptVersioned`. In practice the production rows are
user-and-workspace-scoped (the legacy purpose is
`\${userId}\${workspaceId}otp-secret`), so a workspace-only derivation
could not recover them. The PR keeps the public `decryptVersioned` API
intact and adds an optional `legacyAesCbcPurpose` so callers that can
reconstruct the legacy context (the 2FA service and the slow command)
opt in.

### Final state of remaining `APP_SECRET` usages

- HS256 JWT verify (read-only, self-retiring once asymmetric migration
completes).
- Express-session cookie signing.
- Approved-access-domain HMAC (signing root, not at-rest).
- Zero-friction fallback in `resolveEncryptionKeysOrThrow`
(intentional).

No production at-rest data is encrypted with `APP_SECRET`-derived keys
anymore.

## Test plan

- [x] `npx jest src/engine/core-modules/secret-encryption
src/engine/core-modules/two-factor-authentication` — 170 unit tests
pass, including new unit tests for the legacy CBC util and the new
`SecretEncryptionService` fallback branch.
- [x] `npx jest --config ./jest-integration.config.ts
test/integration/upgrade/suites/2-5-instance-command-slow-1798000009000-encrypt-totp-secrets.integration-spec.ts`
— 4 integration tests cover legacy-CBC seed → slow command → `enc:v2`
round-trip, idempotency, CHECK constraint enforcement on `up()`, and
rollback via `down()`.
- [x] `npx oxlint --type-aware` and `npx prettier --check` clean on all
touched files.
- [ ] CI on this PR (server validation, tests, lint, typecheck).
2026-05-14 13:24:06 +02:00
Abdul RahmanandGitHub fc53f18a9f Twenty discord integration (#20530)
<img width="1290" height="777" alt="Screenshot 2026-05-14 at 8 55 02 AM"
src="https://github.com/user-attachments/assets/37cde89b-b4c3-438a-8ccf-39621f9799b4"
/>



https://github.com/user-attachments/assets/cd8af6f9-f2e5-47cf-bf1f-57590bb358d1



https://github.com/user-attachments/assets/ff53c6b7-ee1a-42db-bbc5-d7df5b3fa6b0



https://github.com/user-attachments/assets/ddca2c16-774d-4672-aa61-05e878d8b2b7
2026-05-14 10:57:53 +00:00
Anish PaudelandGitHub ddaba26abe chore(.vscode): add remaining packages to VSCode workspace (#20570)
## Context
This PR extends the multi-root VSCode workspace configuration introduced
in #2937 by adding the remaining packages from the `packages/*`
directories to the `twenty.code-workspace` folders array.

## Problem
Previously, some packages were not added to the multi-root workspace
configuration. As a result, when opening the repository as a vscode
workspace, those packages were hidden from the VSCode Explorer because
they were not part of the configured workspace folders.

## Benefits

- Prevents packages from being hidden when the repository is opened as a
vscode workspace.
- Improves consistency and navigation across the monorepo workspace
experience.

## Related PR
- #2937
2026-05-14 10:38:18 +00:00
neo773andGitHub 42975a4168 fix(server): decouple SDK client generation from workspace activation (#20514)
`activateWorkspace` enqueues SDK gen job inside
`WorkspaceManagerService.init()` introduced by
https://github.com/twentyhq/twenty/pull/19271

But if enqueue call fails it crashes cuz it doesn't have try catch so
created workspace is in corrupted state

<img width="636" height="812" alt="image"
src="https://github.com/user-attachments/assets/09acd042-46d0-4225-adc0-c74ea770785d"
/>


FIx:
Move SDK enqueue out of `init()` Call after
`activateAndInitializeUpgradeState` succeeds, wrap in try catch. Mirror
preInstalledAppsService.installOnWorkspace pattern.

Assuming enqueue failure if Redis is unavailable we fallback to
`SdkClientArchiveService.downloadArchiveBufferOrGenerate` which
generates it on the fly

Around 19 workspaces in prod affected with status `ONGOING_CREATION`
2026-05-14 10:25:24 +00:00
Félix MalfaitandGitHub dbc033b29b fix(auth): exclude /authorize from MinimalMetadataGater loading gate (#20572)
## Summary

Fixes the blank `/authorize` page reported when a user reopens the OAuth
consent screen on `app.twenty.com` after a prior multi-workspace SSO
sign-in.

### Reproduction

1. ChatGPT (or any MCP client) opens
`https://app.twenty.com/authorize?client_id=…` while signed out.
2. User picks "Continue with Google", lands in workspace selection,
picks a workspace, authorizes the app. Works.
3. Some time later, ChatGPT re-opens
`https://app.twenty.com/authorize?client_id=…`.
4. **Observed:** fully blank page, no console errors. Deleting the
`tokenPair` cookie unblocks it.

### Root cause

The multi-workspace social-SSO branch
([auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011))
lands the user on `app.twenty.com/welcome?tokenPair=…` with a
workspace-agnostic token. `SignInUpGlobalScopeFormEffect` writes that
into the host-scoped `tokenPair` cookie on `app.twenty.com`, and nothing
clears it after the user proceeds to a workspace subdomain. On the next
visit to `app.twenty.com/authorize?…`, `MinimalMetadataGater` sees
`hasAccessTokenPair === true` and renders `<UserOrMetadataLoader />`
instead of `<Authorize />`. The loader never goes away because:

- `IsMinimalMetadataReadyEffect` waits for `metadataStore.status ===
'up-to-date'`.
- `MinimalMetadataLoadEffect` only loads metadata when
`hasAccessTokenPair && isActiveWorkspace`, and the default domain has no
workspace context.

Skeleton loader stays forever → user perceives "blank page".

Some users get rescued by `WorkspaceProviderEffect` auto-redirecting
them to their last-authenticated workspace subdomain, but that cookie is
set with `domain: .twenty.com` and silently fails to persist for users
on custom domains — so the bug is most visible there.

### Fix

`/authorize` only issues `findApplicationRegistrationByClientId`, which
is a `PublicEndpointGuard` query. It doesn't need workspace metadata.
Add it to the gater's excluded-paths list alongside the existing
pre-auth pages (`SignInUp`, `Verify`, `Invite`, …) so the page renders
immediately regardless of token state.

This is a one-line, minimum-blast-radius fix. Two related cleanups are
separate concerns and not addressed here:
- Clearing the workspace-agnostic `tokenPair` cookie on `app.twenty.com`
after workspace selection.
- Fixing `lastAuthenticatedWorkspaceDomain` propagation for
custom-domain users.

## Test plan

- [x] `npx oxlint` + `prettier --check` on the touched file — clean.
- [x] `npx nx typecheck twenty-front` — clean.
- [ ] Manual: with a stale `tokenPair` cookie set on `app.twenty.com`,
open `https://app.twenty.com/authorize?client_id=<valid>&…` — consent
screen renders, no blank.
- [ ] Manual: signed-out → open the same URL — still redirects to
`/welcome` and back through the flow.
- [ ] Manual: signed-in on a workspace subdomain → open
`https://app.twenty.com/authorize?…` — `WorkspaceProviderEffect`
auto-redirect still kicks in (unchanged behavior).
2026-05-14 10:17:17 +00:00
Abdullah.andGitHub 72f857fc10 [Website] Refine feature card scroll entrance to a subtle opacity fade (#20574)
Thomas mentioned the animations need to be subtle. Therefore, this PR
removes transform-based slide/rotate animations in favor of a clean 0.6s
opacity fade for a polished, professional appearance.

Before:


https://github.com/user-attachments/assets/bede627f-d4b3-4126-8eb0-c1d5a9b4d16a

After:


https://github.com/user-attachments/assets/85cb604b-a8c9-4730-85d3-8edb8ef0fc71
2026-05-14 10:01:07 +00:00
a47e1e0e5e Fix time consuming search ilike fallback (#20544)
## Context
When the tsvector full-text search returns 0 hits on the first page,
SearchService falls back to ILIKE '%word%' over searchVector::text. The
leading wildcard makes the GIN index unusable, so it seq-scans the
table.
On large searchable custom objects (e.g. a workspace with ~500k rows in
_logs) a single fallback can take 2–3s, multiplied across all searchable
objects in one request.

## Implementation
Wrap the fallback query in a tiny TypeORM transaction and apply a
Postgres per-statement timeout via set_config('statement_timeout', ms,
true) (= SET LOCAL). On timeout, Postgres throws 57014 (QUERY_CANCELED);
we catch it, warn-log with workspace/object context, and return [] for
that object

## Note
This PR bounds the slow fallback and doesn't make it fast. The right
structural fix is to let the fallback use an index. Since tsvector does
not work with certain language (which is the reason why the ILIKE
fallback was implemented in the first place), we should probably use the
pg_trgm extension instead (@FelixMalfait)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-14 10:00:33 +00:00
Charles BochetandGitHub 0d5617d446 chore(server): drop unused postgresCredentials feature (#20573)
## Summary

Drops the `postgresCredentials` legacy feature: a never-finished
"postgres proxy" that would have let users query their workspace data
over a standard Postgres connection. Nothing — frontend, e2e, Zapier,
docs, other server code — calls these mutations/query.

## History

- **Introduced** June 2024 (#5767, Thomas Trompette) as "first step for
creating credentials for database proxy", alongside the Postgres FDW /
remote-server work and the custom `twenty-postgres-spilo` image. Planned
follow-ups (provisioning a DB on the proxy, mapping users, exposing it
as a remote server) never landed.
- **Abandoned** January 2026 (#17001, Weiko) when the sibling "remote
integration" feature was removed as a BREAKING CHANGE — "not maintained
for more than a year and never officially launched". The spilo image was
then replaced with vanilla `postgres:16` (#19182, March 2026), retiring
the FDW infrastructure entirely.
- This PR finishes the cleanup: removes the orphaned module, the
`allPostgresCredentials` relation, `JwtTokenTypeEnum.POSTGRES_PROXY` +
payload, the reserved metadata keywords, and adds a 2.5.0 fast instance
command that drops `core.postgresCredentials` (reversible `down`).
Regenerated frontend GraphQL types + SDK metadata client.

## Test plan

- [x] `tsgo --noEmit` clean on twenty-server + twenty-front; lint +
prettier clean on touched files.
- [x] `database:migrate:generate` reports no pending schema diff; server
boots and serves the new schema.
2026-05-14 12:04:09 +02:00
Abdullah.andGitHub 61683d8bda [Website] Replace product page hero visual with interactive CRM depicting AI chat in action. (#20566)
Before:

<img width="1439" height="518" alt="image"
src="https://github.com/user-attachments/assets/4d294a1b-c5a0-43b2-9895-61a8ee19da62"
/>

After:


https://github.com/user-attachments/assets/c019586f-ef9f-4ae0-8afe-14f08e8cb057
2026-05-14 07:48:14 +00:00
ab705b14d7 i18n - translations (#20569)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-14 09:38:09 +02:00
90537b3b88 i18n - translations (#20567)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-14 09:34:18 +02:00
Paul RastoinandGitHub a754a95d38 Regrant id token write to claude for oidc swap (#20564) 2026-05-14 07:19:08 +00:00
8dd0aa46e3 Update OAuth consent modal design (#20540)
## Summary

Updates the OAuth consent screen to match the provided modal treatment,
including the header artwork, app-to-Twenty logo layout, scope icons,
and a content-hugging title that stays on one line until its max width.

Also preserves `returnToPath` through Google and Microsoft social SSO so
users who sign in from `/authorize` return to the OAuth consent flow
with the original OAuth query parameters intact.

## Reference

![OAuth consent modal
reference](https://raw.githubusercontent.com/twentyhq/twenty/046e02d9b3916e286c6bb4ef1c8045b0df53c074/oauth-modal-reference.png)

## Validation

- `yarn nx test twenty-front
--testFile=src/modules/auth/hooks/__tests__/useAuth.test.tsx
--coverage=false`
- `yarn nx test twenty-server
--testFile=src/engine/core-modules/auth/services/auth.service.spec.ts
--coverage=false`
- `yarn nx typecheck twenty-front`
- `yarn nx typecheck twenty-server`
- `git diff --check`

## Notes

The reference screenshot is linked from a separate branch-hosted image
commit so it renders in the PR body without adding that screenshot to
the product diff.

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-14 07:15:11 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
91b2390ee8 chore(deps-dev): bump vite-plugin-svgr from 4.3.0 to 4.5.0 (#20561)
Bumps [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr)
from 4.3.0 to 4.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pd4d10/vite-plugin-svgr/releases">vite-plugin-svgr's
releases</a>.</em></p>
<blockquote>
<h2>v4.5.0</h2>
<p><em>No significant changes</em></p>
<h5>    <a
href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.4.0...v4.5.0">View
changes on GitHub</a></h5>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/d1f8887202a6edd119053c665f0679419df70025"><code>d1f8887</code></a>
4.5.0</li>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/b326999d5a2546227bb4b2d2cdcf6092392f4d11"><code>b326999</code></a>
ci: fix npm publish</li>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/79300b869297ab2063a730f33ec6f7984b8acbe8"><code>79300b8</code></a>
4.4.0</li>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/b7d3c36dd658a6b8344b82307f36dc3acb0d1479"><code>b7d3c36</code></a>
chore: update deps</li>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/6f9d9af421ec891c1594b5511d4865cea6c8f79a"><code>6f9d9af</code></a>
feat: support rolldown-vite via <code>transformWithOxc</code> (<a
href="https://redirect.github.com/pd4d10/vite-plugin-svgr/issues/130">#130</a>)</li>
<li><a
href="https://github.com/pd4d10/vite-plugin-svgr/commit/cef5adead59da0abfc0b81edf4b2022b193e4607"><code>cef5ade</code></a>
ci: fix release note</li>
<li>See full diff in <a
href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.3.0...v4.5.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite-plugin-svgr&package-manager=npm_and_yarn&previous-version=4.3.0&new-version=4.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 07:04:39 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b36345a699 chore(deps-dev): bump @mui/material from 7.3.8 to 7.3.11 (#20562)
Bumps
[@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material)
from 7.3.8 to 7.3.11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/releases">@​mui/material's
releases</a>.</em></p>
<blockquote>
<h2>v7.3.11</h2>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@7.3.11</code></h3>
<ul>
<li>[autocomplete] Fix highlight sync and scroll preservation (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[autocomplete] Fix popper rendering issues (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[autocomplete] Improve highlight tracking and selection state (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[button] Fix <code>startIcon</code> alignment (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[button] Remove duplicated className entries (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate
(<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[dialog][drawer][focus trap] Fix initial focus target (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[drawer] Fix transition jump (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[input] Fix layout shift with display: flex (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>)
<a
href="https://github.com/oliviertassinari"><code>@​oliviertassinari</code></a></li>
<li>[inputs] Fix autofocus in SSR environment (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[popper] Persist positioning styles when popperOptions changes
reference (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[switch] Fix incorrect <code>role</code> with
<code>slotProps.input</code> (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[utils] Add shadow dom utils (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Update banner to announce v9 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[docs] Add v9 in the versions select in v7.mui.com (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>)
<a
href="https://github.com/alexfauquette"><code>@​alexfauquette</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[internal] Update some host-reference entries (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/alexfauquette"><code>@​alexfauquette</code></a>,
<a href="https://github.com/mj12albert"><code>@​mj12albert</code></a>,
<a
href="https://github.com/oliviertassinari"><code>@​oliviertassinari</code></a>,
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a>,
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></p>
<h2>v7.3.10</h2>
<p>A big thanks to the 15 contributors who made this release possible. A
few highlights :</p>
<ul>
<li>📖 Added the Menubar component page to the docs.</li>
</ul>
<h3><code>@mui/material@7.3.10</code></h3>
<ul>
<li>[alert][dialog] Accessibility improvements (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[autocomplete] Add <code>root</code> slot (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>)
<a href="https://github.com/GerardasB"><code>@​GerardasB</code></a></li>
<li>[autocomplete] Fix helper text focusing input when clicked (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[autocomplete] Fix popup reopening on window focus regain with
openOnFocus (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47914">#47914</a>)
<a href="https://github.com/aman44444"><code>@​aman44444</code></a></li>
<li>[autocomplete] Optimize selected option lookup (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48027">#48027</a>)
<a href="https://github.com/anchmelev"><code>@​anchmelev</code></a></li>
<li>[autocomplete] Support full slots for clearIndicator and
popupIndicator (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47913">#47913</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[button-base] Fix native button detection (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47994">#47994</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[input] Fix high contrast cutoff on first character (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48160">#48160</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[list item text][card header] Revert cleanup of duplicated CSS rules
(<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47938">#47938</a>)
<a href="https://github.com/sai6855"><code>@​sai6855</code></a></li>
<li>[popper] Add missing classes export (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48033">#48033</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[select] Fix focus visible always set on menu item (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48022">#48022</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[slider] Accept readonly array for the value prop (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47961">#47961</a>)
<a href="https://github.com/pcorpet"><code>@​pcorpet</code></a></li>
<li>[switch] Add border to make it visible in high contrast mode (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48210">#48210</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/blob/v7.3.11/CHANGELOG.md">@​mui/material's
changelog</a>.</em></p>
<blockquote>
<h2>7.3.11</h2>
<!-- raw HTML omitted -->
<p><em>May 6, 2026</em></p>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@7.3.11</code></h3>
<ul>
<li>[autocomplete] Fix highlight sync and scroll preservation (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[autocomplete] Fix popper rendering issues (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[autocomplete] Improve highlight tracking and selection state (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[button] Fix <code>startIcon</code> alignment (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[button] Remove duplicated className entries (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate
(<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[dialog][drawer][focus trap] Fix initial focus target (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[drawer] Fix transition jump (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[input] Fix layout shift with display: flex (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>)
<a
href="https://github.com/oliviertassinari"><code>@​oliviertassinari</code></a></li>
<li>[inputs] Fix autofocus in SSR environment (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[popper] Persist positioning styles when popperOptions changes
reference (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[switch] Fix incorrect <code>role</code> with
<code>slotProps.input</code> (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[utils] Add shadow dom utils (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Update banner to announce v9 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[docs] Add v9 in the versions select in v7.mui.com (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>)
<a
href="https://github.com/alexfauquette"><code>@​alexfauquette</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[internal] Update some host-reference entries (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/alexfauquette"><code>@​alexfauquette</code></a>,
<a href="https://github.com/mj12albert"><code>@​mj12albert</code></a>,
<a
href="https://github.com/oliviertassinari"><code>@​oliviertassinari</code></a>,
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a>,
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></p>
<h2>7.3.10</h2>
<!-- raw HTML omitted -->
<p><em>Apr 8, 2026</em></p>
<p>A big thanks to the 15 contributors who made this release possible. A
few highlights :</p>
<ul>
<li>📖 Added the Menubar component page to the docs.</li>
</ul>
<h3><code>@mui/material@7.3.10</code></h3>
<ul>
<li>[alert][dialog] Accessibility improvements (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>)
<a
href="https://github.com/silviuaavram"><code>@​silviuaavram</code></a></li>
<li>[autocomplete] Add <code>root</code> slot (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>)
<a href="https://github.com/GerardasB"><code>@​GerardasB</code></a></li>
<li>[autocomplete] Fix helper text focusing input when clicked (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/mui/material-ui/commit/6ddda377e979d1783b2cc00226098fde4509bb2e"><code>6ddda37</code></a>
[release] v7.3.11 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48478">#48478</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/a2c7fa0eaeec48767ef7728a1d418609e0bf44ea"><code>a2c7fa0</code></a>
[switch] Fix incorrect <code>role</code> with
<code>slotProps.input</code> (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/c3628bd4930e9089fdf41cba78fc2b5798a2bc76"><code>c3628bd</code></a>
[input] Fix layout shift with display: flex (<a
href="https://github.com/oliviertassinari"><code>@​oliviertassinari</code></a>)
(<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/290c89694eae625cf4ad20f874098f0b8889ceb4"><code>290c896</code></a>
[autocomplete] Fix highlight sync and scroll preservation (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/6e172b760815dc3dc8254f58e0dcd793fe690979"><code>6e172b7</code></a>
[autocomplete] Fix popper rendering issues (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/3f2323ce5e780134dcbe75802cf5ab5035822dd4"><code>3f2323c</code></a>
[drawer] Fix transition jump (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/1afbdfebd521eb96c2bd4d43ec1f9b2f48960a00"><code>1afbdfe</code></a>
[button] Fix <code>startIcon</code> alignment (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/dcef24179ffd7a08f47e4c6b656cd58181c34fdc"><code>dcef241</code></a>
[dialog][drawer][focus trap] Fix initial focus target (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>) (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/4b0275ea82d1807324eb04034db50501eac1f802"><code>4b0275e</code></a>
[autocomplete] Improve highlight tracking and selection state (<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a>)
(...</li>
<li><a
href="https://github.com/mui/material-ui/commit/47e41decac18a891f329f854b0d22e2585fa95be"><code>47e41de</code></a>
[internal] Update some host-reference entries (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/mui/material-ui/commits/v7.3.11/packages/mui-material">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@mui/material&package-manager=npm_and_yarn&previous-version=7.3.8&new-version=7.3.11)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 07:04:10 +00:00
Félix MalfaitandGitHub 34d9fcaba1 chore(auth): drop unused workspacePersonalInviteToken from SSO state (#20557)
## Summary

Pure dead-code removal. The Google and Microsoft SSO strategies have
been packing `workspacePersonalInviteToken` into the OAuth `state` blob
and re-emitting it on `validate()`, but
[`signInUpWithSocialSSO`](packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts)
never destructures or reads it from the user object. The SSO flow
resolves invitations by the IdP-verified email instead:

```ts
const invitation =
  currentWorkspace && email
    ? await this.findInvitationForSignInUp({
        currentWorkspace,
        email,   // ← matched against appToken.context.email
      })
    : undefined;
```

So the strategy plumbing is write-only and confusing for readers.

Removed from:
-
[`SocialSSOState`](packages/twenty-server/src/engine/core-modules/auth/types/social-sso-state.type.ts)
- `GoogleRequest['user']` and `MicrosoftRequest['user']`
- The `state` JSON in both strategies' `authenticate()`
- The user object in both strategies' `validate()`

No frontend change needed — `useAuth.buildRedirectUrl` still sets the
`inviteToken` query param when a personal invite token is present (used
by other paths), and nothing on the SSO server side was reading it.

The token-based invitation lookup is preserved for the password signup
flow via `auth.resolver.signUp` → `findInvitationForSignInUp({
currentWorkspace, workspacePersonalInviteToken })`. Unrelated,
untouched.

## Test plan

- [x] `npx jest engine/core-modules/auth` (twenty-server) — 26 suites /
178 tests pass.
- [x] `tsgo -p tsconfig.json --noEmit` — no new errors on the touched
files (pre-existing `IS_REST_METADATA_API_NEW_FORMAT_DIRECT` errors on
main are unrelated).
- [x] `oxlint` + `prettier --check` on touched files — clean.
- [ ] Manual smoke: Google sign-in still works (workspace selection /
verify flow unaffected since `workspaceInviteHash`, `workspaceId`,
`action`, `locale`, `billingCheckoutSessionState`, `returnToPath` still
flow correctly).
2026-05-14 06:15:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8edc76d3d0 chore(deps): bump react-dropzone from 14.2.3 to 14.4.1 (#20560)
Bumps [react-dropzone](https://github.com/react-dropzone/react-dropzone)
from 14.2.3 to 14.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/react-dropzone/react-dropzone/releases">react-dropzone's
releases</a>.</em></p>
<blockquote>
<h2>v14.4.1</h2>
<h2><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.4.0...v14.4.1">14.4.1</a>
(2026-02-10)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>prevent tarball from being included in published package (<a
href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b">7919a23</a>)</li>
</ul>
<h2>v14.4.0</h2>
<h1><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.8...v14.4.0">14.4.0</a>
(2026-01-29)</h1>
<h3>Bug Fixes</h3>
<ul>
<li>accept files with empty type during drag events (<a
href="https://github.com/react-dropzone/react-dropzone/commit/eaa8ba54963480afbba50415b1dd792514fefac1">eaa8ba5</a>)</li>
<li>correct dragLeave filter logic and add dragend test (<a
href="https://github.com/react-dropzone/react-dropzone/commit/273aff4a151aba05ddd473cbc49dc3db59132f2f">273aff4</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add isDragGlobal state for document-level drag detection (<a
href="https://github.com/react-dropzone/react-dropzone/commit/f0874b0ad8e94dbf662b16bc82d0aa2b082ec8ee">f0874b0</a>)</li>
</ul>
<h2>v14.3.8</h2>
<h2><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.7...v14.3.8">14.3.8</a>
(2025-02-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>event_type:</strong> 🎨 Update drop event type to include
FileSystemFileHandle (<a
href="https://github.com/react-dropzone/react-dropzone/commit/d6911c991e077151e302b599b92269432ab0472b">d6911c9</a>)</li>
</ul>
<h2>v14.3.7</h2>
<h2><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.6...v14.3.7">14.3.7</a>
(2025-02-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>make ESM build compatible with native Node.js (<a
href="https://github.com/react-dropzone/react-dropzone/commit/201687900724b45ec98d26cde3626a1c6687c9e1">2016879</a>)</li>
</ul>
<h2>v14.3.6</h2>
<h2><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.5...v14.3.6">14.3.6</a>
(2025-02-23)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>types:</strong> fix React 19 incompatible JSX type import
(<a
href="https://github.com/react-dropzone/react-dropzone/commit/356d9d5cb604c47e393f332f3dfe0e8d12c58d95">356d9d5</a>)</li>
</ul>
<h2>v14.3.5</h2>
<h2><a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.4...v14.3.5">14.3.5</a>
(2024-11-04)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/9247d71a4dd6d9f36f177fee31c90a327a34d297"><code>9247d71</code></a>
chore: add *.tgz to .gitignore</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b"><code>7919a23</code></a>
fix: prevent tarball from being included in published package</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/e2ae36d0c910e64ea7e269e741f26c909f758000"><code>e2ae36d</code></a>
build: remove <code>@​semantic-release/git</code></li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/84ff2d6671f95c925cb3e5c4b785fabd1a13c9fc"><code>84ff2d6</code></a>
build: update deps for semantic-release and node version</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/65095ea5c15b34724793ad38248bf10cd3fef147"><code>65095ea</code></a>
build: add --ignore-engines flag to test workflow for Node 18
compatibility</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/d70ea9632b70770b8d5b68d214f9532842120005"><code>d70ea96</code></a>
build: enable passwordless publishing with NPM trusted publishers</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/2c2c56ee46aefaaa3e81778d822dca4eb2e3c7f7"><code>2c2c56e</code></a>
build: add full git history fetch and build step to release
workflow</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/a9e97413c8a7684d5495ba4c869a8c629e210c59"><code>a9e9741</code></a>
build: add semantic-release configuration with trusted publishing
support</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/1455f4cc0a3c389a056532027017e92874d41fba"><code>1455f4c</code></a>
build: Update checkout action to version 4 in release workflow</li>
<li><a
href="https://github.com/react-dropzone/react-dropzone/commit/f4e67064ebc9310ac0e6e04d6f04cc7c4b4da057"><code>f4e6706</code></a>
build: Update Node.js setup action to version 4</li>
<li>Additional commits viewable in <a
href="https://github.com/react-dropzone/react-dropzone/compare/v14.2.3...v14.4.1">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for react-dropzone since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=react-dropzone&package-manager=npm_and_yarn&previous-version=14.2.3&new-version=14.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 06:13:10 +00:00
Charles BochetandGitHub 7fa136f305 feat(twenty-server): migrate remaining at-rest encryption sites to versioned envelope (#20550)
## Summary

Second PR in the encryption key rotation series. The previous PR
(#20528) introduced `ENCRYPTION_KEY` + the versioned
`enc:v2:<keyId>:<base64>` envelope inside `SecretEncryptionService` and
migrated `ConnectedAccountTokenEncryptionService` as the first consumer.
This PR routes every remaining at-rest encryption site through the
versioned envelope so that `ENCRYPTION_KEY` (and the future
`FALLBACK_ENCRYPTION_KEY`) actually covers them. The legacy unprefixed
CTR ciphertext remains readable as a fallback during the rollout window
— every migrated read site uses `decryptVersioned`, which transparently
delegates to the legacy CTR decrypt when it sees an unprefixed payload.

### Service migrations
- **`ApplicationVariableEntityService` (#8)** — workspace-scoped. HKDF
info is bound to each row's `workspaceId`. A new
`decryptAndMaskVersioned` helper lands on `SecretEncryptionService` for
the resolver display path.
- **`ApplicationRegistrationVariableService` (#7)** + consumers —
**instance-scoped**. Registration variables are server-level config
readable by every workspace that installs the application, so HKDF info
is `instance`. Updated consumers:
  - `LogicFunctionExecutorService.buildServerVariableEnvMap`
  - `ConnectionProviderService.getClientCredentials`
- **`LogicFunctionExecutorService.buildEnvVar` (#9)** —
workspace-scoped. Each variable's `workspaceId` is threaded into
`decryptVersioned`, so per-workspace HKDF contexts are honoured at
execution time.
- **`UpdateApplicationVariableActionHandlerService`**
(workspace-migration runner) — threads `workspaceId` through the
secret/non-secret toggle.
- **`JwtKeyManagerService` (#3)** — instance-scoped. Signing keys are
shared across the JWKS.
- **`ConfigStorageService` (#6)** — instance-scoped sensitive STRING
config variables.

### Slow instance commands (2.5.0)

Each migrated site has a paired backfill that re-encrypts existing rows
into the v2 envelope before the column is constrained:

| timestamp | command | scope | CHECK constraint |
|---|---|---|---|
| `1798000005000` | encrypt-application-variable | workspaceId |
`"isSecret" = false OR value = '' OR value LIKE 'enc:v2:%'` |
| `1798000006000` | encrypt-application-registration-variable | instance
| `"encryptedValue" = '' OR value LIKE 'enc:v2:%'` |
| `1798000007000` | encrypt-signing-key-private-keys | instance |
`"privateKey" IS NULL OR value LIKE 'enc:v2:%'` |
| `1798000008000` | encrypt-sensitive-config-storage | instance | _none_
— heterogeneous jsonb column |

All backfills are idempotent (the SELECT filter skips rows already in v2
form) and run before their respective `up()` adds the CHECK constraint.
Every `down()` deliberately stops at dropping the CHECK constraint —
they intentionally do not re-introduce plaintext on rollback.

### Tests

- Unit specs for each new slow command cover the v2 upgrade path, the
idempotency invariant, and the instance vs workspace HKDF scope.
- New `JwtKeyManagerService` spec asserts
`decryptVersioned`/`encryptVersioned` are called without `workspaceId`
(instance scope).
- Updated existing specs for `ApplicationVariableEntityService`,
`ConfigStorageService`, and `buildEnvVar` to assert the versioned API
and the workspace HKDF context plumbing.
- New `SecretEncryptionService.decryptAndMaskVersioned` cases in the
service spec.
- Updated the `applicationRegistrationVariable` integration spec to
assert the column now stores `enc:v2:<keyId>:<base64>` instead of raw
legacy CTR.

### Out of scope (future PRs)
- `PostgresCredentialsService` — bespoke
`jwtWrapperService.generateAppSecret`–derived key +
`encryptText`/`decryptText` from `auth.util.ts`; deserves its own
migration.
- `SimpleSecretEncryptionUtil` (TOTP) — entirely different `aes-256-cbc`
`iv:enc` format; deserves its own migration.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx nx lint:diff-with-main twenty-server` (oxlint + prettier)
- [x] Local jest run for `secret-encryption | connected-account-token |
application-variable | application-registration-variable | build-env-var
| jwt-key-manager | config-storage | encrypt-application-variable |
encrypt-application-registration-variable | encrypt-signing-key |
encrypt-sensitive-config-storage` — 17 suites, 106 tests pass.
- [x] Local jest run for `upgrade | instance-command` — 12 suites, 86
tests pass.
- [ ] CI green
- [ ] Manual review of CHECK constraint shapes by a server reviewer
(each one matches `enc:v2:%` rather than `enc:v_:%` since none of the
migrated columns can legitimately hold `enc:v1:` ciphertext).
2026-05-14 06:10:52 +00:00
Charles BochetandGitHub 04eb913551 chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary

- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.

Net diff: 37 files, +106 / -1727.

## Test plan

- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
2026-05-14 08:12:00 +02:00
Charles BochetandGitHub 6dd1e8a471 feat(upgrade): expose twenty_upgrade_workspaces_up_to_date_total (#20555)
## Summary
Adds a fourth gauge alongside the existing
`twenty_upgrade_workspaces_behind_total` /
`twenty_upgrade_workspaces_failed_total` so dashboards can show how many
workspaces are currently healthy, not just the ones that need attention.

- New gauge: `twenty_upgrade_workspaces_up_to_date_total`
- New count is computed during
`UpgradeStatusService.refreshInstanceAndAllWorkspacesStatus` (cheap — we
already iterate over every workspace), persisted in the existing
`UpgradeStatusCacheService` so the cache-hit path stays a single round
trip, and surfaced via `InstanceAndAllWorkspacesUpgradeStatusDTO` for
the admin panel.

## Files
-
`packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts`
— register the new ObservableGauge
-
`packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts`
— count UP_TO_DATE workspaces during refresh, propagate through cached
path
-
`packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts`
— persist `upToDateWorkspaceCount` next to behind/failed sets
-
`packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts`
— `Int` field on the admin DTO
- Tests: extended `upgrade-status.service.spec.ts` (14/14 green) —
cached and refresh paths both assert on `upToDateWorkspaceCount`

## Follow-up
A companion `twenty-infra` PR adds the new tile + line on the
upgrade-status Grafana dashboard.
2026-05-13 23:45:56 +02:00
martmullandGitHub d81756f2e8 Add default value to apiKey for authentication method (#20552)
- fix authentication method default value when docker instance to apiKey
2026-05-13 21:17:44 +00:00
Félix MalfaitandGitHub 49b9660420 fix(auth): preserve returnToPath across Google/Microsoft SSO redirects (#20537)
## Summary

Fixes the consent-modal-not-reopening half of
[#20535](https://github.com/twentyhq/twenty/issues/20535): when a
signed-out user opens an OAuth `/authorize?...` URL (e.g. ChatGPT
connecting to `api.twenty.com/mcp`) and signs in with **Google or
Microsoft**, the original `/authorize` request was lost and the consent
screen never reopened.

### Root cause

`PageChangeEffect` already saves the deep link as `returnToPath` (Jotai
atom) before navigating to `/welcome`. That atom is in-memory: it
survives SPA navigation, and the cross-subdomain workspace hop is
handled by `useBuildSearchParamsFromUrlSyncedStates` round-tripping the
value through the URL.

But the social-SSO path leaves `app.twenty.com` entirely —
`app.twenty.com/welcome` → `api.twenty.com/auth/google` → Google →
`api.twenty.com/auth/google/redirect` → frontend — so the atom is wiped.
None of the existing code paths plumbed `returnToPath` through that hop:
- `useAuth.buildRedirectUrl` packed `workspaceInviteHash`/`action`/etc.
but not `returnToPath`.
- `SocialSSOState` / the Google + Microsoft strategies didn't carry it
through the OAuth `state` blob.
- `signInUpWithSocialSSO` + `computeRedirectURI` didn't re-emit it on
the redirect back to the frontend.

The email path worked because all transitions stay on the default
frontend domain, so the atom survives until `SignInUpGlobalScopeForm`
bakes it into the workspace URL.

### What changed

Plumb `returnToPath` through the SSO state the same way
`workspaceInviteHash` and `action` already flow:

- **Frontend** (`useAuth.buildRedirectUrl`): read `returnToPath` from
the Jotai store and append it to `/auth/google` / `/auth/microsoft` when
set and structurally valid.
- **Server types** (`SocialSSOState`, `GoogleRequest['user']`,
`MicrosoftRequest['user']`): add optional `returnToPath`.
- **Strategies** (`google.auth.strategy.ts`,
`microsoft.auth.strategy.ts`): include `returnToPath:
req.query.returnToPath` in the JSON `state` and read it back in
`validate`.
- **auth.service.ts** (`signInUpWithSocialSSO`, `computeRedirectURI`):
forward `returnToPath` on both branches — the multi-workspace redirect
to `AppPath.SignInUp?tokenPair=...` and the single-workspace redirect to
`<workspace>/verify?loginToken=...`. Validated via a new
`isValidReturnToPath` helper so a tampered query value can't become an
open-redirect vector.

After the round-trip, `useInitializeQueryParamState` rehydrates the atom
from the URL and `usePageChangeEffectNavigateLocation` resolves it as
the post-auth destination — same mechanism the email path already relied
on.

Out of scope: the OAuth `resource` parameter handling tracked in
[#20296](https://github.com/twentyhq/twenty/issues/20296) is independent
and not addressed here.

## Test plan

- [x] `npx jest src/engine/core-modules/auth` (twenty-server) — 27
suites / 183 tests pass, including new
`is-valid-return-to-path.util.spec.ts`.
- [x] `npx jest src/modules/auth` (twenty-front) — 13 suites / 52 tests
pass, including two new cases in `useAuth.test.tsx` covering the happy
path and the protocol-relative open-redirect guard.
- [x] `npx nx typecheck twenty-server` / `twenty-front` — clean.
- [x] `npx oxlint` + `prettier --check` on touched files — clean.
- [ ] Manual: signed-out user opens
`https://app.twenty.com/authorize?client_id=...` → Continue with Google
→ completes Google → selects workspace → consent screen renders.
- [ ] Manual: same flow, single workspace — lands on consent screen
directly after Verify.
- [ ] Manual: email path still works (regression).
- [ ] Manual: tamper `returnToPath=//evil.com` on the `/auth/google` URL
→ server validation rejects, user lands at default home, not at
`evil.com`.

E2E note: existing `return-to-path.spec.ts` already covers deep links
with query params through the email path. A mock OAuth provider would be
needed to cover the SSO path end-to-end; unit coverage stands in for
now.
2026-05-13 21:54:54 +02:00
Charles BochetandGitHub 2a9fef2341 feat(upgrade): emit structured logfmt logs for upgrade flow (#20539)
## Summary

Adds a small helper that lets every log line in the upgrade flow stay
human-readable while emitting a structured tail that Loki / the
upgrade-status Grafana dashboard can filter on.

Output shape per `logger.log()` call:

```
<humanMessage as-is, may span multiple lines>
[upgrade] event=<event> key=value …    ← always single line
```

Same call produces **one** structured Loki event regardless of how
chatty the human-readable part gets — the dashboard's `|= "[upgrade]"`
filter only matches the trailing line.

## Helper API

```ts
formatUpgradeLog({
  humanMessage: string,                  // free-form, multi-line OK, for engineers scrolling raw pod logs
  event: string,                         // required anchor for Loki filtering / dashboards
  logFields?: Record<string,             // short structured key=value tail
    string | number | boolean | null | undefined
  >,
});
```

- `humanMessage` is preserved as-is. A thrown `new Error('line one\nline
two')` surfacing through `humanMessage` stays human-readable across
multiple lines.
- `logFields` values are logfmt-escaped: whitespace / `=` / `"` get
quoted, embedded `\` / `"` / `\n` / `\r` / `\t` are escaped, `null` /
`undefined` emit literally (`key=null`, `key=undefined`) instead of
being silently dropped — caught via `isDefined` from
`twenty-shared/utils`.
- `event` itself runs through the same escape so an event name with
whitespace or `=` can't break parsing.

## Example output

```
Initialized upgrade sequence: 8 step(s)
[upgrade] event=sequence.initialized stepCount=8 dryRun=false

Upgrading workspace abc-123 1/10
[upgrade] event=workspace.start workspaceId=abc-123 index=1 total=10 dryRun=false

Upgrade for workspace abc-123 completed.
[upgrade] event=workspace.success workspaceId=abc-123 executedByVersion=1.4.0 dryRun=false

Upgrade summary: 42 workspace(s) succeeded, 1 workspace(s) failed
[upgrade] event=summary totalSuccesses=42 totalFailures=1 dryRun=false

Upgrade failed: Workspace migration runner failed:
  - Option id is required
  - Option id is invalid
[upgrade] event=aborted totalSuccesses=41 totalFailures=2 dryRun=false
```

Loki query for the dashboard: `{namespace="twenty"} |= "[upgrade]" |
logfmt event, workspaceId, command, executedByVersion`

## Scope

Only the **upgrade-specific** call sites carry the tag:

- `upgrade.command.ts` — `sequence.initialized`, `sequence.step`
(verbose), `summary`, `aborted`
- `upgrade-sequence-runner.service.ts` — `sequence.stopped`,
`sequence.aborted`
- `workspace-command-runner.service.ts` — `workspace.start`,
`workspace.success`, `cache.invalidate.failed`

`instance-command-runner.service.ts` is intentionally **not** tagged —
`runFastInstanceCommand` / `runSlowInstanceCommand` are also invoked
from `RunInstanceCommandsCommand` (DB init / `run-instance-commands`),
so an `[upgrade]` tag there would mislead at init time. Those lines stay
plain-text; stacks still flow on their own via NestJS
`logger.error(message, error.stack)`.

`chalk` is dropped from `upgrade.command.ts` — ANSI escapes break log
parsers and chalk is a no-op without a TTY anyway.

## Tests

9 inline-snapshot tests in `format-upgrade-log.util.spec.ts` surface the
actual output of every interesting shape (summary call site, multi-line
humanMessage, quoted / escaped / control-character logField values,
null/undefined fields, event name escaping). Snapshots double as
documentation of what a real upgrade log line looks like.

## Test plan

- [x] Unit tests green (`jest format-upgrade-log`)
- [x] oxlint + prettier clean
- [x] tsgo typecheck clean on the upgrade module
- [x] CI green
- [ ] Smoke test on staging: run `upgrade` command, confirm `[upgrade]`
structured lines surface in Loki and `| logfmt` extracts fields
2026-05-13 19:31:22 +00:00
0dc8426727 i18n - translations (#20554)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 21:33:01 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
e16977f97b [breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary

Lets users choose which sub-field of a composite column to sort by —
directly from the sort chip — by clicking the sub-field label and
picking from a dropdown. Persists per view via a new nullable
\`subFieldName\` column on \`ViewSort\`.

Replaces #20438, which proposed a field-settings (admin) configuration
for the same problem. The chip-level approach is more discoverable (the
option lives where the user is looking) and per-view, so different views
on the same object can sort by different sub-fields.

### What changes for users

- **FullName columns**: previously sorted by \`firstName\` and
\`lastName\` together as a stable dual-key sort. Now the user can pick
which sub-field is primary (the other is the tie-breaker). Default
remains \`firstName\` primary, \`lastName\` tie-breaker.
- **Address columns**: previously not sortable at all (not in
\`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown
listing each enabled sub-field. Default is \`addressCity\` if enabled,
else the first enabled sub-field. Disabling a sub-field at the
field-metadata level (existing setting) removes it from the dropdown.
- **Other composite types** (Currency, Phones, Emails, Links, Actor) and
scalar fields keep their existing single-key sort behavior.

### UX

```
┌─────────────────────────┐    ┌─────────────────────────┐
│ ↑ Name · Last name  ✕ │    │ ↑ Address · City  ✕ │
└────────┬────────────────┘    └────────┬────────────────┘
         ▼ (click sub-field)            ▼
   ┌────────────┐                ┌────────────┐
   │ First name │                │ Address 1  │
   │ Last name ✓│                │ Address 2  │
   └────────────┘                │ City      ✓│
                                 │ State      │
                                 │ Postcode   │
                                 │ Country    │
                                 └────────────┘
```

The chip body still toggles direction on click — the \`Dropdown\`'s
internal wrapper calls \`stopPropagation\` so the sub-field click
doesn't bubble to the chip's onClick.

## What changed

**Backend:**
- \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column
- \`ViewSortDTO\`, \`CreateViewSortInput\`,
\`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable:
true })\`
- \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so
the property flows through the update merge path
- \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` —
new \`subFieldName\` entry with \`toCompare: true\` so cache diffs
notice it
- \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads
\`subFieldName\` through
- Instance command migration (\`add-sub-field-name-to-view-sort\`) —
single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\`

**Frontend:**
- \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string |
null\`
- \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field
round-trips
- \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new
field through, include it in the diff so the usual
\`useSaveRecordSortsToViewSorts\` create/update flow fires when it
changes
- \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both
\`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\`
- \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` —
new optional third arg. \`turnSortsIntoOrderBy\` threads
\`sort.subFieldName\` into it.
- \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\`
- New helpers: \`getEnabledAddressSubFields\` (filters by the field's
\`subFields\` setting, falls back to the 6 default visible address
sub-fields), \`getDefaultSortSubFieldForAddress\`,
\`getDefaultSortSubFieldForFullName\`
- New shared types/constants: \`AllowedFullNameSubField\`,
\`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\`
- \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders
as \` · {sub-field}\` with subdued weight after the main label
- \`EditableSortChip\` — builds options from field metadata
(\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName,
\`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels,
persists picks via \`upsertRecordSort\`

## Test plan

- [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front,
twenty-server
- [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed
files: 0 errors
- [x] \`prettier --check\`: clean
- [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the
new \`subFieldName\` override branch for FULL_NAME and ADDRESS;
\`getDefaultSortSubFieldForAddress\` covers the city/first-enabled
fallback path; \`getDefaultSortSubFieldForFullName\` exercises its
constant
- [ ] Manual: sort a People view by Full Name → click the chip's
sub-field label → switch between First name and Last name → reload page
→ choice is preserved
- [ ] Manual: sort a Company view by Address → confirm dropdown lists
only enabled sub-fields → disable Address \`addressCity\` in field
settings → confirm dropdown options update and runtime falls back to the
first enabled sub-field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-05-13 21:25:15 +02:00
8bc21ec434 i18n - docs translations (#20553)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 21:00:52 +02:00
915ce37d20 i18n - translations (#20551)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 19:32:03 +02:00
martmullandGitHub 0cc2194399 Simplify create-twenty-app command (#20512)
## Simplify `create-twenty-app` for zero-interaction use

Makes `npx create-twenty-app@latest my-app` a fully non-interactive,
single-command experience suitable for automated environments (Codex,
Claude plugins).

  ### Changes

- **Remove all interactive prompts** — app name, display name,
description, and scaffold confirmation are now derived from CLI args
with sensible defaults. `inquirer` dependency removed
   entirely.
- **Replace OAuth with API key auth** — use the seeded dev API key
(`DEV_API_KEY`) to authenticate against the Docker instance as
`tim@apple.dev`, eliminating the browser-based OAuth
  flow.
- **Docker-first with early validation** — check Docker is installed
before scaffolding; if missing, print the install URL and exit. Detect
alternative runtimes (Podman, nerdctl).
- **Parallel image pull** — `docker pull` runs in the background during
scaffold + dependency install, saving 10-30s on typical runs.
- **Always pull latest image** — ensures the dev server is up-to-date on
every run.
- **Stop detecting port 3000** — only check port 2020 (Docker instance).
- **Update CLI flags** — remove `--skip-local-instance` and `--yes`; add
`--skip-docker`.
- **Update CI workflows and docs** — align e2e workflows, package
README, and template README/cd.yml with the new flow.
2026-05-13 16:44:27 +00:00
70a3b25680 i18n - docs translations (#20549)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 19:07:39 +02:00
6a9509ec01 i18n - translations (#20548)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 19:00:36 +02:00
EtienneandGitHub fcd2d586ee chore(billing) - remove feature flag (#20531)
- remove feature flag
- remove old enforce cap usage logic
2026-05-13 18:52:29 +02:00
EtienneandGitHub 695a374efd fix - nav drawer expansion (#20545)
[PR](https://github.com/twentyhq/twenty/pull/20505), I merge, does not
fix the whole issue

Closes https://github.com/twentyhq/twenty/issues/20502
2026-05-13 16:30:37 +00:00
martmullandGitHub dea1f89904 Inject none secret env variables into front components (#20511)
## Summary
- Inject non-secret application variables (`isSecret: false`) into front
component `process.env` via the existing Web Worker `setWorkerEnv`
mechanism
- Filter secret variables server-side in the resolver so they never
reach the browser
- Set application variables before system variables (`TWENTY_API_URL`,
`TWENTY_APP_ACCESS_TOKEN`) to prevent override
- Wire up environment variable keys in the logic function code editor
for TypeScript autocomplete

  ## Test plan
  - [x] Unit tests for `buildNonSecretEnvVar` (6 passing)
  - [x] Typecheck passes for `twenty-front` and `twenty-server`
- [x] Install an app with both `isSecret: false` and `isSecret: true`
variables, open a front component, verify only non-secret vars appear in
`process.env`
- [x] Open a logic function editor, verify autocomplete suggests
declared variable keys
2026-05-13 16:27:56 +00:00
59b993bdb3 i18n - translations (#20547)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-13 18:32:28 +02:00
Charles BochetandGitHub e0b4c9918b feat(twenty-server): introduce ENCRYPTION_KEY env var with versioned envelope (#20528)
## Summary

- Adds `ENCRYPTION_KEY` (primary) and `FALLBACK_ENCRYPTION_KEY`
(decrypt-only fallback for rotation) env vars to twenty-server, with
backward-compatible fallback to `APP_SECRET` when `ENCRYPTION_KEY` is
unset.
- Introduces a versioned ciphertext envelope `enc:v2:<keyId>:<base64>`
using AES-256-GCM with HKDF-SHA256 derived per-context keys. The 8-hex
`keyId` fingerprint lets every row identify which physical key encrypted
it, so rotation routes directly to primary or fallback without trial
decryption; GCM's auth tag gives true integrity (legacy CTR has none).
- Migrates `ConnectedAccountTokenEncryptionService` to the new envelope
and plumbs `workspaceId` through every caller, so per-workspace HKDF
context binds each row to its tenant.

The remaining encryption sites (`jwt-key-manager`, `config-storage`,
`postgres-credentials`, `application-variable`, TOTP) stay on the legacy
unprefixed CTR path and will be migrated in follow-up PRs. The
operator-facing rotation runbook is out of scope here.

### Format details

`enc:v{N}:{keyId}:{base64}` — `N=2` is the only version produced by new
writes (`v1` exists for backward-compatible decryption of existing
connected-account rows). `keyId =
sha256(rawKey).slice(0,4).toString('hex')`. The CHECK constraint on
`core.connectedAccount.{accessToken,refreshToken}` is relaxed from `LIKE
'enc:v1:%'` to `LIKE 'enc:v_:%'` so both versions pass.

### Key resolution

| `ENCRYPTION_KEY` | `FALLBACK_ENCRYPTION_KEY` | `APP_SECRET` | Encrypt
with | Decrypt try order |
|---|---|---|---|---|
| set | set | (any) | `ENCRYPTION_KEY` | match `keyId` → primary →
fallback |
| set | unset | (any) | `ENCRYPTION_KEY` | match `keyId` → primary |
| unset | set | set | `APP_SECRET` | match `keyId` → `APP_SECRET` →
fallback |
| unset | unset | set | `APP_SECRET` | match `keyId` → `APP_SECRET` |
| unset | unset | unset | startup error | n/a |

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx jest
'secret-encryption|connected-account-token-encryption|connected-account-refresh-tokens|encrypt-connected-account-tokens|connection-provider-oauth-flow'`
— 87 tests pass
- [x] New `secret-encryption.service.versioned.spec.ts` covers: key
resolution table (no-key error, APP_SECRET fallback, ENCRYPTION_KEY
precedence), v2 round-trip with/without workspaceId, GCM tamper
rejection, workspaceId-mismatch rejection, keyId-based primary→fallback
routing, missing-key error names the fingerprint, v1 legacy decryption,
no-prefix legacy decryption, malformed envelope rejection.
- [x] Updated `connected-account-token-encryption.service.spec.ts`
covers workspaceId binding and HKDF context isolation.
- [x] Updated slow instance command spec verifies workspaceId is
threaded through encryption and the relaxed `enc:v_:%` LIKE pattern
matches both v1 and v2.
- [ ] Manual E2E: connect a Gmail account on a freshly deployed instance
with `APP_SECRET` only → confirm `core.connectedAccount.accessToken` is
`enc:v2:<keyId>:<base64>`.
- [ ] Manual E2E: rotate — set `ENCRYPTION_KEY=<new>` and
`FALLBACK_ENCRYPTION_KEY=<old APP_SECRET>`, restart, confirm
pre-rotation rows still decrypt and new rows carry the new `keyId`.
- [ ] Manual E2E: missing key — set `ENCRYPTION_KEY=<new>` without the
fallback, confirm decrypt error names the old `keyId` so the operator
can identify the missing key.
2026-05-13 16:15:54 +00:00
aec2e01662 fix(server): handle ImapFlow socket errors instead of crashing the process (#20510)
## Summary

`ImapFlow` is an `EventEmitter`; per Node.js semantics, an emitted
`'error'` event with no listener becomes an uncaught exception that
exits the process. Both ImapFlow construction sites in `twenty-server`
(`ImapClientProvider` used by all messaging flows, and
`testImapConnection` in the connection-wizard validator) currently build
the client without attaching a permanent `'error'` listener, so a
transient socket condition (idle timeout, network blip, server-side
disconnect) crashes `twenty-server` and triggers a container restart
with a ~1 min HTTP 502 window for end users.

This patch attaches an `'error'` listener at each call site that logs
the error and lets `imapflow`'s internal reconnect handle recovery. Same
shape / same precedent as #20143 (Redis session-store client) which
fixed #20144.

Closes #20509.

## What changed

-
`packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts`:
`ImapClientProvider.createConnection` now attaches `client.on('error',
...)` between construction and `connect()`.
-
`packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts`:
`testImapConnection` does the same on its short-lived test client.

Both listeners log via the existing `Logger` instance (matching the
resolver-level logging already in `ImapClientProvider.getClient`) and
surface `error.stack` so transient socket conditions are observable but
no longer fatal.

## Crash this fixes (real production stack)

```
node:events:487
      throw er; // Unhandled 'error' event
      ^

Error: Socket timeout
    at TLSSocket.<anonymous> (/app/node_modules/imapflow/lib/imap-flow.js:795:29)
    at TLSSocket.emit (node:events:509:28)
    at Socket._onTimeout (node:net:610:8)
    ...
Emitted 'error' event on ImapFlow instance at:
    at ImapFlow.emitError (/app/node_modules/imapflow/lib/imap-flow.js:397:14)
  code: 'ETIMEOUT',
```

End-user impact: server process exits cleanly (code 0), Docker / k8s
restarts it; the DB, worker, redis, and caddy containers are unaffected
— only the API server dies, taking the GraphQL/REST surface offline for
a ~1 min health-check warmup.

## Test plan

- [x] `npx nx typecheck twenty-server` (planned — relying on CI for
verification)
- [x] `npx nx lint:diff-with-main twenty-server` (planned — relying on
CI for verification)
- [x] Manually reproduced the crash on `v2.2` by hitting an
IMAP/SMTP/CalDAV outbound flow with Gmail; with the patch applied
locally to the running container (verified the listener fires and logs
without process exit), the server stays up across the same trigger
sequence.
- [ ] Unit-level coverage: behavior is "listener exists, doesn't throw"
— not easily covered without a contrived socket-mock test. Existing call
sites have no unit tests today; happy to add one if a reviewer prefers,
otherwise mirroring the convention from #20143 which merged without a
new test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-13 15:13:55 +00:00
EtienneandGitHub aecfe699f4 feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish
callback in chat-execution.service.ts calls a new
decrementAndCheckAvailableCredits method on each model step, so Redis is
debited incrementally as the agent runs rather than all at once at the
end.

Credit exhaustion stops the stream mid-run. When a step depletes the
remaining credits, a hasNoMoreAvailableCredits flag is set and passed
into the stopWhen predicate of streamText, causing the agent to halt
before starting the next step.

A new credits-exhausted event is introduced. After the stream drains and
the response is persisted, if credits ran out the job publishes a
dedicated credits-exhausted event to the frontend instead of the normal
message-persisted event.

The frontend handles this new event. useAgentChatSubscription has a new
credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error
on the atom, closes the writer, and stops the streaming state —
triggering the existing AiChatCreditsExhaustedMessage UI.
2026-05-13 15:09:08 +00:00
gusfromspaceandGitHub 068fc9363d fix(navigation): settings drawer should never appear collapsed (#20505)
## Summary

Fixes #20502

/claim #20502

The navigation drawer's collapsed state is persisted to \`localStorage\`
via \`isNavigationDrawerExpandedState\`. When a user collapses the
drawer in the main app and then opens settings via a **direct URL,
refresh, or new tab**, the settings layout renders in collapsed mode —
no \`useOpenSettingsMenu\` call is made in those paths to force
expansion.

\`StyledAnimatedContainer\` (which controls the outer drawer width) used
raw \`isNavigationDrawerExpanded\` with no settings-route override.
Inner components (\`NavigationDrawerItemsCollapsableContainer\`) already
guard with \`isExpanded = isNavigationDrawerExpanded || isSettingsPage\`
— the outer container simply needed the same treatment.

**Fix:** derive \`isExpanded = isSettingsDrawer ||
isNavigationDrawerExpanded\` using the already-in-scope
\`useIsSettingsDrawer()\` result and pass it to both
\`StyledAnimatedContainer\` and \`StyledContainer\`. 1 derived variable,
2 prop changes, no new hooks or state.

**Changed files:**
-
\`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx\`

## Test plan

Verified via code review and CI. The fix is structurally identical to
the existing \`isSettingsPage\` guard already used in
\`NavigationDrawerItemsCollapsableContainer\` — the outer container
simply lacked the same treatment. No new hooks, no side effects, no
state mutations.

Manual UI verification (collapse → navigate to settings → confirm
expanded) was not performed against a running instance. If the
maintainers want to verify, the logic path is the same as the inner
component guard that already ships in production.

> **Note (2026-05-12):** PR #20508 was submitted after this PR with a
\`useEffect\`-based approach. That approach has already received a
review comment from a team member noting that \`useEffect\` should be a
last resort per the project's own React guidelines. This fix uses no
effects or imperative state — only a derived variable.

---

> [!NOTE]
> **AI-Assisted Contribution**
> This patch was generated by
[Mesopredator](https://github.com/GusFromSpace), an autonomous code
intelligence system.
> Static analysis located the bug, the fix was written and verified with
\`tsc --noEmit\` + \`node\` test suite, and reviewed by a human before
submission.
> Please treat this as a community contribution and request changes if
needed.
2026-05-13 14:19:25 +00:00
99a5a038bc fix(server): add Apple seed workspace as fallback for single-workspace mode (#20498)
## Issue
As per the developer docs, the local setup uses the `npx nx
database:reset twenty-server` command, which seeds 4 workspaces. This
works correctly for multi-workspace mode
(`IS_MULTIWORKSPACE_ENABLED=true`) and integration tests but causes
issues in single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`) or
when switching from multi-workspace mode back to single-workspace mode.

Also, the default mode is single-workspace but 4 workspaces are already
seeded in the database. As a result,
`WorkspaceDomainsService.getDefaultWorkspace()` selects the newest
workspace (Empty4), which is intended only for integration testing and
contains no user data.


There is also an existing warning log mentioning fallback to the Apple
seed workspace when multiple workspaces are found in single-workspace
mode i.e `IS_MULTIWORKSPACE_ENABLED=true`, but it was never implemented.

```
 if (workspaces.length > 1) {
      Logger.warn(
        ` ${workspaces.length} workspaces found in database. In single-workspace mode, there should be only one workspace. Apple seed workspace will be used as fallback if it found.`,
      );
    }
```

Although we could replace with `"nx command-no-deps --
workspace:seed:dev --light"` in `project.json` for `database:reset`,
which will only seed one workspace but it wont resolve issue when
switching from multi-workspace mode back to single-workspace mode or for
integration test `with-db-reset`.


## Changes

- Implement fallback behavior already hinted by existing warning logs.
- Ensure the Apple seed workspace is used as the fallback in
single-workspace mode when multiple workspaces exist.

This improves:
- Local developer onboarding experience.
- Switching between multi-workspace and single-workspace development
modes.
- Consistency during local development and integration testing.

## Related PR
- #19822 
- #20464 

These PRs only resolves the issue for docker environments but not for
local development setup.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-13 14:06:10 +00:00
EtienneandGitHub 3d90b3882b fix(ai-agent-node) - agent node execution error (#20534)
**Root cause:** getWorkflowRunContext(stepInfos) builds a Record<string,
unknown> from the previous steps' results. There is no workspaceId key
in it, so context.workspaceId as string silently evaluated to undefined.
That undefined was then passed all the way down to
WorkspaceCacheService.getOrRecompute, **which correctly throws** when
workspaceId is not a valid UUID.

Before : 
<img width="525" height="130" alt="Screenshot 2026-05-13 at 14 58 54"
src="https://github.com/user-attachments/assets/0549b4dc-7063-44e5-95a1-00a460a6d7f1"
/>

Introduced with billing v2 yesterday, since then, workspaceId is needed
to bill credit usage
2026-05-13 13:20:27 +00:00
Paul RastoinandGitHub 643ec121a7 [twenty-server] no-misused-promise lint (#20529)
# Introduction
Adding `no-miused-promise` lint rule to the twenty-server
In order to flag such pattern
```ts
//  Flagged — forEach doesn't await the callback
items.forEach(async (item) => {
  await process(item);
});
```

## What happened
- Refactored the code-interpreter driver to have a async onResult (
which is also expected by e2b )
- Workspace manager still dirty solution including force cast
- Basic fixes
2026-05-13 13:14:06 +00:00
27fd124c2e Dedicated REST controllers for object & field metadata (#20364)
## Summary
- Replace the dynamic `RestApiMetadataController` (which parsed
`/rest/metadata/*path` and proxied to internal GraphQL) with two
dedicated controllers: `ObjectMetadataController` and
  `FieldMetadataController`.
- Drop the GraphQL hop: reads hit Postgres directly via TypeORM
repositories; writes call the existing
`{create,update,delete}One{Object,Field}` service methods.
- Introduce a new clean response shape behind a workspace feature flag
(`IS_REST_METADATA_API_NEW_FORMAT_DIRECT`) — see grace period below.
- Update the OpenAPI spec so the REST playground reflects the (default)
legacy shape during the grace period.

  ## Why
The legacy metadata controller was over-complex: it routed every method
through a path parser, a set of GraphQL query-builder factories, an
internal GraphQL call, and a
`cleanGraphQLResponse` post-processor. Operation names from GraphQL
(`createOneObject`, `updateOneField`, …) leaked straight into REST
responses. The internal-GraphQL hop also gave us
nothing on metadata reads — pagination, filtering, and serialization all
happen against the same Postgres tables either way.

  ## Feature flag & grace period
  `IS_REST_METADATA_API_NEW_FORMAT_DIRECT` (workspace-scoped):
- **Existing workspaces:** flag absent → resolves to `false` → **legacy
response shape** (no behavior change).
- **Newly created workspaces:** flag seeded to `true` via
`DEFAULT_FEATURE_FLAGS` → **new response shape** from day one.
- **Toggle:** support-assisted (no frontend); customers contact us to
opt into the new shape early.
- **Removal:** the flag, the legacy adapter utils
(`to-legacy-{object,field}-metadata-response.util.ts`), and the
parametrized test wrapper get deleted after the grace window. New shape
becomes the only shape; OpenAPI flips to new shape; POST loses the
conditional and reverts to a declarative response.

  ## Response shapes

| Operation | Legacy (flag OFF, default for existing) | New (flag ON) |
|-----------|-----------------------------------------|---------------|
| `GET /rest/metadata/objects` | `{ data: { objects: [...] }, pageInfo,
totalCount }` | `{ data: [...], pageInfo, totalCount }` |
| `GET /rest/metadata/objects/:id` | `{ data: { object: {...} } }` | `{
... }` |
| `POST /rest/metadata/objects` | `201 { data: { createOneObject: {...}
} }` | `201 { ... }` |
| `PATCH/PUT /rest/metadata/objects/:id` | `{ data: { updateOneObject:
{...} } }` | `{ ... }` |
| `DELETE /rest/metadata/objects/:id` | `{ data: { deleteOneObject: {
... } } }` | `{ ... }` |

Same matrix for `/rest/metadata/fields`. Cursor params
(`starting_after`, `ending_before`, `limit`) and `totalCount` are
preserved across both shapes. POST returns `201` in both (old
  controller already did — the doc on main saying `200` was wrong).

  ## Implementation notes
- Reads go straight to Postgres with TypeORM cursor pagination
(`paginateByIdCursor` util, mutually-exclusive `starting_after` /
`ending_before`). No cache on this path — caching +
  filterable pagination didn't combine cleanly.
- Object endpoints inline `fields[]` via a single follow-up `WHERE
objectMetadataId IN (...)` query.
- Controllers read the flag via `FeatureFlagService.isFeatureEnabled`
and conditionally pass the result through a legacy-shape adapter util
before returning.
- Per-domain REST exception filters
(`{Object,Field}MetadataRestApiExceptionFilter`); the `exceptionCode →
httpStatus` switch is extracted to a util so it can be merged with the
  existing GraphQL handler later.
- New controllers live inside the metadata domain modules
(`metadata-modules/{object,field}-metadata/controllers/`) to match
existing precedent (view-field, view, page-layout, …).
- Removes: `RestApiMetadataController`, `RestApiMetadataService`,
`metadata/query-builder/`, `clean-graphql-response.utils.ts`.
- Integration tests are parametrized over both flag values via
`describe.each` — both shapes are asserted in CI.
- OpenAPI fixes inherited from the migration (kept as-is): documents
flat `fields: [...]` rather than the obsolete `{edges:{node:[...]}}`
wrapping; always emits `totalCount`; POST
status `201`. These match what customers actually receive on both
shapes.

Note: Next goal is to implement something similar for graphql and remove
nestjs-query dependency for those 2 entities, then generalise it.
Note2: We have the same issue with Core Rest API such as
```json
{
  "data": {
    "createCompany": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "createdAt": "2026-05-07T12:14:52.769Z",
      "updatedAt": "2026-05-07T12:14:52.769Z",
      "deletedAt": "2026-05-07T12:14:52.769Z",
   ...
```
with "createCompany" here which is odd compared to REST standards (FYI
@etiennejouan @charlesBochet)

## Before (Without feature flag)
<img width="1346" height="712" alt="Screenshot 2026-05-12 at 20 50 38"
src="https://github.com/user-attachments/assets/316ce225-1045-4aac-97a9-60fd537eb1ec"
/>
<img width="1378" height="729" alt="Screenshot 2026-05-12 at 20 52 24"
src="https://github.com/user-attachments/assets/a621ab6f-e4f8-44d5-817c-1efd25d33c30"
/>

## After (With feature flag)
<img width="1376" height="728" alt="Screenshot 2026-05-12 at 20 50 46"
src="https://github.com/user-attachments/assets/2424d9c5-e4ed-497c-8e5c-6b54d78675e4"
/>
<img width="1375" height="727" alt="Screenshot 2026-05-12 at 20 51 47"
src="https://github.com/user-attachments/assets/101d957f-38ed-45d9-ab7b-f4f4eb983397"
/>

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-13 12:31:16 +00:00
bbc55193f5 Fix phone unique constraints (#20261)
## Summary

Closes #20195

Fix phone field unique constraints so phone numbers are considered
unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`.

- Include `primaryPhoneCallingCode` in the shared phone composite unique
constraint metadata
- Align the frontend settings composite field config with the backend
metadata
- Return all included unique composite subfields when building
create-many conflict fields
- Match composite unique conflict fields as a group during create-many
upserts

## Root Cause

Phone composite metadata only marked `primaryPhoneNumber` as part of the
unique constraint. That made different international phone numbers with
the same national number conflict, for example `+1 123456789` and `+32
123456789`.

## Test Plan

- `yarn workspace twenty-shared build`
- `jest --runTestsByPath <index action handler and create-many utility
specs>`
- `prettier --check <touched files>`
- `oxlint --type-aware <touched files>`
- `nx run twenty-shared:typecheck`
- `nx run twenty-server:typecheck`
- `nx run twenty-front:typecheck`

---------

Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-13 11:57:19 +00:00
Charles BochetandGitHub ac653182b2 feat(server): migrate all remaining JWT token types to ES256 (#20513)
## Summary

Extends the asymmetric signing work from #20467 to cover **every
remaining `JwtTokenTypeEnum` value**: `LOGIN`, `WORKSPACE_AGNOSTIC`,
`FILE`, `API_KEY`, `APPLICATION_ACCESS`, `APPLICATION_REFRESH`,
`APP_OAUTH_STATE`, plus the ACCESS-shaped session token issued by the
code interpreter tool.

After this PR, every JWT the server signs is ES256 with a `kid` pointing
at the current `core."signingKey"` row, while legacy HS256 tokens (no
`kid` header) remain verifiable indefinitely through the existing
fallback in `JwtWrapperService.resolveVerificationKey`. No new entity /
migration / config: this is a pure routing change on top of the
infrastructure that already shipped.

## Why

`#20467` only flipped `ACCESS` and `REFRESH` to ES256. Every other JWT
type was still HS256-signed against the global `APP_SECRET`, which kept
the original blast radius (a leaked `APP_SECRET` invalidates *every* JWT
type forever). Migrating the rest unifies the sign path on rotatable
per-server private keys without forcing any token reissue.

## Mechanical changes

### Sign side (8 services)
- `LoginTokenService.generateLoginToken`
- `TransientTokenService.generateTransientToken`
- `WorkspaceAgnosticTokenService.generateWorkspaceAgnosticToken`
- `ApplicationTokenService.signApplicationToken` (`APPLICATION_ACCESS` +
`APPLICATION_REFRESH`)
- `ApiKeyService.generateApiKeyToken`
- `FileUrlService.signFileByIdUrl` / `signWorkspaceLogoUrl`
- `ConnectionProviderOAuthFlowService.signState` (`APP_OAUTH_STATE`)
- `CodeInterpreterTool.generateSessionToken`

Each call site swaps `jwtWrapperService.sign(payload, { secret:
generateAppSecret(...), ... })` for `await
jwtWrapperService.signAsync(payload, { expiresIn, [jwtid] })`. The
`generateAppSecret` calls on the sign side are dropped (verifier-side
`generateAppSecret` stays in `resolveVerificationKey` for the HS256
fallback).

### Verifier side
- `WorkspaceAgnosticTokenService.validateToken` now goes through
`verifyJwtToken` instead of the bespoke `verify({ secret })` path, so
new ES256 tokens are accepted while the legacy HS256 fallback inside
`resolveVerificationKey` still serves the old shape.
- `JwtWrapperService.sign()` is kept (legacy compat / tests) but is now
strictly deprecated — there are no remaining production callers.

### Async ripple (`signFileByIdUrl` was synchronous)
- `FileUrlService.signFileByIdUrl` and `signWorkspaceLogoUrl` are now
`async`; the `signUrl` callback used by `getRecordImageIdentifier` is
widened to accept `Promise<string | null>`.
- Every direct/indirect caller is updated: admin panel (user lookup +
statistics + top workspaces), search service
(`computeSearchObjectResults`, `getImageIdentifierValue`), workspace
resolver (`logo` resolver, public workspace by domain/id),
`WorkspaceMemberTranspiler` (now `async toWorkspaceMemberDto[s]` /
`toDeletedWorkspaceMemberDto[s]` / `generateSignedAvatarUrl`),
`UserService.loadSignedAvatarUrlsByUserId`,
`UserWorkspaceService.castWorkspaceToAvailableWorkspace`,
workspace-invitation, approved-access-domain, agent-chat-streaming,
agent-message-part resolver, navigation-menu-item record identifier,
file-ai-chat / file-core-picture / file-email-attachment / file-workflow
/ files-field services, rich-text & files-field query result getters,
and the code-interpreter tool.

## Backward compatibility

- **Legacy HS256 tokens (no `kid`)** keep verifying via
`resolveVerificationKey` → `extractAppSecretBody` → `generateAppSecret`
for both `workspaceId`-bearing and `userId`-bearing payloads.
- The `API_KEY` HS256-via-ACCESS-secret fallback (#16504) still kicks in
inside `verifyJwtToken` for pre-2025-12-12 API keys.
- No payload shape changes, no DB writes, no env var changes — old
tokens issued by `main` continue to authenticate.

## Tests

### Unit (all green locally — 63/63)
Updated specs for every migrated service to mock `signAsync` instead of
`sign` and assert the new option shape:
- `login-token.service.spec.ts`, `transient-token.service.spec.ts`,
`workspace-agnostic-token.service.spec.ts`,
`application-token.service.spec.ts`, `api-key.service.spec.ts`,
`connection-provider-oauth-flow.service.spec.ts`.

### Integration (`jwt-key-rotation.integration-spec.ts`)
- Existing ACCESS coverage (current key, legacy HS256 fallback,
rotated-out key, revoked key, unknown kid) is preserved.
- New `it.each` assertion: `REFRESH`, `WORKSPACE_AGNOSTIC`, and `LOGIN`
tokens emitted by the real signUp → signUpInNewWorkspace →
getAuthTokensFromLoginToken pipeline are ES256 with a `kid` matching the
current signing key — proves end-to-end that the migration didn't
regress those flows.

## Open question (separate decision)

This PR keeps the legacy HS256 verification fallback **forever**. We may
eventually want to sunset it for `API_KEY` once telemetry shows
pre-migration tokens are gone, but that's a separate product/security
decision and not part of this change.

## Test plan

- [ ] CI green
- [ ] `npx nx lint:diff-with-main twenty-server` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `jwt-key-rotation` integration suite passes (new + existing
assertions)
- [ ] Manually verify: signing in issues an ES256 ACCESS / REFRESH
token, generating an API key issues an ES256 token with `kid`, signed
file URL JWT is ES256 with `kid`
- [ ] Pre-existing HS256 tokens still authenticate (covered by
integration test, but worth a manual check with a token from `main`)
2026-05-13 10:53:45 +00:00
Paul RastoinandGitHub a159a68e2c [twenty-server] no floating promises lint rule (#20499)
## Introduction

That's an audit + RFC

## Fire-and-forget (`void`) -- Intentional, correct

These are telemetry, metrics, and audit logging in hot paths or
non-critical contexts. `void` is the right choice.

| File | What was voided |
|---|---|
| `sign-in-up.service.ts` | `metricsService.incrementCounter` (sign-up
metric) + `auditService.insertWorkspaceEvent` (workspace created) |
| `use-graphql-error-handler.hook.ts` | 5x
`metricsService.incrementCounter` (GraphQL operation metrics) |
| `bullmq.driver.ts` | 2x `metricsService.incrementCounter` (job
completed/failed metrics) |
| `call-webhook.job.ts` | 2x `auditService.insertWorkspaceEvent` + 1x
`metricsService.incrementCounter` |
| `custom-domain-manager.service.ts` | `analytics.insertWorkspaceEvent`
(domain activation event) |
| `logic-function-executor.service.ts` |
`auditService.insertWorkspaceEvent` (function execution) |
| `workflow-runner.workspace-service.ts` |
`metricsService.incrementCounter` (throttle metric) |
| `cleaner.workspace-service.ts` | `metricsService.incrementCounter`
(deleted workspace metric) |
| `stream-agent-chat.job.ts` | Detached IIFE for streaming chunks
(intentional concurrent pipeline) |
| `workspace-auth-context.middleware.ts` |
`withWorkspaceAuthContext(...)` (AsyncLocalStorage, returns void anyway)
|

## Top-level script entry points (`void bootstrap()`)

These are module-level calls where the promise has no consumer. `void`
makes the lint rule happy and documents the intent.

| File | What changed |
|---|---|
| `main.ts` | `void bootstrap()` |
| `command.ts` | `void bootstrap()` |
| `queue-worker.ts` | `void bootstrap()` |
| `truncate-db.ts` | `void dropSchemasSequentially()` |
| `codegen/index.ts` | `void generateTests(forceArg)` |

## Now properly awaited -- Real bug fixes

These were floating promises that could silently fail, lose data, or
cause race conditions.

| File | What was fixed |
|---|---|
| `billing-sync-plans-data.command.ts` | `meters.map(async ...)` wrapped
in `Promise.all` -- was returning before upserts finished |
| `cache-storage.service.ts` | `setAdd` and `setPop` had `.then()`
chains that weren't returned/awaited |
| `create-audit-log-from-internal-event.ts` | 4x
`auditService.createObjectEvent` now awaited inside a job |
| `cleaner.workspace-service.ts` | 2x `emailService.send(...)` now
awaited -- emails could silently fail |
| `agent-async-executor.service.ts` | `calculateAndBillUsage` +
`billNativeWebSearchUsage` in `finally` block now awaited |
| `repair-tool-call.util.ts` | `calculateAndBillUsage` now awaited |
| `agent-title-generation.service.ts` | `calculateAndBillUsage` now
awaited |
| `chat-execution.service.ts` | `billNativeWebSearchUsage` now awaited |
| `ai-generate-text.controller.ts` | `calculateAndBillUsage` in
`finally` block now awaited |
| `agent-turn.resolver.ts` | `messageQueueService.add(...)` now awaited
|
| `command.ts` | `app.close()` now awaited (was exiting before graceful
shutdown) |
| `i18n.service.ts` | `loadTranslations()` in `onModuleInit` now awaited
|
| `workspace-query-hook.explorer.ts` | `explore()` in `onModuleInit` now
awaited |
| `message-queue.explorer.ts` | `handleProcessorGroupCollection` in
`onModuleInit` now awaited |
| `ai-billing.service.spec.ts` | Test now properly `await`s the async
call |
| `messaging-messages-import.service.spec.ts` | `expect(...)` now
properly `await`ed for async assertion |
| `archive.finalize()` (3 files) | Voided -- promise resolution already
handled by `pipeline()` / `on('end')` |

## Impersonation & security audit trail -- Upgraded from `void` to
`await`

These were previously fire-and-forget but are
security/compliance-critical events that must be reliably persisted.

| File | What was fixed |
|---|---|
| `impersonation.service.ts` | 4x `auditService.insertWorkspaceEvent`
now awaited (impersonation attempt, token generation
attempt/success/failure) |
| `auth.resolver.ts` | 5x `auditService.insertWorkspaceEvent` now
awaited (impersonation token exchange attempt/success/failure at server
and workspace levels) |
| `auth.service.ts` | 2x `analytics.insertWorkspaceEvent` now awaited
(impersonation attempted/issued) |

## Billing audit -- Upgraded from `void` to `await`

Payment events should be reliably persisted for financial/compliance
reporting.

| File | What was fixed |
|---|---|
| `billing-webhook-invoice.service.ts` |
`auditService.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT)` now awaited
inside Stripe webhook handler |

## Fire-and-forget with proper error handling -- Upgraded from bare
`void`

These remain non-blocking but now catch and log errors instead of
risking unhandled rejections.

| File | What was fixed |
|---|---|
| `logic-function-executor.service.ts` |
`applicationLogsService.writeLogs` now uses `.catch()` instead of bare
`void` -- user-facing logs should surface errors |

## Systemic infrastructure fixes

| File | What was fixed |
|---|---|
| `metrics.service.ts` | `incrementCounter`: Redis cache write
(`metricsCacheService.updateCounter`) now uses `.catch()` internally
instead of raw `await` -- prevents unhandled rejections across all `void
metricsService.incrementCounter(...)` call sites when Redis is unhealthy
|
| `audit.service.ts` | `preventIfDisabled`: made properly `async` with
`await` and consistent `Promise<{ success: boolean }>` return type.
Removed broken `catch` that returned an `AuditException` as a value
(wrong constructor args, unreachable dead code). Removed unused
`AuditException` import |

## Fixed in this session (beyond original PR)

| File | What changed |
|---|---|
| `telemetry.listener.ts` | Removed misleading `Promise.all` + `void`
combo; replaced with simple `for...of` + `void` |
| `message-queue.explorer.ts` | Changed from `void` to `await` so
startup crashes on registration failure |
2026-05-13 10:08:42 +00:00
38d9eacff8 chore: sync AI model catalog from models.dev (#20523)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-13 08:56:41 +02:00
martmullandGitHub 7ade9e3aab Fix application variable issue (#20500)
Fixes wrong formatting converting secret value to ******** before saving
Issued by
https://discord.com/channels/1130383047699738754/1423290797079662602/1502674904770936903
2026-05-12 18:52:17 +00:00
martmullandGitHub c4e897a7b5 Improve linear app (#20453)
- Add front component form to create linear issue
<img width="1512" height="831" alt="image"
src="https://github.com/user-attachments/assets/ffbb223f-30a8-4c64-ac6d-002c29b604c1"
/>
<img width="1512" height="829" alt="image"
src="https://github.com/user-attachments/assets/a5ed2464-35a9-4a60-804c-5f15eb0043b4"
/>

- improve marketplace Linear app page
<img width="1302" height="834" alt="image"
src="https://github.com/user-attachments/assets/cdec7ec2-953d-4a49-a797-5369834b03c1"
/>


- update admin settings to display non secret values
<img width="861" height="473" alt="image"
src="https://github.com/user-attachments/assets/41dadf02-aa5d-4eb6-befe-0ad8ad4049b2"
/>
2026-05-12 16:03:31 +00:00
WeikoandGitHub 70bb011daa fix: map FlatEntityMaps and WorkspaceMigrationRunner exceptions to proper status codes on REST and GraphQL (#20494)
## Context

Calling `POST /rest/views` (and other metadata mutations) currently
returns a generic `500` for user-input failures:

Ex:
1. **Invalid `objectMetadataId`** —
`resolveEntityRelationUniversalIdentifiers` throws
`FlatEntityMapsException(RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND)`.
Should be `404`.
2. **Missing required field** (e.g. `icon`) — Postgres raises a `NOT
NULL` violation, wrapped as
`WorkspaceMigrationRunnerException(EXECUTION_FAILED)` carrying a
`QueryFailedError`. Should be `400`.

Neither was caught by `ViewRestApiExceptionFilter`, so both fell through
to `UnhandledExceptionFilter` and were emitted as `500`s without
reaching Sentry.
Same gap existed on most metadata GraphQL resolvers — only
`page-layout*` and `role` resolvers covered
`WorkspaceMigrationRunnerException` via
`WorkspaceMigrationGraphqlApiExceptionInterceptor`.

## Changes

### New filters

REST (`HttpExceptionHandlerService` + Sentry-aware):
- `FlatEntityMapsRestApiExceptionFilter` — maps
`RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND` / `ENTITY_NOT_FOUND` → `404`,
`ENTITY_ALREADY_EXISTS` → `409`, others → `500`.
- `WorkspaceMigrationRunnerRestApiExceptionFilter` — for
`EXECUTION_FAILED`, unwraps the underlying `metadata` /
`workspaceSchema` / `actionTranspilation` error; if it's a
`QueryFailedError` it gets remapped to `400` via
`HttpExceptionHandlerService`. `APPLICATION_NOT_FOUND` → `404`,
`DDL_LOCKED` → `503`, otherwise `500`.

GraphQL (graphql-errors + existing formatter):
- `FlatEntityMapsGraphqlApiExceptionFilter` — kept as the GraphQL-shaped
counterpart (`NotFoundError` / `InternalServerError`).
- `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` — reuses
`workspaceMigrationRunnerExceptionFormatter` for parity with the
existing interceptor.

### Wiring

Filters are now declared **per controller / resolver** via `@UseFilters`
(no global `APP_FILTER` registration) so they participate in the normal
NestJS filter chain instead of being preempted by
`UnhandledExceptionFilter`.

REST:
- `view.controller.ts` — adds `FlatEntityMapsRestApiExceptionFilter` and
`WorkspaceMigrationRunnerRestApiExceptionFilter`.

GraphQL (14 resolvers, all that mutate flat entities):
- `FlatEntityMapsGraphqlApiExceptionFilter` added to: `view`,
`view-field`, `view-field-group`, `view-sort`, `view-group`,
`view-filter`, `view-filter-group`, `page-layout`, `page-layout-tab`,
`page-layout-widget`, `role`, `object-metadata`, `field-metadata`,
`index-metadata`.
- `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` added to the same
list **except** the four already covered by
`WorkspaceMigrationGraphqlApiExceptionInterceptor` (`page-layout`,
`page-layout-tab`, `page-layout-widget`, `role`) — to avoid
double-handling.

## Why per-resolver / per-controller instead of global

Earlier attempt to register the filters globally via `APP_FILTER`
regressed: NestJS reverses the global filter list and
`selectExceptionFilterMetadata` is first-match-wins, so
`UnhandledExceptionFilter` (registered last via `app.useGlobalFilters`
in `main.ts`) ended up first in the iteration order and preempted every
domain-specific filter. The per-resolver / per-controller approach is
explicit and predictable.

## Before
<img width="953" height="450" alt="Screenshot 2026-05-12 at 15 31 40"
src="https://github.com/user-attachments/assets/3c3bc6a8-f6bc-4032-97d0-7243540cfb90"
/>


## After
<img width="1050" height="598" alt="Screenshot 2026-05-12 at 15 31 17"
src="https://github.com/user-attachments/assets/c66c9ce5-d1ea-4f1d-b2fe-07979e2261f7"
/>
<img width="1068" height="503" alt="Screenshot 2026-05-12 at 15 31 09"
src="https://github.com/user-attachments/assets/ddd9eed8-812b-47d6-96cb-b019b807991b"
/>
2026-05-12 16:01:50 +00:00
Charles BochetandGitHub 9e515afb13 feat(server): asymmetric JWT signing with kid + key rotation table (#20467)
## Context

Today every JWT issued by Twenty (access, refresh, login, file, etc.) is
HMAC-signed with a per-token-type secret derived from the global
`APP_SECRET`. Rotating that secret invalidates **every** active token at
once and there is no way to scope a leak to a subset of tokens.

This PR is the first slice of a broader effort to **decouple stateful
encryption (`APP_SECRET`-derived secrets) from stateless encryption
(JWTs)**. It introduces an asymmetric (private/public key) signing path
for `ACCESS` and `REFRESH` tokens and a signing-key registry to enable
**safe rotation**: leaked keys can be revoked by flipping
`revokedAt`/`isCurrent` on the matching row without invalidating tokens
issued by other keys.

> Out of scope (intentionally): swapping stateful encryption for
`APP_SECRET`, asymmetric signing for token types other than
`ACCESS`/`REFRESH`, an admin-panel rotation UI, and an enterprise
re-encryption command. Those will land in follow-up PRs.

## What changes

- **New `core.signingKey` table** (instance command `2.5.0` /
`1778550000000`) storing both the public key (PEM, in clear) and the
private key (PEM, encrypted with `APP_SECRET` via
`SecretEncryptionService`). One row is marked `isCurrent = true`
(enforced by a partial unique index). The row's UUID `id` is used
directly as the JWT `kid`.
- When a key is rotated out, its `privateKey` is nulled (we never keep
historical private keys) but the `publicKey` row stays so previously
issued tokens can still be verified.
- **`JwtKeyManagerService`** lazily loads-or-generates the current
signing key on first use:
  - If a row with `isCurrent = true` exists → decrypts and uses it.
- Otherwise → generates a fresh EC P-256 keypair, encrypts the private
key, inserts the row (UUID id = kid). Handles concurrent insert races
via the unique constraint.
- **`JwtWrapperService.signAsync()`** signs `ACCESS`/`REFRESH` payloads
with `ES256` and a `kid` header. Falls back to `HS256` if no signing key
is available (boot-time DB error, transient failure).
- **Dual-path verification** in both `JwtWrapperService.verifyJwtToken`
and the Passport `JwtAuthStrategy.secretOrKeyProvider`:
- JWT with a `kid` header → resolve the public key PEM by id and verify
with `ES256`,
- otherwise → fall back to the existing `APP_SECRET`-derived `HS256`
path (unchanged).
- **`AccessTokenService` / `RefreshTokenService`** now sign through
`signAsync` (single public surface; the routing detail stays internal to
the wrapper).
- **Public key cache**: a new `SigningKeyEntityCacheProviderService`
plugs into `CoreEntityCacheService` (`signingKeyPublicKey` namespace)
and serves PEMs by id, with the standard local-memo + Redis layering.
- **PEM strings end-to-end**: `jsonwebtoken` accepts PEM strings
directly for both sign and verify, so the manager never converts to a
Node `KeyObject` and the cache hands the PEM straight to `jwt.verify`.

## Why ES256 (and not EdDSA / RS256)

- `@nestjs/jwt` is backed by `jsonwebtoken`, which does **not** support
EdDSA today.
- ES256 keys are tiny (~120 bytes vs 1.6 kB for RS256), signatures are
short (~64 bytes), and signing/verification is fast — important since
JWT verification runs on every authenticated request.
- ES256 is widely supported and standardized (RFC 7518), with mature
ecosystem support.

## Why store the private key in DB (not env)

- No new secret to provision: existing instances already have
`APP_SECRET`, which we reuse to encrypt the private key at rest.
- Self-healing: a fresh instance auto-generates its first signing key on
first boot. Nothing to copy/paste.
- Rotation is a SQL operation against `core.signingKey`, not a redeploy
+ env mutation.

## Backward compatibility

- All previously-issued tokens (no `kid`) keep verifying through the
legacy HS256 path with their existing `APP_SECRET`-derived secret. No
forced re-login.
- Token types not in scope (`WORKSPACE_AGNOSTIC`, `API_KEY`, `FILE`,
`LOGIN`, `EMAIL_VERIFICATION`, etc.) keep their current HS256 behavior
unchanged — they still go through the synchronous
`JwtWrapperService.sign(payload, options)` with a caller-supplied
secret.
- `signWithAppSecret` is kept intentionally as the HS256 fallback path;
it will be deprecated in a follow-up PR.
- If the DB lookup/generation fails for any reason, the wrapper logs the
error and falls back to HS256 — no startup crash, no silent regression.

## Rotation story

1. Bootstrap: first signing call lazily inserts a row in
`core.signingKey` with `isCurrent = true`, `privateKey =
encrypt(pem_A)`. New tokens carry `kid_A`.
2. Rotate: `UPDATE core."signingKey" SET "isCurrent" = false,
"privateKey" = NULL WHERE id = '<kid_A>';` then insert a new row with
`isCurrent = true`. New tokens carry `kid_B`. Tokens still in flight
with `kid_A` keep verifying because the public-key row for `kid_A` is
still there.
3. Revoke: `UPDATE core."signingKey" SET "revokedAt" = now() WHERE id =
'<kid_A>';`. All tokens with `kid_A` now fail verification cleanly with
`UNAUTHENTICATED` (no 500).
4. Tokens with no `kid` (legacy) are unaffected throughout.

## Test plan

- [x] Unit: `JwtWrapperService` dual-path verification (HS256 no-kid vs
ES256 with-kid), unknown-kid → `UNAUTHENTICATED`, `signAsync` happy path
+ `null` when no key, `signAsync` rejection for non-rotatable types.
- [x] Unit: `JwtAuthStrategy` `secretOrKeyProvider` dual-path resolution
and algorithm validation.
- [x] All existing JWT/auth/application unit tests adjusted to the
renamed public method.
- [x] Integration (`jwt-key-rotation.integration-spec.ts`):
- **Happy path**: signed-up user's `ACCESS` token has `alg=ES256` +
correct UUID `kid`, the `isCurrent=true` row exists in
`core.signingKey`, `getCurrentUser` resolves.
- **Legacy fallback**: hand-crafted no-kid HS256 token verifies via the
legacy `APP_SECRET`-derived path.
- **Previous-key rotation**: token signed by a hardcoded *previous* key
whose row is pre-inserted with `privateKey = NULL` (rotated-out) still
verifies — proves the leaked-key revocation flow works in both
directions.
- **Unknown kid**: token signed with an orphan UUID `kid` is cleanly
rejected (no 500).
- [x] `npx nx typecheck twenty-server`
- [x] `npx nx test twenty-server`
- [x] `npx nx run twenty-server:lint`
2026-05-12 15:54:44 +00:00
martmullandGitHub a34bf11dae Upgrade cli tools (#20496)
as title
upgrade version to 2.4.0
2026-05-12 16:56:10 +02:00
ce54f69a4e i18n - website translations (#20495)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-12 16:11:53 +02:00
Abdullah.andGitHub 3904addeb0 chore: add an icon to why-twenty page and update preview (#20482)
Added light-bulb icon and replaced the preview with kanban image.

<img width="751" height="391" alt="image"
src="https://github.com/user-attachments/assets/ac406e84-512f-4dcc-8068-cdb9e3978e92"
/>
2026-05-12 13:50:06 +00:00
d53f3e4ccc fix(kanban): give title full width when card is not hovered (#20455)
## Summary

On kanban cards, the title was being truncated even when the checkbox
wasn't displayed. The checkbox is hidden via `opacity: 0` on the card's
non-hovered state, which keeps it in flex flow and still reserves its
~24px of width — so the title's flex item was shrinking unnecessarily.

This change collapses the checkbox container's `max-width` to `0` (with
`overflow: hidden`) while it's hidden, and expands it back to the
checkbox's natural size (`spacing[6]` = 24px) on hover or when selected.
The existing `transition: all ease-in-out 160ms` animates the title
expanding into the freed space.

### Before
Title truncates with ellipsis even though the checkbox slot is empty:

<img width="350" alt="before"
src="https://i.imgur.com/placeholder-before.png" />

### After
Title uses the full row width when not hovered; the checkbox slides in
on hover (or when the card is selected) and the title reflows.

### Tooltip
The full title is already exposed on hover when truncated — `RecordChip`
→ `Chip` already wraps the label in `OverflowingTextWithTooltip`, which
detects overflow (`scrollWidth > clientWidth`) and renders an
`AppTooltip` with the full text. No additional wiring needed.

## Test plan

- [ ] On a kanban board, verify a long record title now uses the full
card width when the card is not hovered (no ellipsis if the title fits).
- [ ] Hover the card: the checkbox slides in smoothly (animated), and
the title reflows (may now truncate if it doesn't fit).
- [ ] Hover the (now-truncated) title: tooltip with the full title
appears.
- [ ] Select the card via the checkbox: checkbox stays visible (and
title stays in its hover-state width) without hovering.
- [ ] Compact view (eye icon) still renders correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 15:56:33 +02:00
565995e715 security: harden CI against supply-chain attacks (#20476)
- Pin all third-party actions to SHA
- Gate claude.yml triggers to internal authors with Harden-Runner egress
audit
- Ignore fork-PR lifecycle scripts
- Narrow cross-repo dispatch payloads
- Add 7d npm release-age gate
- Add CODEOWNERS on .github/** and .yarnrc.yml

---------

Co-authored-by: prastoin <paul@twenty.com>
2026-05-12 12:20:29 +00:00
EtienneandGitHub 79b612ee12 Billing - Add default ff (#20480) 2026-05-12 10:16:38 +00:00
Charles BochetandGitHub 5003fcbbf2 chore: remove dead feature flags (#20460)
## Summary

Two related cleanups, following the same pattern as #19916 and #19074.

### Dead feature flags

Drops four feature flags whose only references are the enum entry and
the generated GraphQL/SDK files:

- `IS_COMMAND_MENU_ITEM_ENABLED` — never read anywhere.
- `IS_DATASOURCE_MIGRATED` — already commented `@deprecated`. Zero
non-generated consumers.
- `IS_RICH_TEXT_V1_MIGRATED` — the 1-19 migration that gated it was
removed in #19074; the flag became dead at that point.
- `IS_CONNECTED_ACCOUNT_MIGRATED` — only read by the 1-21
`migrate-messaging-infrastructure-to-metadata` command as an
early-return guard, but the flag was never written anywhere in the
codebase, so the guard never fired (and that workspace command is now
removed entirely — see below).

Generated GraphQL/SDK schemas and the `workspace-entity-manager` test
mock are trimmed to match.

### 1-21 workspace commands

Same pattern as #19074 (which removed workspace commands ≤ 1.18). Twenty
is now on 2-5; the 1-21 workspace commands have long since run on every
active workspace and are dead code.

Removes:

- All 14 workspace commands under `upgrade-version-command/1-21/`
(compose-email menu item, key-value-pair index, datasource backfill,
message-thread backfill, dedup engine commands, select-all fixes, AI
response format migration, edit-layout label, drop messaging FKs, folder
parent-id migration, messaging-infra-to-metadata, navigation refactor,
message-thread label fix, search-menu-item label).
- The `1-21-upgrade-version-command.module.ts` registration and the
`V1_21_UpgradeVersionCommandModule` import from
`WorkspaceCommandProviderModule`.

**Kept** (intentionally): the 3 `1-21-instance-command-fast-*` files.
Unlike workspace commands (which mutate data), instance commands carry
**schema deltas** still required by current entity definitions
(`AddViewFieldGroupIdIndex`, `MigrateMessagingCalendarToCore`,
`AddEmailThreadWidgetType`). They remain registered in
`INSTANCE_COMMANDS` and `'1.21.0'` stays in `TWENTY_PREVIOUS_VERSIONS`.
They will fold away naturally on a future version bump when a
`CoreMigrationCheck`-style snapshot picks them up.

## Test plan

- [x] `npx nx typecheck twenty-shared`
- [x] `npx nx typecheck twenty-server`
- [x] `npx nx typecheck twenty-front`
- [x] `npx prettier --check` on the changed files
- [x] `npx oxlint` on the changed server files
- [x] `npx jest feature-flag` (4 suites, 20 tests pass)
- [x] `npx jest workspace-entity-manager` (1 suite, 5 tests pass)
2026-05-12 11:25:20 +02:00
b6b4824104 ci(preview-env): drop yarn -- separator so --light reaches the seed command (#20479)
## Summary

Follow-up to #20464. That PR added `--light` to the preview env seed
command but left the `--` between `yarn command:prod` and the script
args. After yarn strips its own `--`, nest-commander still sees `argv:
[..., '--', 'workspace:seed:dev', '--light']`. Commander.js treats `--`
as the end-of-options marker, so `--light` is parsed as a positional arg
and silently ignored — the seed runs in full mode (Apple + YCombinator +
Empty3 + Empty4) and Empty4 still ends up as the default workspace.

## Evidence

In the preview run on `f706cc052b` (which had #20464's `--light` flag),
the seed step took only ~40s but the `GqlTypeGenerator` log emits four
regenerations across two workspaces with custom objects:

- 28 standard → 28 + 5 custom (`rocket, surveyResult, employmentHistory,
petCareAgreement, pet`) — matches Apple
- 28 standard → 28 + 1 custom (`surveyResult`) — matches YCombinator

With `--light` actually applied, `getLightConfig` returns `{ objects:
[], fields: [] }` so no custom objects should be generated.

The working `twenty-app-dev` invocation in
`packages/twenty-docker/twenty-app-dev/rootfs/etc/s6-overlay/scripts/init-db.sh:66`
is `yarn command:prod workspace:seed:dev --light` — no `--`. Matching
that fixes it.

## Test plan

- [ ] Trigger the preview-app label on a PR, confirm only the Apple
workspace is created and `tim@apple.dev` signs in there
- [ ] Confirm the seed step still passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 10:50:08 +02:00
Félix MalfaitandGitHub 8d54ff6ca0 fix(ci): probe real schema in breaking-changes server readiness check (#20465)
## Summary

The `GraphQL and OpenAPI Breaking Changes Detection` workflow has been
posting graphql-inspector stack traces as PR comments — see [#20445
comment](https://github.com/twentyhq/twenty/pull/20445#issuecomment-4421142635)
for an example.

### Root cause

- The wait step probed readiness with `curl -s URL > /dev/null 2>&1`,
which exits 0 for **any** HTTP response — including 5xx and GraphQL
error JSON. NestJS opens the HTTP listener before the workspace schema
cache is fully populated, so the wait often completed while the server
still served auth/metadata error JSON.
- The introspection download therefore wrote a small (~154-byte) error
payload instead of the real schema. `jq empty` in the validation step
only checks JSON *syntax*, so `{"errors":[...]}` passed validation.
- `graphql-inspector diff` then failed with `Unable to read JSON file:
... Not valid JSON content`, the workflow swallowed the error into the
diff markdown, and the bot posted that stack trace verbatim on the PR.

In the failing run, the main-branch files were 154 B (GraphQL) and 112 B
(REST 500); the current-branch files in the same run were 600 KB–2.8 MB.

### Fix

- Wait steps now POST an authenticated introspection (`{ __schema {
queryType { name } } }`) and require `.data.__schema` plus a 2xx
response from `/rest/open-api/core` (`curl -f`) before declaring the
server ready.
- Validation step now checks for the expected shape (`.data.__schema`
for GraphQL, `.openapi`/`.swagger` for OpenAPI) and includes the first
200 bytes of any bad payload in the warning, so when something genuinely
goes wrong the next debugger has a real lead instead of a generic stack
trace.

## Test plan

- [ ] CI runs against this branch — the workflow's own readiness probes
are now exercised against the real server, so a green run validates the
new check.
- [ ] If the readiness probe still passes but downloads regress, the
strengthened validation step will surface the payload in the workflow
logs instead of posting a graphql-inspector stack trace on the PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-12 06:43:35 +00:00
Abdul RahmanandGitHub 1adb59f7f8 Support optional labels on logic-function input schema fields (#20471)
Adds an optional label field to logic-function input schema properties
(InputSchemaProperty and InputJsonSchema). When set, the workflow
builder renders the label in place of the raw property key for both leaf
inputs and nested sections; when unset, it falls back to the key.
jsonSchemaToInputSchema propagates the label so app authors can declare
it in their JSON schema. Payload paths, the variable picker, and saved
workflow inputs continue to use the property key — labels are
display-only.
2026-05-12 06:40:10 +00:00
Abdul RahmanandGitHub 93df64b9b0 Show logic function label instead of technical name in workflow UI (#20470)
### Before
<img width="1304" height="812" alt="Screenshot 2026-05-12 at 7 02 32 AM"
src="https://github.com/user-attachments/assets/94ca4b1d-69c0-4059-8c45-dd8eae8e2a29"
/>



### After
<img width="1296" height="782" alt="Screenshot 2026-05-12 at 6 53 26 AM"
src="https://github.com/user-attachments/assets/47f2e291-73df-4471-9174-bd5aca23e228"
/>
2026-05-12 05:46:56 +00:00
a3b0a34207 Fix lint:diff-with-main oxlint rules build dependency (#20389)
## Summary

Closes #20382.

`lint:diff-with-main` can load `.oxlintrc.json` files that reference
`../twenty-oxlint-rules/dist/oxlint-plugin.mjs`, but the diff-lint
targets did not build `twenty-oxlint-rules` first. On fresh clones, that
generated plugin file is missing and oxlint fails before linting.

This PR adds `twenty-oxlint-rules:build` before diff lint for:
- the root `lint:diff-with-main` target default
- the custom `twenty-front:lint:diff-with-main` target
- the custom `twenty-server:lint:diff-with-main` target

It also adds regression coverage for:
- the default diff-lint target dependency
- the custom front/server diff-lint target dependencies
- preserving `twenty-website-new` custom dependencies because it does
not load the Twenty oxlint plugin

## Tests

- `npx vitest run --config
packages/twenty-oxlint-rules/vitest.config.mts
workspace/lint-diff-with-main-targets.spec.ts`
- `node_modules/.bin/nx test twenty-oxlint-rules`
- `node_modules/.bin/nx typecheck twenty-oxlint-rules`
- `node_modules/.bin/nx build twenty-oxlint-rules`
- `node_modules/.bin/nx lint:diff-with-main twenty-server`
- `node_modules/.bin/nx lint:diff-with-main twenty-front`
- `npx oxlint -c packages/twenty-oxlint-rules/.oxlintrc.json
packages/twenty-oxlint-rules/workspace/lint-diff-with-main-targets.spec.ts`
- `git diff --check`

## Notes

- `twenty-website-new:lint:diff-with-main` dependency shape remains
unchanged. Full local execution is blocked by missing local `unzip`,
which the existing `check-lottie-frames` script requires.

## Docs / Changelog

No docs or manual changelog update needed. This fixes Nx task wiring for
an existing documented command.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-11 22:03:49 +00:00
009f597eec ci(preview-env): use --light seed so Apple is the default workspace (#20464)
## Summary

- Pass `--light` to `workspace:seed:dev` in the preview env keepalive
workflow so only the Apple workspace is created
- Avoids `Empty4` being picked as the default workspace at sign-in
(which has no users), making the prefilled `tim@apple.dev` credentials
land on a useful workspace

## Why

`workspace:seed:dev` (no flag) seeds Apple + YCombinator + Empty3 +
Empty4. Preview envs run in single-workspace mode
(`IS_MULTIWORKSPACE_ENABLED=false`), so
[`WorkspaceDomainsService.getDefaultWorkspace`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts)
returns the most recently created workspace — Empty4 — which has no
users. Users hitting the preview URL therefore see "Welcome, Empty4."
and can't sign in. Same failure mode #19822 fixed for `twenty-app-dev`.

## Test plan

- [ ] Trigger the `preview-app` label on a PR and confirm the preview
URL signs in to the Apple workspace, not Empty4
- [ ] Confirm the seed step still passes (no `Empty3`/`Empty4`
references break it)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 22:46:54 +02:00
b0413575f5 i18n - translations (#20461)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-11 22:01:37 +02:00
neo773GitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
b03f044d0f feat(messaging): add workspace toggle to sync internal emails (#20457)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-11 19:42:51 +00:00
Charles BochetandGitHub 75c22a2119 feat(front-component-renderer): forward file input metadata (#20458)
## Summary

`<input type=\"file\">` inside front-components was silently
non-functional:
- The host-side `serializeEvent` did not read `target.files`, so the
worker received an empty `onChange` detail.
- `SerializedEventData` had no `files` field.
- The `html-input` schema in `AllowedHtmlElements` exposed neither
`accept`, `multiple`, nor `capture` — the worker could not even
configure the picker.

This PR forwards file metadata (`name`, `size`, `type`, `lastModified`)
through the existing serialized event detail and accepts the missing
attributes on the `html-input` remote element. A new Storybook play test
guards the regression by uploading single and multiple files via
`userEvent.upload`.

Reading file contents inside the worker is intentionally out of scope
here and will need a separate host API bridge (the host has the `File`
objects on the real input element; passing bytes through `postMessage`
is a bigger design call).
2026-05-11 19:30:38 +00:00
3d8207af0f ci(preview-env): replace bore.pub with Cloudflare quick tunnel (#20459)
## Summary

`bore.pub`'s public server has been increasingly unreliable: tunnels
register fine on the runner side (our `Create Tunnel` step always
succeeds), but the bore.pub side later stops accepting inbound traffic,
leaving the preview environment unreachable for the rest of the 5h
keep-alive window with no signal back to the runner. Recent symptom:
`curl http://bore.pub:50422` → `Couldn't connect to server`, while the
corresponding action keeps sleeping.

This PR replaces the `codetalkio/expose-tunnel` action with a direct
invocation of `cloudflared` running an account-less [Cloudflare quick
tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/).
The tunnel is served from Cloudflare's edge so reliability is materially
better, and the URL is HTTPS by default (`https://*.trycloudflare.com`),
which also eliminates the mixed-content issues we'd hit when
`SERVER_URL` was `http://bore.pub:port`.

## What changes

- `Create Tunnel` step now:
  - Downloads a pinned `cloudflared` binary (`2026.3.0`)
- Starts `cloudflared tunnel --url http://localhost:3000` in the
background, logging to `$RUNNER_TEMP/cloudflared.log`
- Polls the log for `https://<name>.trycloudflare.com` (up to 2
minutes), failing fast if the process exits
- Writes the URL to the `tunnel-url` step output — same name as before,
so no downstream changes needed
- `Cleanup` step kills the `cloudflared` process for hygiene

## What stays the same

- `SERVER_URL` plumbing through `.env` → `docker compose up`
- `tunnel-url` artifact
- `$GITHUB_STEP_SUMMARY` formatting
- PR-comment dispatch (`twentyhq/ci-privileged`)
- 5h keep-alive sleep

## Trade-offs

- Quick tunnels are explicitly labelled by Cloudflare for
"testing/development" use without an SLA. For our preview-env use case
(ephemeral, per-PR) that fits, but if we ever need stable URLs on a
custom domain we'd move to *named* tunnels — same `cloudflared` binary,
plus a free Cloudflare account + delegated domain + a service token
stored as a repo secret. Strictly additive when we want it.
- `cloudflared` is pinned to `2026.3.0` to avoid surprise breakage from
upstream releases. Bumping is a one-line change.

## Testing

**Locally (macOS) — verified end-to-end:**
- `cloudflared tunnel --url http://localhost:18080` against a `python3
-m http.server`
- Regex `https://[a-zA-Z0-9-]+\.trycloudflare\.com` correctly extracts
the URL from the log
- `curl $URL/` returns the upstream server's response (HTTP 200, ~0.5s)
- Process supervision: if `cloudflared` dies mid-wait, the step fails
fast instead of hitting the 2-min timeout

**Validation:**
- `actionlint` passes (the remaining shellcheck warnings are in
pre-existing steps, not my changes)
- `shellcheck` on the new Create Tunnel script: clean

**What's not testable from a PR (and why):**
- The full keep-alive workflow runs on `repository_dispatch`, which
always uses the workflow file from `main`. So the cloudflared logic only
runs against PR contents *after* merge.
- I'll trigger a one-off Ubuntu-runner test of just the install + URL
extraction logic via a throwaway branch (`workflow_dispatch`-only) and
link the run here before this merges.

## Test plan

- [ ] Throwaway run validates: cloudflared installs on `ubuntu-latest`,
prints the URL, regex matches, tunnel is reachable from outside the
runner.
- [ ] After merge, the next PR's preview environment uses
`*.trycloudflare.com` instead of `bore.pub:port`, and the URL stays
reachable for the full 5h window.
- [ ] PR-comment bot still posts the preview URL correctly (link should
now be `https://*.trycloudflare.com`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 21:39:22 +02:00
Priyanshu BartwalandGitHub c227b0d06a Fix(UI): Side panel having two scrollbars (#20456)
Fixes: #20417

Screenshot:
<img width="411" height="945" alt="image"
src="https://github.com/user-attachments/assets/925b3ac9-49f2-4fcc-920e-3b9dc34ac466"
/>
2026-05-11 18:42:55 +00:00
f634a4a0c0 fix(front-component): preserve caret position on controlled input/textarea updates (#20416)
## Problem

In the front-component sandbox, typing in the middle of a pre-filled
`<input>` or `<textarea>` caused the caret to jump to the end on every
keystroke. Characters appeared at the correct position, but editing
mid-string was effectively broken.

Root cause: the remote-DOM bridge round-trips every keystroke through
the
worker. By the time the updated `value` prop arrives back at the host,
React applies it by setting `inputElement.value = X` directly, which
browsers always reset the caret to the end.

Typing at the end was unaffected, which is why this went unnoticed in
search fields and similar append-only inputs.

## Fix

For text-like `<input>` types and `<textarea>`, the `value` prop is now
applied imperatively through a ref callback instead of being passed as a
React controlled prop:

- If the DOM value already matches the incoming prop, the assignment is
  skipped entirely.
- If a write is needed and the element is focused, `selectionStart` and
  `selectionEnd` are captured before the assignment and restored
  afterwards with `setSelectionRange`.

Non-text input types (checkbox, radio, file, color, range) and all other
host elements are unaffected.

## Testing

Drop the repro from the issue into any front-component, click between
two
characters in the pre-filled value, and type — the caret should now stay
at the insertion point.

Fixes #20409

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-11 17:47:30 +00:00
689ec16f50 i18n - website translations (#20454)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-11 18:28:11 +02:00
EtienneandGitHub 1b09c69c39 refactor(file v2) - deletion (#20356) 2026-05-11 16:16:46 +00:00
b1f7a2c544 [Website] Replace feature card screenshots with interactive visuals (#20442)
Replace static screenshot images with lightweight interactive
mini-components for all 7 feature cards (Dashboard, Tasks, Emails,
Contacts, Pipeline, Files, Data Import). Add scroll-triggered entrance
animations, shared WindowChrome component, and dark-themed visual
tokens. Remove unused screenshot assets.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 16:13:46 +00:00
Charles BochetandGitHub e7032d0638 fix: prevent admin panel workspace upgrade error from overflowing the table (#20394)
## Summary

In the admin panel workspace detail page, the **Upgrade Status > Last
error** row was rendering the raw `errorMessage` string directly. Long
messages (typically full stack traces) overflowed the table cell and
overlapped neighbouring rows, breaking the layout.

The `Last command` row in the same table already uses
`OverflowingTextWithTooltip` (the helper used elsewhere in settings
tables) to clamp long values to a single line and reveal the full text
in a tooltip on hover.

This PR applies the same treatment to the `Last error` row, with
`isTooltipMultiline` so newlines in the stack trace are preserved when
the tooltip opens.

## Test plan

- [ ] Open Admin Panel > a workspace with a failed upgrade and verify
the `Last error` row stays on a single line with an ellipsis
- [ ] Hover the row and verify the full multi-line error is shown in the
tooltip
- [ ] Verify other rows (Last command, Last updated, etc.) and the
workspace info section are unaffected
2026-05-11 15:46:57 +00:00
5f3407878d i18n - docs translations (#20451)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-11 17:27:46 +02:00
martmullandGitHub 7c053716ae Stop rejecting application install when APP_VERSION is wrong (#20443)
as title

allows to install https://github.com/JordanChoo/twenty-multi-pipeline
locally
2026-05-11 15:17:17 +00:00
6c18bacb93 Encrypt connected account accessToken and refreshToken (#20441)
# Introduction
Encrypt the `connectedAccount` `accessToken` and `refreshToken` using
`APP_SECRET` in order to mitigate potential data leak or `core` table
compromise

## Decrypt
Temporary allow already plain text stored token to be retrieve without
decryption until the slow instance has been passed
Will uncomment the invariant check in a patch when the instance slow has
fully be run

## Standards
- Token are encrypted as quickly as possible
- A token cannot be written in database non encrypted by mistake using a
custom constraint ( `enc:` prefix )

## What's next
We should standardize not managing secret as is in the the services and
layer, they should be encrypted on the flight the earliest and should
never be logged
Will create a dedicated pattern afterwards for `applicationVariables`
secrets too

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-05-11 15:16:12 +00:00
Thomas des FrancsandGitHub 93d83b2e36 [codex] Add Twenty Claude skills package (#20450)
## Summary

Adds a new `twenty-claude-skills` workspace package under `packages/`
for Claude skills related to Twenty.

## Changes

- Registers `packages/twenty-claude-skills` in the root Yarn workspace
list.
- Adds package metadata for `twenty-claude-skills`.
- Adds a README documenting the multi-skill layout.
- Adds the `twenty-record-presentation` skill under
`skills/twenty-record-presentation/SKILL.md`.

## Impact

This gives Claude-specific Twenty skills a dedicated package location
while preserving the skill metadata from the provided skill bundle.

## Validation

- Parsed the root `package.json` and
`packages/twenty-claude-skills/package.json` with Node.
- Compared the imported skill content against the source `.skill`
archive; the only difference is a trailing newline at EOF.
2026-05-11 14:56:45 +00:00
Paul RastoinandGitHub 0c5aec9c73 Ignore twenty versions constant files in prettier (#20448) 2026-05-11 14:23:21 +00:00
Abdul RahmanandGitHub ed75fc8a25 Use workflow inputSchema to render boolean, number, and enum fields in code/logic function steps (#20439)
<img width="415" height="772" alt="Screenshot 2026-05-11 at 4 44 08 PM"
src="https://github.com/user-attachments/assets/32dbdd3c-e60b-4c43-90bc-18be05f22dcf"
/>
<img width="414" height="371" alt="Screenshot 2026-05-11 at 4 48 24 PM"
src="https://github.com/user-attachments/assets/83be062c-7ed3-4953-98bb-e4290865040b"
/>
2026-05-11 14:21:08 +00:00
Paul RastoinandGitHub 9813467cee Refactor SAML relayState structure (#20430)
# Introduction
Restructure the RelayState and avoid asserting the idp identifier from
this opaque blob
Inferring the id from the secured validated and signed request params
2026-05-11 13:49:36 +00:00
1ea7c7ecc4 i18n - translations (#20449)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-11 16:03:20 +02:00
626455b534 chore(members): rename "Access" tab to "Invite" + fix e2e (#20447)
## Summary

Two things, both fallout from #20360:

1. Rename the `Members → Access` tab to `Members → Invite`. The previous
label leaned security-flavored; "Invite" reads as the verb users come
here to do.
2. Fix the `signup_invite_email` Playwright test (failing on main, e.g.
https://github.com/twentyhq/twenty/actions/runs/25671161586/job/75356474079).
The invite-link button moved off the default Team tab when the Members
page got tabbed; the test was looking for it on the wrong tab.

## Rename details

- File: `SettingsWorkspaceMembersAccessTab.tsx` →
`SettingsWorkspaceMembersInviteTab.tsx` (single git rename, ~99%
similarity)
- Exported component: `SettingsWorkspaceMembersAccessTab` →
`SettingsWorkspaceMembersInviteTab`
- Tab id (and URL hash): `access` → `invite`
- Tab title: `Access` → `Invite`
- Icon: `IconKey` → `IconUserPlus`
- Doc breadcrumbs (3 files): `Members → Access` → `Members → Invite`

## E2E fix

`MembersSection` (Page Object Model) now has an `inviteTab` locator (via
`getByTestId('tab-invite')`) and a `goToInviteTab()` helper. Both
`copyInviteLink` and `sendInviteEmail` click the Invite tab first, so
they work regardless of which tab the page lands on initially.
Idempotent if already there.

## Test plan

- [x] CI green (e2e test + lint + typecheck + format)
- Lingui `.po` files will pick up the new source paths on the next
translation pass — not touched here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:54:34 +02:00
2c3e81960c chore: bump version to 2.5.0 (#20446)
## 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

## Checklist

- [ ] Verify version constants are correct

---------

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-11 15:53:34 +02:00
martmullandGitHub 487112d438 Upgrade sdk version (#20444)
from 2.3.0 to 2.3.1
2026-05-11 15:31:40 +02:00
5c1fe45760 fix: update broken AI documentation link (#20401)
## Summary

Updated the broken AI documentation link in
`AiChatApiKeyNotConfiguredMessage.tsx`.

## Changes

* Replaced outdated self-hosting AI docs URL
* Updated link to a valid self-hosting documentation page

Fixes #20071

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-11 15:29:37 +02:00
martmullandGitHub e72a907baa Stop rejecting application token on calendar and message events requests (#20440)
fixes https://github.com/twentyhq/twenty/issues/20423 by authorizing
application token to perform calendarEvents and message queries
2026-05-11 15:24:24 +02:00
8909badc59 i18n - website translations (#20434)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-11 14:15:28 +02:00
Abdullah.andGitHub c81019965d [Website] Extract HomeVisual into shared AppPreview section. (#20432) 2026-05-11 06:46:09 +02:00
58dd5d3561 i18n - docs translations (#20431)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-10 22:35:26 +02:00
c611a7ac20 i18n - docs translations (#20429)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-10 20:37:38 +02:00
50a4fe5040 i18n - translations (#20428)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-10 20:24:31 +02:00
34b927ff23 feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## Summary

- **Public domains can now be bound to a specific app.** When a request
hits an app-bound public domain, route resolution restricts
logic-function matching to that app's HTTP-routed functions only —
isolating each app's routes to its own domain instead of letting routes
from other apps in the workspace match nondeterministically.
- **Settings sidebar reorganized.** Removed the standalone Domains page.
Workspace Domain → General. Approved Domains + Invitations → Members
"Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab.
Roles → Members "Roles" tab.

## Why

The use case: someone building a partner portal app or a lead-collection
app declares private objects (leads, partners…) plus a few public HTTP
routes. Each app needs its own domain (`partners.acme.com`,
`leads.acme.com`) without those domains exposing every other app's
routes in the same workspace. Today's PublicDomainEntity is
workspace-scoped only, so all HTTP-routed logic functions in a workspace
compete for any public domain — first match wins nondeterministically.

## Backend

- Added nullable `applicationId` FK to `PublicDomainEntity`
(cascade-deleted with the app); indexed for the route-trigger lookup.
- New fast instance command
`2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain`
adds the column, index, and FK constraint.
- `createPublicDomain(domain, applicationId)` accepts an optional app
binding; new `updatePublicDomain(domain, applicationId)` mutation
rebinds/unbinds an existing domain. Both validate the application
belongs to the workspace.
- `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)`
returns both the workspace and the matched public domain in one query —
replacing the old back-to-back lookups in the route-trigger hot path.
`getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper.
- `RouteTriggerService` filters `logicFunction` by `applicationId` when
the matched public domain is app-scoped; falls back to workspace-wide
when unbound.
- Three sequential validation queries in `createPublicDomain` now run in
parallel via `Promise.all`.

## Frontend

| Old location | New location |
|---|---|
| Settings sidebar → Domains (standalone page) | Removed |
| Domains page → Workspace Domain | General page |
| Domains page → Approved Domains | Members → Access tab |
| Domains page → Emailing Domains | Apps → Developer tab |
| Domains page → Public Domains | Apps → Developer tab |
| Settings sidebar → Roles (standalone) | Members → Roles tab |
| `pages/settings/roles/` | `pages/settings/members/roles/` |

- The Public Domain detail page has an Application picker that uses
`Select`'s native `emptyOption` + `null` value pattern (matches
`SettingsDataModelObjectIdentifiersForm`).
- Members page tabs use the existing `TabListFromUrlOptionalEffect`
mechanism (rendered automatically by `TabList`) for hash-based tab
activation.
- `/settings/members/roles` redirects to `/settings/members#roles` so
role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page
with the Roles tab pre-selected.
- All affected breadcrumbs updated to nest under their new parents.
- `SettingsPath.Roles` and friends now nest under `members/`;
`Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and
`EmailingDomain` under `applications/`.

## Test plan

- [x] `nx typecheck twenty-front` passes
- [x] `nx typecheck twenty-server` passes
- [x] `oxlint --type-aware` clean on all touched files
- [x] `prettier --check` clean on all touched files
- [x] Migration applied locally; `publicDomain.applicationId` (uuid,
nullable) confirmed in DB
- [x] GraphQL schema exposes `PublicDomain.applicationId`,
`createPublicDomain.applicationId`, `updatePublicDomain` mutation
- [x] **End-to-end route resolution scenarios verified locally:**
  - Domain bound to App A, function in App A → route matches 
- Domain bound to App B, function in App A → route does NOT match (HTTP
404 `TRIGGER_NOT_FOUND`) 
- Domain unbound (`applicationId = NULL`) → route matches workspace-wide

  - Unknown path on bound domain → returns 404 cleanly 
- [x] UI sanity (browser-tested at `apple.localhost:3001`):
  - General page shows Workspace Domain card
  - Members page shows Team / Access / Roles tabs
  - Access tab combines Invite by link + by email + Approved Domains
  - Roles tab embeds the role list
- `/settings/members/roles` direct URL → redirects + Roles tab
pre-selected
  - Apps Developer tab shows Emailing Domains + Public Domains sections
- Public Domain detail page has Application picker dropdown listing
workspace apps
- Sidebar nav: "Domains" and "Roles" no longer present (now folded into
General/Members)

## Notes for reviewers

- Creating a public domain via the UI still requires Cloudflare
credentials in the dev `.env` (`CLOUDFLARE_API_KEY`,
`CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step
is unchanged from main.
- The `applicationId` column is nullable, so existing public-domain rows
continue to work workspace-wide — no data backfill required.
- `SettingsRolesContainer` was deleted (no longer referenced after
`SettingsRoles` index page was removed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:17:28 +02:00
Thomas HeinrichsdoblerandGitHub 086830f81b fix(messaging): reset sync state when IMAP/SMTP/CalDAV credentials are updated (#20405)
## Problem

Updating credentials for an existing IMAP/SMTP/CalDAV connected account
in **Settings → Accounts → Connection settings** has no effect on the
sync. The save persists the new `connectionParameters`, but
`messageChannel.syncStatus` / `messageChannel.syncStage` /
`connectedAccount.authFailedAt` are left untouched, and no fetch job is
queued.

This matters most when the channel is in
`FAILED_INSUFFICIENT_PERMISSIONS` (e.g. after Apple invalidates iCloud
app-specific passwords, or on any other auth failure):
`MessagingRelaunchFailedMessageChannelsCronJob` only retries
`FAILED_UNKNOWN`, so the account is stuck on "Sync failed" forever
despite the credentials now being correct. The only known workarounds
are a direct DB update or deleting and recreating the account.

#19273 fixed the frontend cache angle of credential editing; this PR
fixes the backend half of the same UX (the channel state machine).

## Reproduce

1. Connect an IMAP/SMTP account.
2. Force an auth failure (e.g. revoke the app-specific password
upstream). Wait until `messageChannel.syncStatus` flips to
`FAILED_INSUFFICIENT_PERMISSIONS`.
3. Generate a fresh password, edit the account in **Settings →
Accounts**, save.
4. Observe: account stays "Sync failed" indefinitely;
`core.messageChannel.syncStatus` and
`core.connectedAccount.authFailedAt` are unchanged; no IMAP connect
attempt in the worker logs.

## Root cause


`packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts
→ processAccount` saves the updated `connectionParameters` but never
resets the sync state nor enqueues a fetch job.

The OAuth providers handle this:

| Reset step | `google-apis.service.ts` | `microsoft-apis.service.ts` |
`imap-smtp-caldav-apis.service.ts` (before this PR) |
|---|---|---|---|
| `updateConnectedAccountOnReconnect` (clears `authFailedAt`) | yes |
yes | — |
| `accountsToReconnectService.removeAccountToReconnect` | yes | yes | —
|
| `resetAndMarkAsMessagesListFetchPending` | yes | yes | — |
| Enqueue `MessagingMessageListFetchJob` | yes | yes | — |
| `resetAndMarkAsCalendarEventListFetchPending` | yes | yes | — |
| Enqueue `CalendarEventListFetchJob` | yes | yes | — |

#12061 introduced this behaviour for Google/Microsoft. The IMAP service
was added later and the equivalent reconnect plumbing was never ported.

## Fix

Mirrors the Google/Microsoft pattern in `processAccount`:

- **Inside** the transaction, when an account already exists: clear
`authFailedAt` on the connected account.
- **After** the transaction, when an existing account is being updated:
  - drop the account from `accountsToReconnect` user-vars,
- if the message channel exists and IMAP is configured, call
`resetAndMarkAsMessagesListFetchPending` and enqueue
`MessagingMessageListFetchJob` (skipped while the channel is still
`PENDING_CONFIGURATION`),
  - same logic for the calendar channel and `CalendarEventListFetchJob`.

Wires `MessageChannelSyncStatusService`,
`CalendarChannelSyncStatusService`, `AccountsToReconnectService` and the
messaging/calendar queues into `IMAPAPIsModule`.

## Tests

- Extended the existing `should preserve existing channels when updating
account credentials` case to assert: `authFailedAt: null` is written
within the transaction; `removeAccountToReconnect` is called with the
resolved `userId`; `resetAndMarkAs*` and queue `add` are called for both
channels.
- New case: `should not queue fetch jobs for channels still in
PENDING_CONFIGURATION`.
- New case: `should not run reconnect logic when creating a brand new
account`.

I could not run the full server test suite locally (no `node_modules`
checked out); relying on CI.

## Out of scope

- Extending `UpdateConnectedAccountOnReconnectService` to a non-OAuth
shape: kept inline to minimise the blast radius. Refactoring opportunity
for a follow-up.
- Behaviour when the user removes IMAP or CALDAV from the parameters on
update (the channel currently lingers in its old state). Pre-existing
and not made worse by this PR.
2026-05-10 11:58:53 +00:00
a962cdc34f i18n - website translations (#20418)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-10 11:17:55 +02:00
neo773andGitHub a15bf1e608 Reserve inbound subdomain for SES (#20414) 2026-05-10 11:17:10 +02:00
524e5d8cf7 fix: scroll AI chat to bottom on side panel reopen (#20413)
## Summary
- Fix: when reopening the AI chat side panel, users landed at the top of
the conversation and had to scroll down to find the latest messages
- Root cause: the side panel fully unmounts on close
([SidePanelForDesktop.tsx](packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx)
clears `shouldShowContent` after the close transition), so on reopen the
scroll wrapper is recreated with `scrollTop = 0`. The existing
initial-scroll-to-bottom only fires on thread change, but the
displayed-thread atom outlives the unmount, so no thread change is
detected on a remount and the scroll-to-bottom never runs
- Fix: add a tiny `AgentChatScrollToBottomOnMountLayoutEffect` rendered
inside the message list that calls `scrollAiChatToBottom()` directly in
`useLayoutEffect`. Because the parent returns `null` when there are no
messages, the mount only fires when there is content to scroll past

## Why direct scroll, not the existing flag
`agentChatIsInitialScrollPendingOnThreadChangeState` is paired with a
`MutationObserver` settle that only clears the flag once the subtree is
quiet for 150 ms. During a live stream the message subtree mutates on
every token, the settle resets indefinitely and `visibility: hidden`
never lifts. The thread-change handler avoids this because it is gated
on `!agentChatIsStreaming`
([AgentChatStreamSubscriptionEffect.tsx:78-79](packages/twenty-front/src/modules/ai/components/AgentChatStreamSubscriptionEffect.tsx)),
but a mount can happen at any time, including mid-stream. Scrolling the
DOM directly in `useLayoutEffect` runs synchronously between commit and
paint, so the user sees the bottom on the first paint with no flash and
no settle dependency.

## Tradeoff
A user who scrolled up to read history and then closes/reopens the panel
will land back at the bottom instead of where they were. Standard chat
UX (Slack, ChatGPT, iMessage); preserving per-thread scroll position
would need a new atom and is left out of scope.

## Test plan
- [ ] Open AI chat with messages, close the side panel, reopen → lands
at the bottom (latest messages visible)
- [ ] Reopen the side panel **mid-stream** → lands at the bottom and
continues to follow new tokens (chat is not hidden)
- [ ] Switch between threads → existing thread-change scroll still works
(no double-scroll, no regression)
- [ ] Open AI chat with no messages → no flash, no errors
- [ ] Rapidly close/reopen the panel a few times → each reopen lands at
the bottom

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 10:47:52 +02:00
Abdullah.andGitHub 2e5ccd9b86 [Website] Codebase cleanup and SEO improvements. (#20415) 2026-05-09 22:07:31 +02:00
459c64f642 Prevent non-admin users from impersonating admin users (#20412)
## Summary
- Adds a privilege check to workspace-level impersonation: non-admin
users can no longer impersonate users who have `canAccessFullAdminPanel`
or `canImpersonate` flags
- Adds the same check in JWT token validation as defense-in-depth
(invalidates existing impersonation sessions targeting admin users)
- Adds 3 unit tests covering: non-admin → admin blocked, non-admin →
canImpersonate blocked, admin → admin allowed

## Test plan
- [x] Unit tests pass (14/14 in `impersonation.service.spec.ts`)
- [x] Typecheck passes
- [ ] Verify workspace-level impersonation of regular users still works
normally
- [ ] Verify server-level impersonation by admins is unaffected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-09 11:57:38 +02:00
3420d63b7a i18n - translations (#20411)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-09 11:12:05 +02:00
Félix MalfaitGitHubClaudeCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>claude[bot] <41898282+claude[bot]@users.noreply.github.com>neo773neo773
4da8878697 feat: add email forwarding message channel (#19535)
## Summary

- Add email forwarding as a new message channel type, allowing users to
forward emails from addresses like `support@mycompany.com` into Twenty
- Inbound emails arrive via S3 (SES → S3 bucket), are polled by a cron
job, parsed, routed to the correct workspace/channel, and persisted as
messages
- Dedicated settings page at `/settings/accounts/new-email-forwarding`
where users provide their source email handle and receive a unique
forwarding address
- Forwarding channels bypass the IMAP/mailbox sync state machine — they
skip cron-driven sync, relaunch, and message-list-fetch lifecycle stages
- Forwarding address section shown at the top of the Emails settings
page so users can find/copy their addresses after initial setup
- Tab names for forwarding channels display the user-provided handle
(e.g. `support@mycompany.com`) instead of the internal routing address
- Shared utilities extracted from IMAP driver: `extractThreadId`,
`extractParticipants`, `extractAddresses` to avoid code duplication
- Uses the existing S3 bucket (STORAGE_S3_*) with `inbound-email/`
prefix — no separate bucket needed
- Feature gated behind `isEmailForwardingEnabled` client config
(requires `INBOUND_EMAIL_DOMAIN` + S3 storage)

## New backend modules

- `InboundEmailS3ClientProvider` — lazy-initialized S3 client using
existing storage config
- `InboundEmailStorageService` — S3 operations (get, move to
processed/unmatched/failed)
- `InboundEmailParserService` — RFC 822 parsing via `postal-mime`,
builds `MessageWithParticipants`
- `InboundEmailImportService` — orchestrates download → parse → route →
persist → archive
- `MessagingInboundEmailPollCronJob` — polls S3 `incoming/` prefix,
enqueues import jobs
- `CreateEmailForwardingChannelInput` DTO — accepts user-provided
`handle`

## New frontend components

- `SettingsAccountsNewEmailForwardingChannel` — dedicated page with
handle input form + forwarding address result
- `SettingsAccountsEmailForwardingSection` — forwarding address list on
the Emails settings page
- `useConnectedAccountHandleMap` — shared hook for account ID → handle
lookup
- `useCreateEmailForwardingChannel` — mutation hook accepting handle
parameter

## Test plan

- [x] 17 unit tests for inbound email import service (all outcomes:
imported, unmatched, loop_dropped, unconfigured, parse_failed,
persist_failed)
- [x] 16 tests for `computeSyncStatus` including EMAIL_FORWARDING cases
- [x] 11 tests for `extractEnvelopeRecipient` utility
- [x] TypeScript typechecks pass for both twenty-server and twenty-front
- [x] Lint passes for both packages
- [ ] Manual: create forwarding channel, verify forwarding address
generated
- [ ] Manual: send email to forwarding address, verify it appears in
Twenty

https://claude.ai/code/session_01KpyF6p4cUEnuaT4h8DP5Pm

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
Co-authored-by: neo773 <neo773@protonmail.com>
2026-05-09 11:00:57 +02:00
23aa859502 refactor: scope ApplicationRegistrationService findOneById to tenant rows (#20408)
## Summary

- `findOneById` is the lookup used by tenant-scoped operations
(`update`, `delete`, `rotateClientSecret`, `getStats`,
`transferOwnership`). It currently also matches `ownerWorkspaceId IS
NULL` rows, which was a leftover from when `ownerWorkspaceId` was made
nullable to support catalog-synced apps.
- System-level rows (marketplace catalog entries, the Twenty CLI
registration, dynamic OAuth client registrations) are already managed
through dedicated admin paths — `findAll()` and `findOneByIdGlobal()`
behind `AdminPanelGuard` — so the `IS NULL` fallback in `findOneById` is
unused by any real caller.
- Dropping it tightens the contract: tenant-scoped helpers operate on
tenant rows, global helpers operate on the global view. No behavior
change for any current legitimate flow.

## Test plan

- [ ] Existing application-registration GraphQL queries/mutations
(`findOneApplicationRegistration`, `updateApplicationRegistration`,
`deleteApplicationRegistration`,
`rotateApplicationRegistrationClientSecret`,
`findApplicationRegistrationStats`,
`transferApplicationRegistrationOwnership`) continue to work for a
workspace's own registrations.
- [ ] Admin Panel "Apps" tab continues to list and view all
registrations (uses `findAllApplicationRegistrations` /
`findOneAdminApplicationRegistration`, unaffected).
- [ ] Marketplace catalog sync still upserts catalog rows (uses
`findOneByUniversalIdentifier`, unaffected).
- [ ] Twenty CLI registration bootstrap still works (uses
`findOneByUniversalIdentifier`, unaffected).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:37:21 +02:00
martmullandGitHub 773245fa65 Isolate twenty apps from nx project (#20406)
- avoids importing twenty-shared or else in twenty-apps applications
- update and add workflow action in twenty linear app
2026-05-08 13:16:25 +00:00
Félix MalfaitandGitHub 0f8ee5714f feat(sdk): warn when local server image is behind latest (#20352)
Closes #20328.

## Summary
- Adds a CLI-side check that warns when `twenty-app-dev` is older than
the latest published Docker Hub tag.
- Reads `APP_VERSION` from the running container via `docker inspect` —
no server endpoint, no version exposed publicly. (`APP_VERSION` is
already baked in by `packages/twenty-docker/twenty/Dockerfile` for both
`twenty` and `twenty-app-dev` targets.)
- Fetches latest semver tag from Docker Hub (same API the admin panel
already uses) and caches the result for 24h in
`~/.twenty/version-check-cache.json`.
- Wired into `twenty dev`, `twenty install`, and `twenty server start`.
- Best-effort: silent on container-missing / docker / network errors,
never blocks a command.

## Why CLI-side instead of a `/healthz` extension
The original issue suggested comparing the running server version
against Docker Hub. Exposing the running version on a public endpoint
has a small but real security cost (helps attackers fingerprint
vulnerable deployments), and the version is already inside the image —
so the CLI can read it directly without ever calling the server.

## Test plan
- [x] `nx run twenty-sdk:test` — added unit tests for `parseSemver` /
`compareSemver`
- [x] `nx run twenty-sdk:typecheck`
- [x] `nx run twenty-sdk:lint`
- [ ] Manual: with an old `twenty-app-dev` image running, run `yarn
twenty install` → see warning
- [ ] Manual: with an up-to-date image, run `yarn twenty dev` → no
warning, cache file written
- [ ] Manual: no container at all → no warning, no error
2026-05-08 14:11:34 +02:00
Paul RastoinandGitHub 7f4f2e932c Simplify dispatch pr review (#20397)
# Introduction
Sending minimal information for required metadata to be fetched
afterwards
2026-05-08 12:53:59 +02:00
martmullandGitHub 0d05788547 Protect sendEmail endpoint and thread user context through logic function executor (#20369)
- Thread userId and userWorkspaceId through
LogicFunctionExecutorService.execute() so
application access tokens carry user context when available. This allows
logic functions
triggered by authenticated HTTP routes to call sendEmail with proper
user identity, making
  the existing verifyOwnership() check work naturally.
  - Gate the sendEmail resolver with
SettingsPermissionGuard(PermissionFlagType.SEND_EMAIL_TOOL) instead of
NoPermissionGuard,
ensuring only callers with the SEND_EMAIL_TOOL permission can send
emails.
2026-05-08 10:03:39 +00:00
Abdullah.andGitHub 837a946b5f fix: basic-ftp has FTP Command injection via CRLF (#20396)
Resolves [Dependabot Alert
918](https://github.com/twentyhq/twenty/security/dependabot/918).
2026-05-08 09:38:40 +00:00
WeikoandGitHub cb0b71dbdc fix: validate enum values before opening transaction in alterEnumValues (#20376)
## Context
The validation throws after startTransaction() and outside the
surrounding try.
If the empty-enum branch ever fires, a BEGIN is left open on the
borrowed QueryRunner and never rolled back by this method the caller has
no way of knowing it now owes a ROLLBACK. Whatever the caller does next
on that QueryRunner runs inside the leftover transaction, and if its
lifecycle ends with a release() instead of a rollbackTransaction(), the
connection goes back to the pool with state still pending.
2026-05-08 07:00:13 +00:00
Charles BochetandGitHub 546ab0a036 fix: handle widgets with missing universalConfiguration in 2.3 delete-gauge-widgets command (#20393)
## Summary

The 2.3 `upgrade:2-3:delete-gauge-widgets` workspace command crashed in
production for ~10 workspaces (out of 5000) with:

```
[Error] Cannot read properties of undefined (reading 'configurationType')
    at .../2-3-workspace-command-1798000000000-delete-gauge-widgets.command.js:35:164
    at Array.filter (<anonymous>)
```

### Root cause

Those workspaces have legacy `pageLayoutWidget` rows whose
`configuration` JSONB does not contain a recognized `configurationType`.
This is consistent with the 1.15 backfill
(`MigratePageLayoutWidgetConfigurationCommand`) only migrating widgets
with the deprecated `graphType` and the `IFRAME` /
`STANDALONE_RICH_TEXT` types — any other widget type that was already
missing `configurationType` (or has a value not in the current enum) was
left as-is.

When the cache is recomputed,
`fromPageLayoutWidgetConfigurationToUniversalConfiguration` switches on
`configuration.configurationType`. With no matching case, the function
falls through and returns `undefined`, so the cached
`widget.universalConfiguration` ends up `undefined`. The gauge filter
then dereferences `.configurationType` and throws.

We can't reproduce the affected data locally, but the symptom uniquely
points at this fall-through path — every other code path either throws
earlier (e.g. when `configuration` itself is null) or yields a defined
`universalConfiguration`.

### Fix

In
`2-3-workspace-command-1798000000000-delete-gauge-widgets.command.ts`:

- Skip widgets whose `universalConfiguration` is `undefined` — by
definition they aren't gauge widgets, so they don't belong in the
deletion set.
- Log them as a warning (id and count) so we still have visibility on
the corrupt rows for follow-up cleanup.
- Use optional chaining when comparing the configuration type so the
filter is robust to the same shape going forward.

The fix is minimal and additive: workspaces without corrupt widgets
behave exactly as before, and the upgrade can now succeed on the
affected workspaces.

## Test plan

- [ ] CI lint + typecheck green
- [ ] Run the upgrade on a healthy workspace locally — gauge widgets are
still deleted, no warnings logged
- [ ] On production, verify the 2.3 upgrade no longer fails on the
affected ~10 workspaces and that the warning logs surface the offending
widget ids for follow-up

## Follow-ups (out of scope of this PR)

- Investigate the corrupt widgets surfaced by the new warning log and
decide whether to backfill / delete them in a dedicated upgrade command
- Consider hardening
`fromPageLayoutWidgetConfigurationToUniversalConfiguration` so the
switch fall-through fails loudly (or returns a sentinel) instead of
silently yielding `undefined`
2026-05-08 09:05:22 +02:00
89eeb34ca7 i18n - website translations (#20384)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-08 09:00:45 +02:00
ee8004922e chore: sync AI model catalog from models.dev (#20392)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

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

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

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-08 08:32:12 +02:00
18b9cc5281 i18n - docs translations (#20385)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-08 03:13:20 +02:00
Abdullah.andGitHub 284ebeb12d [Website] Reintroduce the product page. (#20349)
This PR re-introduces the Product page that we decided to hold back in
the first release. Subsequent PRs will work on polishing the Product
page to have interactive visuals, but merging the static version to
record a snapshot.

No production deployments are planned until product page is polished and
blog has the required content - we'd push to prod next week with these
two checkpoints.
2026-05-08 00:01:04 +00:00
fa903c6971 i18n - docs translations (#20378)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-08 00:41:35 +02:00
martmullandGitHub e294d74e07 Detail steps during create twenty app (#20374)
## Before
<img width="391" height="188" alt="image"
src="https://github.com/user-attachments/assets/78a0139f-d992-49cb-98ff-531bdbadc64b"
/>

## After
<img width="419" height="773" alt="image"
src="https://github.com/user-attachments/assets/da9b36a4-74e0-4445-b8c5-7a2329eb4f46"
/>
2026-05-07 20:51:24 +00:00
59f2b1c724 i18n - docs translations (#20375)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 22:47:24 +02:00
4aca4d1143 Add defineApplicationRole method (#20314)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 18:55:13 +00:00
1553ff2857 i18n - docs translations (#20373)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 20:55:43 +02:00
9441e0456c i18n - translations (#20372)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 20:47:07 +02:00
bitloiandGitHub 7999cd3dde fix: Use settings table rows and detail page for app connections (#20257)
## Summary

Closes #20220

- Replace app connection-provider `SettingsListCard` rows with settings
table rows that link to a per-connection detail page.
- Add a connection detail page with inline display-name editing,
provider and handle metadata, visibility, scopes, timestamps, reconnect,
and confirmed disconnect.
- Add a scoped connected-account rename mutation and persist visibility
when reconnecting an existing app OAuth account.

## Tests

- `./node_modules/.bin/jest
packages/twenty-front/src/pages/settings/applications/__tests__/SettingsApplicationConnectionDetail.test.tsx
--config packages/twenty-front/jest.config.mjs --runInBand`
- `./node_modules/.bin/jest
packages/twenty-front/src/pages/settings/applications/tabs/__tests__/SettingsApplicationConnectionsSection.test.tsx
--config packages/twenty-front/jest.config.mjs --runInBand`
- `./node_modules/.bin/jest
packages/twenty-shared/src/utils/navigation/__tests__/getSettingsPath.test.ts
--config packages/twenty-shared/jest.config.mjs --runInBand`
- `./node_modules/.bin/jest
packages/twenty-server/src/engine/metadata-modules/connected-account/resolvers/__tests__/connected-account.resolver.spec.ts
--config packages/twenty-server/jest.config.mjs --runInBand`
- `./node_modules/.bin/jest
packages/twenty-server/src/engine/core-modules/application/application-oauth-provider/__tests__/application-oauth-provider-flow.service.spec.ts
--config packages/twenty-server/jest.config.mjs --runInBand`
- `./node_modules/.bin/oxlint ...`
- `./node_modules/.bin/prettier --check ...`
- `./node_modules/.bin/tsgo -p packages/twenty-shared/tsconfig.json`

## Notes

Full frontend and server typechecks are currently blocked by unrelated
existing workspace issues:
- frontend implicit `any` errors in
`useFrontComponentExecutionContext.ts`
- server missing workspace/dependency modules such as `twenty-emails`,
`twenty-client-sdk/generate`, and `@ai-sdk/azure`
2026-05-07 18:29:02 +00:00
EtienneandGitHub 86eab9ac24 Billing - remove default feature flag (#20365) 2026-05-07 16:48:44 +00:00
95bc8aea28 i18n - docs translations (#20366)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 18:53:27 +02:00
24e64350ee i18n - translations (#20362)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 18:02:01 +02:00
40da6f605d Fix docs apps navigation (#20359)
## Scope & Context

Closes #20358.

The Apps docs were restructured into nested pages, but the generated
docs navigation source still pointed to the old flat Apps page slugs.
This made Developers > Apps navigation entries point to removed English
docs pages.

## Technical inputs

- Update `packages/twenty-docs/navigation/base-structure.json` to use
the current nested Apps docs structure.
- Regenerate `packages/twenty-docs/docs.json` and
`packages/twenty-docs/navigation/navigation.template.json` from the
updated structure.
- Update `generate-docs-json.ts` so localized navigation falls back to
the English slug when the localized `.mdx` file does not exist yet,
avoiding generated 404 links while translations catch up.

## Validation

- Parsed `docs.json`, `base-structure.json`, and
`navigation.template.json` as valid JSON.
- Checked all generated navigation page slugs against existing `.mdx`
files: `missing nav mdx 0`.
- Checked Apps navigation specifically: `missing apps nav mdx 0`.

`yarn docs:generate` could not be run in this local environment because
Yarn/Corepack is not installed here, so I regenerated the JSON files
with the same generator logic via Node.

Co-authored-by: dev111-actor <dev111-actor@users.noreply.github.com>
2026-05-07 18:01:57 +02:00
EtienneandGitHub 9fc5be1c4c Billing - Migrate from Stripe metering (#20298)
**Overall strategy**
**1. Introduce “Billing V2” behind a workspace flag**
Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing
workspaces stay on the old behavior until they’re migrated or explicitly
on V2.

**2. Replace workflow metered SKUs with a resource-credit product**
Conceptually, billable “workflow execution” usage is not the primary
subscription line item anymore. Add a RESOURCE_CREDIT product (and keep
WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and
limits are expressed through credit buckets (e.g. price metadata like
credit_amount), so one product can represent pooled credits instead of a
narrow workflow-only meter.

**3. Migrate subscriptions in two layers**
Schema/catalog: persist extra price metadata (instance upgrade) so the
server knows credit amounts and can match Stripe prices to the new
model.
Per workspace: the registered workspace command
upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have
WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT
prices (using existing Stripe schedule +
BillingSubscriptionUpdateService stack), then treats the workspace as V2
(flag). Workspaces without that legacy item or without a subscription
are skipped.

**4. Unify subscription lifecycle + usage on the server**

**5. Refresh the product surface in Settings**

Test : 

- [x] Subscribe v1 +  Update subscribe + Migrate
- [x]  Subscribe v2 + Update subscribe
2026-05-07 15:42:11 +00:00
Paul RastoinandGitHub ca58c7f15e Fix auto draft workflow (#20357)
# Introduction
Cannot use github graphql mutation with a `GITHUB_TOKEN` needs a
authenticated one
Dispatching to ci-priv to do so
2026-05-07 14:57:17 +00:00
Paul RastoinandGitHub a3224880e0 External contributor auto-draft and dispatch pr-review event type (#20329)
# Introduction

## Auto draft
On external contributor PR creation auto draft it and comment stating
that it needs to be marked as ready for review when it is

## Caveats / future improvement
Lets iterate first but we can imagine future pain points such as:
- Cubic only runs on ready for review PRs
- Expected green ci before turning ready to be review ? ( we could
invoke cubic ourselves )\
- Auto close external contributors draft PR after x duration

## Auto review
Once a PR started to be review or is being synchronized then auto
dispatch auto review
2026-05-07 13:28:43 +00:00
e2afcac076 i18n - docs translations (#20353)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 15:03:14 +02:00
2b4fa9d8cf fix: workspace member "me" filters now work in dashboard widgets (#20266)
## Summary

Fixes #20225

Dashboard graph widgets only passed `timeZone` in
`filterValueDependencies` when computing the GraphQL operation filter.
As a result, `isCurrentWorkspaceMemberSelected` ("me") filters were
silently ignored — the current workspace member ID was `undefined`.

Regular view filters already use `useFilterValueDependencies` which
provides both `timeZone` and `currentWorkspaceMemberId`. This PR
replaces the manual `{ timeZone: userTimezone }` object in
`useGraphWidgetQueryCommon` with `useFilterValueDependencies()`, giving
dashboard widgets full feature parity with view filters.

**Changed file:**
`packages/twenty-front/src/modules/page-layout/widgets/graph/hooks/useGraphWidgetQueryCommon.ts`

---------

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-07 12:36:14 +00:00
2eae25dc34 Improved create-twenty-app documentation for AI coding agents (#20325)
Added a bit of enhanced context for better agentic coding, based on this
[Discord
conversation](https://discord.com/channels/1130383047699738754/1130383048173682821/1501538550301331477).

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-07 14:31:27 +02:00
Paul RastoinandGitHub 1179357bc7 Oxlint ignore twenty-version constant (#20350)
# Introduction
We could handle that by installing prettier on the package.json root and
running it over the twenty versions files inside the ci.
But to be honest it seems redundant, and would bloat the ci in the end.
Lets just not care about lint in these codegen files

Related https://github.com/twentyhq/twenty/pull/20345
2026-05-07 12:01:55 +00:00
f720186122 chore: bump version to 2.4.0 (#20345)
## 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

## Checklist

- [x] Verify version constants are correct

---------

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-07 09:41:47 +00:00
9f930aa366 i18n - translations (#20347)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 11:19:50 +02:00
EtienneandGitHub e49673df79 Fix plan-required modal issue (#20346)
Commit ee6c0ef904 (Replace sign-in mocked metadata with hardcoded
BackgroundMock) removed the mocked metadata loading path in
MinimalMetadataLoadEffect. Before this change, users with an access
token but an inactive workspace (plan-required state) would get mocked
metadata loaded, which satisfied IsMinimalMetadataReadyEffect's
areObjectsLoaded check. After the change, shouldLoadRealMetadata =
hasAccessTokenPair && isActiveWorkspace is false for plan-required
users, so nothing is loaded, isMinimalMetadataReady stays false, and
MinimalMetadataGater renders the loading skeleton forever instead of the
actual auth modal content.

Fix: Add AppPath.PlanRequired and AppPath.PlanRequiredSuccess to
isOnExcludedPath in MinimalMetadataGater, mirroring how AppPath.Invite
is already excluded — both are pages where the user may have a token but
the workspace isn't fully active, so they don't need metadata to render.
2026-05-07 11:12:50 +02:00
martmullandGitHub 900f70fb9e Add description to oAuth_only app created (#20336)
## Before

<img width="1000" height="555" alt="image"
src="https://github.com/user-attachments/assets/bda317c7-93e7-448e-8e65-a9cc1be6df95"
/>

## After
<img width="1010" height="504" alt="image"
src="https://github.com/user-attachments/assets/5cbc5c6c-5cc3-40d8-9545-ba0f3d2675b4"
/>
<img width="980" height="574" alt="image"
src="https://github.com/user-attachments/assets/4386fe56-0614-4a4e-82a0-c9f46af69c85"
/>
2026-05-07 09:02:04 +00:00
Charles BochetandGitHub 027331c893 chore(front): move mocked-metadata helpers under src/testing (#20341)
## Summary

Follow-up to #20308. After that PR, production code no longer loads
mocked metadata when the user is unauthenticated. The remaining
mocked-metadata helpers (`useLoadMockedMetadata`,
`preloadMockedMetadata`) are now only consumed by Storybook decorators,
so move them next to the other testing-only utilities to make their
scope explicit and prevent accidental re-introduction of a runtime
dependency on mocks.

- `src/modules/metadata-store/hooks/useLoadMockedMetadata.ts`
  → `src/testing/hooks/useLoadMockedMetadata.ts`
- `src/modules/metadata-store/utils/preloadMockedMetadata.ts`
  → `src/testing/utils/preloadMockedMetadata.ts`

The three Storybook decorator imports
(`MockedMetadataLoadEffect`, `ObjectMetadataItemsDecorator`,
`WorkflowStepDecorator`) are updated to the new `~/testing/...` paths.

No runtime behavior change.

## Test plan

- [x] `npx oxlint --type-aware -c .oxlintrc.json` on touched files —
clean
- [x] `npx prettier --check` on touched files — clean
- [ ] CI: storybook, unit tests, e2e tests
2026-05-07 08:32:41 +00:00
6364 changed files with 330547 additions and 147315 deletions
+21 -21
View File
@@ -106,24 +106,24 @@ Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
### 2. Create File Structure
**Create changelog file:**
- Path: `packages/twenty-website-new/src/content/releases/{VERSION}.mdx`
- Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
**Create image folder:**
- Path: `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
- Example for version 1.9.0: `packages/twenty-website-new/public/images/releases/1.9/`
- Example for version 2.0.0: `packages/twenty-website-new/public/images/releases/2.0/`
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
```bash
# Create the image folder
mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
```
### 3. Move Illustration Files
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
**Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
**Naming Convention:** `{VERSION}-descriptive-name.webp`
@@ -133,8 +133,8 @@ Examples:
```bash
# Move and rename source files, then convert to webp if needed
cp ~/Downloads/🆕/source-file.png packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
cd packages/twenty-website && node scripts/convert-png-to-webp.mjs
```
### 4. Research Features (if needed)
@@ -183,7 +183,7 @@ Description of the third feature.
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
**Reference Previous Changelogs:**
- Check `packages/twenty-website-new/src/content/releases/` for examples
- Check `packages/twenty-website/src/content/releases/` for examples
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
### 6. Review
@@ -191,10 +191,10 @@ Description of the third feature.
Open the changelog file for review:
```bash
# Open in Cursor
cursor packages/twenty-website-new/src/content/releases/{VERSION}.mdx
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
# Open image folder to verify illustrations
open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
```
Review checklist:
@@ -222,8 +222,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
[Show full MDX content]
Images moved to:
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
Please review the content. Once you approve, I'll commit the changes and create the pull request.
```
@@ -242,8 +242,8 @@ Possible user responses:
git status
# Add files
git add packages/twenty-website-new/src/content/releases/{VERSION}.mdx
git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
# Commit
git commit -m "Add {VERSION} release changelog"
@@ -266,7 +266,7 @@ This release includes:
- Feature 2
- Feature 3
Changelog file: \`packages/twenty-website-new/src/content/releases/{VERSION}.mdx\`
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
Release date: {DATE}" \
--base main \
--head {VERSION}
@@ -280,13 +280,13 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
- **Convention**: One file per complete version
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
- **Location**: `packages/twenty-website-new/src/content/releases/`
- **Location**: `packages/twenty-website/src/content/releases/`
### Image Folders
- **Format**: `{MAJOR}.{MINOR}/`
- **Convention**: One folder per minor version (shared across patches)
- **Examples**: `1.6/`, `1.7/`, `2.0/`
- **Location**: `packages/twenty-website-new/public/images/releases/`
- **Location**: `packages/twenty-website/public/images/releases/`
### Image Files
- **Format**: `{VERSION}-descriptive-name.webp`
@@ -311,8 +311,8 @@ Features to document:
3. ___________________________
Branch name: {VERSION}
Changelog path: packages/twenty-website-new/src/content/releases/{VERSION}.mdx
Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
```
## Tips
+72
View File
@@ -0,0 +1,72 @@
---
description: GitHub Actions security guidelines for supply chain protection
globs: **/.github/**/*.yml, **/.github/**/*.yaml
alwaysApply: false
---
# GitHub Actions Security
## Pin Third-Party Actions to Commit SHAs
Always reference external actions and reusable workflows by their full commit SHA, never by a mutable tag or branch. Tags can be force-pushed by a compromised maintainer account.
```yaml
# ❌ Mutable tag — vulnerable to supply chain attacks
uses: actions/checkout@v4
uses: actions/setup-node@v4
# ✅ Pinned to commit SHA with tag comment for readability
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
```
## Prefer `gh api` Over Third-Party Dispatch Actions
For repository dispatch calls, use `gh api` directly instead of third-party actions like `peter-evans/repository-dispatch`. This eliminates a supply-chain dependency entirely.
```yaml
# ✅ Use env vars + bracket notation to prevent injection
- name: Dispatch to target repo
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
gh api repos/org/repo/dispatches \
-f event_type=my-event \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[branch]=$BRANCH"
# ✅ Simple dispatch without payload
- name: Trigger workflow
env:
GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
run: |
gh api repos/org/repo/dispatches -f event_type=my-event
# ❌ Third-party action dependency
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DISPATCH_TOKEN }}
repository: org/repo
event-type: my-event
# ❌ Inline ${{ }} in shell — vulnerable to injection
- run: |
gh api repos/org/repo/dispatches --input - <<EOF
{"event_type": "x", "client_payload": {"branch": "${{ github.head_ref }}"}}
EOF
```
## Minimal Permissions
Always declare explicit `permissions` at the job level with the least privilege required. Never rely on the default `GITHUB_TOKEN` permissions.
```yaml
# ✅ Explicit minimal permissions
permissions:
contents: read
# ❌ Overly broad or implicit permissions
permissions: write-all
```
+6
View File
@@ -0,0 +1,6 @@
/.github/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/CODEOWNERS @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/workflows/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/actions/ @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.github/dependabot.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
/.yarnrc.yml @charlesBochet @FelixMalfait @Weiko @prastoin @bosiraphael @etiennejouan @ijreilly @martmull @thomtrp
+2 -2
View File
@@ -21,7 +21,7 @@ runs:
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty deploy --remote target
run: yarn twenty app:publish --private --remote target
@@ -21,7 +21,7 @@ runs:
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '${{ inputs.app-path }}/.nvmrc'
cache: yarn
@@ -50,4 +50,4 @@ runs:
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty install --remote target
run: yarn twenty app:install --remote target
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
run: git fetch origin main --depth=1
- name: Get last successful commit
if: env.NX_BASE == ''
uses: nrwl/nx-set-shas@v4
uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4
- name: Fallback to origin/main if no base found
if: env.NX_BASE == ''
shell: bash
+1 -1
View File
@@ -25,7 +25,7 @@ runs:
run: |
echo "CACHE_PRIMARY_KEY_PREFIX=v4-${CACHE_KEY}-${REF_NAME}" >> "${GITHUB_OUTPUT}"
- name: Restore cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
id: restore-cache
with:
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
+4 -1
View File
@@ -9,8 +9,11 @@ inputs:
runs:
using: "composite"
steps:
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
uses: actions/cache/save@v4
if: ${{ format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: ${{ inputs.key }}
path: |
@@ -48,7 +48,7 @@ runs:
fi
- name: Checkout docker compose files
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
repository: ${{ inputs.twenty-repository }}
ref: ${{ steps.resolve.outputs.git-ref }}
+7 -4
View File
@@ -29,12 +29,12 @@ runs:
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: Setup Node.js and get yarn cache
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
- name: Restore node_modules
id: cache-node-modules
uses: actions/cache/restore@v4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
with:
key: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
restore-keys: v4-${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
@@ -44,10 +44,13 @@ runs:
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
run: |
yarn config set enableHardenedMode true
yarn config set enableScripts false
yarn --immutable --check-cache
# Fork PRs on pull_request already can't write to the base repo's cache (GitHub built-in).
# The fork guard is defense-in-depth for pull_request_target, which does have write access.
- name: Save cache
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
uses: actions/cache/save@v4
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' && format('{0}', github.event.pull_request.head.repo.fork) != 'true' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
+2 -2
View File
@@ -1,5 +1,5 @@
#
# Crowdin CLI configuration for Website translations (twenty-website-new)
# Crowdin CLI configuration for Website translations (twenty-website)
# Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
@@ -16,7 +16,7 @@ files:
#
# Source file - PO file for Lingui
#
- source: packages/twenty-website-new/src/locales/en.po
- source: packages/twenty-website/src/locales/en.po
#
# Translation files path
#
+5 -6
View File
@@ -14,9 +14,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: auto-deploy-main
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f event_type=auto-deploy-main
+7 -6
View File
@@ -14,9 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: auto-deploy-tag
client-payload: '{"github": ${{ toJson(github) }}}' # Passes the entire github context to the downstream workflow
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f event_type=auto-deploy-tag \
-f "client_payload[github][ref_name]=$REF_NAME"
+2 -2
View File
@@ -21,11 +21,11 @@ jobs:
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
with:
files: ${{ inputs.files }}
+6 -7
View File
@@ -17,7 +17,7 @@ jobs:
NODE_OPTIONS: '--max-old-space-size=4096'
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
@@ -41,7 +41,7 @@ jobs:
- name: Create pull request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AI model catalog from models.dev'
@@ -61,8 +61,7 @@ jobs:
- name: Trigger automerge
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: automated-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=automated-pr-ready
+127 -15
View File
@@ -66,7 +66,7 @@ jobs:
steps:
- name: Checkout current branch
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
@@ -144,6 +144,9 @@ jobs:
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding current branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed current branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -160,13 +163,21 @@ jobs:
- name: Wait for current branch server to be ready
run: |
echo "Waiting for current branch server to start..."
timeout=300
timeout=60
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Current branch server is ready!"
break
fi
@@ -177,7 +188,7 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for current branch server to start"
echo "Timed out waiting for current branch server to serve a valid schema."
echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found"
exit 1
@@ -304,6 +315,9 @@ jobs:
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding main branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed main branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -324,9 +338,17 @@ jobs:
interval=5
elapsed=0
ADMIN_TOKEN=$(jq -r '.APPLE_JANE_ADMIN_ACCESS_TOKEN' packages/twenty-server/test/integration/constants/test-tokens.json)
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
GRAPHQL_RESPONSE=$(curl -s -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d '{"query":"{ __schema { queryType { name } } }"}' 2>/dev/null || echo '{}')
if echo "$GRAPHQL_RESPONSE" | jq -e '.data.__schema' > /dev/null 2>&1 && \
curl -fsS "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" > /dev/null 2>&1; then
echo "Main branch server is ready!"
break
fi
@@ -337,7 +359,7 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for main branch server to start"
echo "Timed out waiting for main branch server to serve a valid schema."
echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found"
exit 1
@@ -407,11 +429,23 @@ jobs:
valid=true
for file in main-schema-introspection.json current-schema-introspection.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json \
main-rest-api.json current-rest-api.json \
main-metadata-schema-introspection.json current-metadata-schema-introspection.json; do
if [ ! -f "$file" ]; then
echo "::warning::Missing GraphQL schema file: $file"
valid=false
elif ! jq -e '.data.__schema' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid GraphQL introspection result. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
for file in main-rest-api.json current-rest-api.json \
main-rest-metadata-api.json current-rest-metadata-api.json; do
if [ ! -f "$file" ] || ! jq empty "$file" 2>/dev/null; then
echo "::warning::Invalid or missing schema file: $file"
if [ ! -f "$file" ]; then
echo "::warning::Missing OpenAPI spec file: $file"
valid=false
elif ! jq -e '.openapi // .swagger' "$file" >/dev/null 2>&1; then
echo "::warning::File $file is not a valid OpenAPI spec. First 200 bytes: $(head -c 200 "$file")"
valid=false
fi
done
@@ -424,6 +458,7 @@ jobs:
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
id: graphql-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
@@ -437,6 +472,7 @@ jobs:
echo "✅ No changes in GraphQL schema"
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "core_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
@@ -454,6 +490,7 @@ jobs:
echo "✅ No changes in GraphQL metadata schema"
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "metadata_breaking=true" >> $GITHUB_OUTPUT
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
@@ -470,6 +507,7 @@ jobs:
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
id: rest-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
@@ -492,6 +530,7 @@ jobs:
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md
@@ -539,6 +578,7 @@ jobs:
fi
- name: Check REST Metadata API Breaking Changes
id: rest-metadata-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
@@ -561,6 +601,7 @@ jobs:
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report (only for breaking changes)
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
@@ -606,9 +647,82 @@ jobs:
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Fail on breaking changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
breaking=false
if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then
echo "❌ GraphQL core schema has breaking changes"
breaking=true
if [ -f graphql-schema-diff.md ]; then
echo ""
cat graphql-schema-diff.md
echo ""
fi
fi
if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then
echo "❌ GraphQL metadata schema has breaking changes"
breaking=true
if [ -f graphql-metadata-diff.md ]; then
echo ""
cat graphql-metadata-diff.md
echo ""
fi
fi
if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST core API has breaking changes"
breaking=true
if [ -f rest-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST metadata API has breaking changes"
breaking=true
if [ -f rest-metadata-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-metadata-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "$breaking" = "true" ]; then
echo ""
echo "This PR introduces breaking changes to the public API."
echo "If intentional, deprecate the old endpoint and introduce a new one."
echo "See the breaking changes report artifact and PR comment for details."
exit 1
fi
echo "✅ No breaking API changes detected"
- name: Upload breaking changes report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: breaking-changes-report
path: |
@@ -626,5 +740,3 @@ jobs:
if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true
fi
+44
View File
@@ -0,0 +1,44 @@
name: CI Codex Plugin
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-codex-plugin/**
.github/workflows/ci-codex-plugin.yaml
codex-plugin-validate:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Codex Plugin / Validate
run: npx nx run twenty-codex-plugin:validate
- name: Codex Plugin / Test
run: npx nx run twenty-codex-plugin:test
@@ -1,201 +0,0 @@
name: CI Hello world App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-hello-world:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx run $pkg:set-local-version --releaseVersion=$CI_VERSION
done
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute hello-world logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName hello-world-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-hello-world-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-hello-world-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-hello-world-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-hello-world]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
@@ -55,7 +55,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
@@ -105,7 +105,7 @@ jobs:
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
- name: Install scaffolded app dependencies
run: |
@@ -120,7 +120,7 @@ jobs:
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
const sdkVersion = pkg.devDependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
@@ -153,7 +153,7 @@ jobs:
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
@@ -1,199 +0,0 @@
name: CI Postcard App E2E
on:
# Temporarily disabled — will be re-enabled when example apps are published.
# push:
# branches:
# - main
#
# pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
packages/create-twenty-app/**
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
!packages/create-twenty-app/package.json
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
create-app-e2e-postcard:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest-4-cores
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
env:
PUBLISHABLE_PACKAGES: twenty-client-sdk twenty-sdk create-twenty-app
TWENTY_API_URL: http://localhost:3000
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Set CI version and prepare packages for publish
run: |
CI_VERSION="0.0.0-ci.$(date +%s)"
echo "CI_VERSION=$CI_VERSION" >> $GITHUB_ENV
npx nx run-many -t set-local-version -p $PUBLISHABLE_PACKAGES --releaseVersion=$CI_VERSION
- name: Build packages
run: |
for pkg in $PUBLISHABLE_PACKAGES; do
npx nx build $pkg
done
- name: Install and start Verdaccio
run: |
npx verdaccio --config .github/verdaccio-config.yaml &
for i in $(seq 1 30); do
if curl -s http://localhost:4873 > /dev/null 2>&1; then
echo "Verdaccio is ready"
break
fi
echo "Waiting for Verdaccio... ($i/30)"
sleep 1
done
- name: Publish packages to local registry
run: |
yarn config set npmRegistryServer http://localhost:4873
yarn config set unsafeHttpWhitelist --json '["localhost"]'
yarn config set npmAuthToken ci-auth-token
for pkg in $PUBLISHABLE_PACKAGES; do
cd packages/$pkg
yarn npm publish --tag ci
cd ../..
done
- name: Scaffold app using published create-twenty-app
run: |
npm install -g create-twenty-app@$CI_VERSION --registry http://localhost:4873
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance
- name: Install scaffolded app dependencies
run: |
cd /tmp/e2e-test-workspace/test-app
echo 'npmRegistryServer: "http://localhost:4873"' >> .yarnrc.yml
echo 'unsafeHttpWhitelist: ["localhost"]' >> .yarnrc.yml
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --no-immutable
echo "--- Installing last SDK versions ---"
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn add twenty-sdk twenty-client-sdk
- name: Verify installed app versions
run: |
cd /tmp/e2e-test-workspace/test-app
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
}
console.log('SDK version in scaffolded app:', sdkVersion);
"
- name: Verify SDK CLI is available
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty --version
- name: Setup server environment
run: npx nx reset:env:e2e-testing-server twenty-server
- name: Create databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Setup database
run: npx nx run twenty-server:database:reset
- name: Start server
run: nohup npx nx start:ci twenty-server &
- name: Wait for server to be ready
run: npx wait-on http://localhost:3000/healthz --timeout 120000 --interval 1000
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Deploy scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty deploy
- name: Install scaffolded app
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty install
- name: Execute postcard logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName postcard-logic-function)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q "Hello, World!"
- name: Execute create-postcard-company logic function
run: |
cd /tmp/e2e-test-workspace/test-app
EXEC_OUTPUT=$(npx --no-install twenty exec --functionName create-postcard-company)
echo "$EXEC_OUTPUT"
echo "$EXEC_OUTPUT" | grep -q 'Created company.*Hello World.*with id'
- name: Run scaffolded app integration test
run: |
cd /tmp/e2e-test-workspace/test-app
yarn test
ci-create-app-e2e-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, create-app-e2e-postcard]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
run: exit 1
+1 -5
View File
@@ -30,12 +30,8 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
+1 -6
View File
@@ -28,13 +28,8 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -54,7 +54,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
+28 -1
View File
@@ -23,9 +23,11 @@ jobs:
packages/twenty-sdk/**
packages/twenty-client-sdk/**
packages/twenty-shared/**
packages/twenty-server/**
!packages/twenty-sdk/package.json
!packages/twenty-client-sdk/package.json
!packages/twenty-shared/package.json
!packages/twenty-server/package.json
example-app-postcard:
needs: changed-files-check
@@ -54,7 +56,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
@@ -83,6 +85,31 @@ jobs:
working-directory: packages/twenty-apps/examples/postcard
run: npx vitest run
- name: Configure remote for SDK CLI
run: |
mkdir -p ~/.twenty
cat > ~/.twenty/config.json <<EOF
{
"version": 1,
"remotes": {
"target": {
"apiUrl": "${TWENTY_API_URL}",
"apiKey": "${TWENTY_API_KEY}",
"accessToken": "${TWENTY_API_KEY}"
}
},
"defaultRemote": "target"
}
EOF
- name: Deploy postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
- name: Install postcard app (registry install path)
working-directory: packages/twenty-apps/examples/postcard
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
ci-example-app-postcard-status-check:
if: always() && !cancelled()
timeout-minutes: 5
@@ -32,12 +32,8 @@ jobs:
matrix:
task: [build, typecheck, lint]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -50,12 +46,8 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -63,7 +55,7 @@ jobs:
- name: Build storybook
run: npx nx storybook:build twenty-front-component-renderer
- name: Upload storybook build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
@@ -76,7 +68,7 @@ jobs:
STORYBOOK_URL: http://localhost:6008
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -84,14 +76,24 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-sdk
- name: Download storybook build
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front-component-renderer
npx playwright install
npx playwright install chromium
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
+21 -23
View File
@@ -38,12 +38,8 @@ jobs:
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch local actions
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -55,7 +51,7 @@ jobs:
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
- name: Upload storybook build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-static
path: packages/twenty-front/storybook-static
@@ -79,7 +75,7 @@ jobs:
STORYBOOK_URL: http://localhost:6006
steps:
- name: Fetch local actions
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -96,14 +92,24 @@ jobs:
npx nx build twenty-ui
npx nx build twenty-front-component-renderer
- name: Download storybook build
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-static
path: packages/twenty-front/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front
npx playwright install
npx playwright install chromium
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Serve storybook & run tests
@@ -121,7 +127,7 @@ jobs:
# exit 1
# fi
# - name: Upload coverage artifact
# uses: actions/upload-artifact@v4
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# with:
# retention-days: 1
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
@@ -136,12 +142,12 @@ jobs:
# matrix:
# storybook_scope: [modules, pages, performance]
# steps:
# - uses: actions/checkout@v4
# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# with:
# fetch-depth: 10
# - name: Install dependencies
# uses: ./.github/actions/yarn-install
# - uses: actions/download-artifact@v4
# - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
# with:
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
# merge-multiple: true
@@ -164,12 +170,8 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -203,12 +205,8 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=10240"
ANALYZE: "true"
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -218,7 +216,7 @@ jobs:
- name: Build frontend
run: npx nx build twenty-front
# - name: Upload frontend build artifact
# uses: actions/upload-artifact@v4
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# with:
# name: frontend-build
# path: packages/twenty-front/build
+5 -5
View File
@@ -41,11 +41,11 @@ jobs:
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
@@ -53,7 +53,7 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Restore Nx build cache
uses: actions/cache/restore@v4
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (restore)
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
@@ -83,7 +83,7 @@ jobs:
- name: Save Nx build cache
if: always()
uses: actions/cache/save@v4
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 (save)
with:
key: v4-e2e-build-${{ github.ref_name }}-${{ github.sha }}
path: |
@@ -119,7 +119,7 @@ jobs:
- name: Upload Playwright results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-results
path: |
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.inputs.ref }}
@@ -47,7 +47,7 @@ jobs:
printf '%s\n' "$VERSION" > version.txt
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
branch: release/${{ steps.sanitize.outputs.version }}
commit-message: "chore: release v${{ steps.sanitize.outputs.version }}"
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
@@ -55,7 +55,7 @@ jobs:
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -6
View File
@@ -30,12 +30,8 @@ jobs:
matrix:
task: [lint, typecheck, test:unit, test:integration]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -74,7 +70,7 @@ jobs:
TWENTY_API_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ1c2VySWQiOiIyMDIwMjAyMC1lNmI1LTQ2ODAtOGEzMi1iODIwOTczNzE1NmIiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNDYzZi00MzViLTgyOGMtMTA3ZTAwN2EyNzExIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWU3Yy00M2Q5LWE1ZGItNjg1YjUwNjlkODE2IiwidHlwZSI6IkFDQ0VTUyIsImF1dGhQcm92aWRlciI6InBhc3N3b3JkIiwiaWF0IjoxNzUxMjgxNzA0LCJleHAiOjIwNjY4NTc3MDR9.HMGqCsVlOAPVUBhKSGlD1X86VoHKt4LIUtET3CGIdik
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
+7 -7
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -65,7 +65,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -83,12 +83,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Get changed upgrade-version-command files
id: changed-files
uses: tj-actions/changed-files@v45
uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
with:
files: |
packages/twenty-server/src/database/commands/upgrade-version-command/**
@@ -178,7 +178,7 @@ jobs:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -278,7 +278,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -341,7 +341,7 @@ jobs:
SHARD_COUNTER: 10
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
+1 -5
View File
@@ -29,12 +29,8 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -101,9 +101,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
+5 -13
View File
@@ -30,12 +30,8 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -48,12 +44,8 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -61,7 +53,7 @@ jobs:
- name: Build storybook
run: npx nx storybook:build twenty-ui
- name: Upload storybook build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
@@ -74,7 +66,7 @@ jobs:
STORYBOOK_URL: http://localhost:6007
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -82,7 +74,7 @@ jobs:
- name: Build dependencies
run: npx nx build twenty-shared
- name: Download storybook build
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: storybook-twenty-ui
path: packages/twenty-ui/storybook-static
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Utils / Run Danger.js
@@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run congratulate-dangerfile.js
+2 -6
View File
@@ -19,7 +19,7 @@ jobs:
files: |
package.json
yarn.lock
packages/twenty-website-new/**
packages/twenty-website/**
packages/twenty-shared/**
website-task:
needs: changed-files-check
@@ -32,12 +32,8 @@ jobs:
matrix:
task: [lint, typecheck, test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
+2 -6
View File
@@ -46,7 +46,7 @@ jobs:
- 6379:6379
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
@@ -97,12 +97,8 @@ jobs:
matrix:
task: [lint, typecheck, validate]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
with:
access_token: ${{ github.token }}
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
+45 -16
View File
@@ -8,10 +8,12 @@ on:
pull_request_review:
types: [submitted]
issues:
types: [opened, assigned]
types: [opened]
repository_dispatch:
types: [claude-core-team-issues]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.client_payload.issue_number }}
cancel-in-progress: false
@@ -19,10 +21,30 @@ concurrency:
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
(
github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
github.event.comment.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
) ||
(
github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
github.event.review.user.type != 'Bot' &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)
) ||
(
github.event_name == 'issues' &&
github.event.action == 'opened' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association)
)
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
@@ -50,14 +72,14 @@ jobs:
- 6379:6379
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Run Claude Code
id: claude-code
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
@@ -122,14 +144,14 @@ jobs:
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build prompt from dispatch payload
id: prompt
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const p = context.payload.client_payload;
@@ -144,7 +166,7 @@ jobs:
core.setOutput('issue_number', p.issue_number);
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@dde2242db6af13460b916652159b6ba19a598f30 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.prompt.outputs.prompt }}
@@ -159,9 +181,16 @@ jobs:
}
- name: Dispatch response to ci-privileged
if: always()
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: claude-cross-repo-response
client-payload: '{"repo": ${{ toJSON(steps.prompt.outputs.repo) }}, "issue_number": ${{ toJSON(steps.prompt.outputs.issue_number) }}, "run_id": ${{ toJSON(github.run_id) }}, "run_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
REPO: ${{ steps.prompt.outputs.repo }}
ISSUE_NUMBER: ${{ steps.prompt.outputs.issue_number }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=claude-cross-repo-response \
-f "client_payload[repo]=$REPO" \
-f "client_payload[issue_number]=$ISSUE_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[run_url]=$RUN_URL"
+5 -6
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
@@ -153,8 +153,7 @@ jobs:
- name: Trigger i18n automerge
if: github.event_name != 'pull_request' && steps.check_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: ${{ github.ref }}
@@ -36,7 +36,7 @@ jobs:
run: yarn docs:generate-navigation-template
- name: Upload docs to Crowdin
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: false
@@ -0,0 +1,28 @@
name: Auto-Draft External PRs
on:
pull_request_target:
types: [opened]
permissions: {}
jobs:
dispatch:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.author_association != 'MEMBER' &&
github.event.pull_request.author_association != 'OWNER' &&
github.event.pull_request.author_association != 'COLLABORATOR'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_NODE_ID: ${{ github.event.pull_request.node_id }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=convert-pr-to-draft \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_node_id]=$PR_NODE_ID"
+6 -7
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
@@ -69,7 +69,7 @@ jobs:
- name: Pull translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false
@@ -139,8 +139,7 @@ jobs:
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
+6 -7
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: main
@@ -80,7 +80,7 @@ jobs:
- name: Upload missing translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: true
@@ -105,8 +105,7 @@ jobs:
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
+14 -7
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Get PR number from workflow run
id: pr-info
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const runId = context.payload.workflow_run.id;
@@ -63,9 +63,16 @@ jobs:
- name: Dispatch to ci-privileged
if: steps.pr-info.outputs.has_pr == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: breaking-changes-report
client-payload: '{"pr_number": ${{ toJSON(steps.pr-info.outputs.pr_number) }}, "run_id": ${{ toJSON(steps.pr-info.outputs.run_id) }}, "repo": ${{ toJSON(github.repository) }}, "branch_state": ${{ toJSON(github.event.workflow_run.head_branch) }}}'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
RUN_ID: ${{ steps.pr-info.outputs.run_id }}
REPOSITORY: ${{ github.repository }}
BRANCH_STATE: ${{ github.event.workflow_run.head_branch }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=breaking-changes-report \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch_state]=$BRANCH_STATE"
@@ -0,0 +1,26 @@
name: PR Review Dispatch
on:
pull_request_target:
types: [ready_for_review, synchronize]
permissions: {}
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dispatch:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=pr-review \
-f "client_payload[pr_number]=$PR_NUMBER"
+13 -18
View File
@@ -1,7 +1,6 @@
name: 'Preview Environment Dispatch'
permissions:
contents: write
permissions: {}
on:
pull_request_target:
@@ -11,7 +10,6 @@ on:
- packages/twenty-server/**
- packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -35,18 +33,15 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Trigger preview environment workflow
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
event-type: preview-environment
client-payload: '{"pr_number": "${{ github.event.pull_request.number }}", "pr_head_sha": "${{ github.event.pull_request.head.sha }}", "repo_full_name": "${{ github.repository }}"}'
- name: Dispatch to ci-privileged for PR comment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: preview-env-url
client-payload: '{"pr_number": ${{ toJSON(github.event.pull_request.number) }}, "keepalive_dispatch_time": ${{ toJSON(github.event.pull_request.updated_at) }}, "repo": ${{ toJSON(github.repository) }}}'
- name: Dispatch preview-env to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-environment \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[repo]=$REPOSITORY"
@@ -1,139 +0,0 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding full dev workspace..."
if ! docker compose exec -T server yarn command:prod -- workspace:seed:dev; then
echo "❌ Seeding full dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@v4
with:
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Determine project and artifact name
id: project
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const workflowName = context.payload.workflow_run.name;
@@ -43,7 +43,7 @@ jobs:
- name: Check if storybook artifact exists
id: check-artifact
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const artifactName = '${{ steps.project.outputs.artifact_name }}';
@@ -65,7 +65,7 @@ jobs:
- name: Get PR number
if: steps.check-artifact.outputs.exists == 'true'
id: pr-info
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const headBranch = context.payload.workflow_run.head_branch;
@@ -107,7 +107,7 @@ jobs:
- name: Download storybook artifact from triggering run
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static
@@ -120,7 +120,7 @@ jobs:
- name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }}
@@ -128,17 +128,20 @@ jobs:
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
repository: twentyhq/ci-privileged
event-type: visual-regression
client-payload: >-
{
"pr_number": "${{ steps.pr-info.outputs.pr_number }}",
"run_id": "${{ github.run_id }}",
"repo": "${{ github.repository }}",
"project": "${{ steps.project.outputs.project }}",
"branch": "${{ github.event.workflow_run.head_branch }}",
"commit": "${{ github.event.workflow_run.head_sha }}"
}
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
RUN_ID: ${{ github.run_id }}
REPOSITORY: ${{ github.repository }}
PROJECT: ${{ steps.project.outputs.project }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=visual-regression \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[project]=$PROJECT" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
+11 -12
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
@@ -54,7 +54,7 @@ jobs:
# Strict mode fails if there are missing website translations.
- name: Compile website translations
id: compile_translations_strict
run: npx nx run twenty-website-new:lingui:compile --strict
run: npx nx run twenty-website:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
@@ -66,13 +66,13 @@ jobs:
- name: Pull website translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false
download_translations: true
source: 'packages/twenty-website-new/src/locales/en.po'
translation: 'packages/twenty-website-new/src/locales/%locale%.po'
source: 'packages/twenty-website/src/locales/en.po'
translation: 'packages/twenty-website/src/locales/%locale%.po'
export_only_approved: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
@@ -101,9 +101,9 @@ jobs:
- name: Compile website translations
id: compile_translations
run: |
npx nx run twenty-website-new:lingui:compile
npx nx run twenty-website:lingui:compile
git status
git add packages/twenty-website-new/src/locales
git add packages/twenty-website/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -128,8 +128,7 @@ jobs:
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
+11 -12
View File
@@ -10,7 +10,7 @@ on:
push:
branches: ['main']
paths:
- 'packages/twenty-website-new/**'
- 'packages/twenty-website/**'
- '.github/crowdin-website.yml'
- '.github/workflows/website-i18n-push.yaml'
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: main
@@ -41,14 +41,14 @@ jobs:
run: npx nx build twenty-shared
- name: Extract website translations
run: npx nx run twenty-website-new:lingui:extract
run: npx nx run twenty-website:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website-new/src/locales
git add packages/twenty-website/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -57,14 +57,14 @@ jobs:
fi
- name: Compile website translations
run: npx nx run twenty-website-new:lingui:compile
run: npx nx run twenty-website:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add packages/twenty-website-new/src/locales/generated
git add packages/twenty-website/src/locales/generated
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
@@ -78,7 +78,7 @@ jobs:
- name: Upload missing website translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: true
@@ -104,8 +104,7 @@ jobs:
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.TWENTY_INFRA_TOKEN }}
repository: twentyhq/twenty-infra
event-type: i18n-pr-ready
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
@@ -0,0 +1,69 @@
name: 'Website Preview Dispatch'
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, closed, labeled]
paths:
- packages/twenty-website/**
- .github/workflows/website-preview-dispatch.yaml
concurrency:
# Keyed on PR number so independent PRs don't cancel each other. `github.ref`
# would resolve to the base branch under pull_request and collide.
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
trigger-build:
# Same fork PRs from outside the org don't have `secrets.*` so the dispatch
# call would fail anyway — skip explicitly to avoid noise.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action != 'closed' && (
(github.event.action == 'labeled' && github.event.label.name == 'preview-website') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && contains(fromJSON('["opened","synchronize","reopened"]'), github.event.action)
)
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-build to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-build \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[pr_head_ref]=$PR_HEAD_REF"
trigger-cleanup:
# Covers both merge and close-without-merge — pull_request `closed` fires
# for both. PRs left open forever are covered by OpenNext's
# `maxVersionAgeDays: 14` + `maxNumberOfVersions: 50` auto-pruning in
# open-next.config.ts, so nothing leaks even if cleanup never runs.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action == 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-cleanup to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-cleanup \
-f "client_payload[pr_number]=$PR_NUMBER"
+1
View File
@@ -51,6 +51,7 @@ dump.rdb
mcp.json
/.junie/
/.agents/plugins/marketplace.json
TRANSLATION_QA_REPORT.md
.playwright-mcp/
.playwright-cli/
+27
View File
@@ -0,0 +1,27 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/.next/**",
"**/coverage/**",
"**/generated/**",
"**/generated-admin/**",
"**/generated-metadata/**",
"**/.cache/**",
"**/node_modules/**",
"**/*.min.js",
"**/*.snap",
"**/*.md",
"**/*.mdx",
"**/seed-project/**/*.mjs",
"packages/twenty-zapier/build/**",
"**/upgrade-version-command/**"
]
}
+40
View File
@@ -43,6 +43,46 @@
{
"name": "packages/twenty-e2e-testing",
"path": "../packages/twenty-e2e-testing"
},
{
"name": "packages/twenty-docs",
"path": "../packages/twenty-docs"
},
{
"name": "packages/create-twenty-app",
"path": "../packages/create-twenty-app"
},
{
"name": "packages/twenty-apps",
"path": "../packages/twenty-apps"
},
{
"name": "packages/twenty-claude-skills",
"path": "../packages/twenty-claude-skills"
},
{
"name": "packages/twenty-cli",
"path": "../packages/twenty-cli"
},
{
"name": "packages/twenty-client-sdk",
"path": "../packages/twenty-client-sdk"
},
{
"name": "packages/twenty-companion",
"path": "../packages/twenty-companion"
},
{
"name": "packages/twenty-front-component-renderer",
"path": "../packages/twenty-front-component-renderer"
},
{
"name": "packages/twenty-sdk",
"path": "../packages/twenty-sdk"
},
{
"name": "packages/twenty-website",
"path": "../packages/twenty-website"
}
],
"settings": {
+8
View File
@@ -4,6 +4,14 @@ enableHardenedMode: true
enableInlineHunks: true
enableScripts: false
nodeLinker: node-modules
npmMinimalAgeGate: 3d
npmPreapprovedPackages:
- twenty-sdk
- twenty-client-sdk
yarnPath: .yarn/releases/yarn-4.13.0.cjs
+1 -1
View File
@@ -110,7 +110,7 @@ packages/
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website-new/ # Next.js marketing website
├── twenty-website/ # Next.js marketing website
├── twenty-docs/ # Documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
+159
View File
@@ -0,0 +1,159 @@
# Twenty Website — DESIGN.md
> Visual system for the Twenty marketing site. Distilled from `packages/twenty-website/src/theme/`. Loaded by every `impeccable` invocation alongside PRODUCT.md.
## Theme
**Light by default.** A founder browsing a partner profile in daylight on a 1427 inch monitor is the default scene. The site does ship a `data-scheme="dark"` override (see `css-variables.ts`), but no current public page opts into it. Treat dark as a deferred surface.
## Color
Palette is OKLCH-equivalent neutrals at the surface level. The brand accents (blue, pink, yellow, green) are present in the token system but used sparingly — none of them appear on the partner pages.
### Strategy: Restrained
Tinted neutrals + one accent ≤10%. The accent for partner pages is the deep ink black (`var(--color-black-100)`) used in CTAs and hover states. Anything beyond a hairline border, an icon glyph, or a primary CTA should question whether it needs color at all.
### Tokens (from `src/theme/colors.ts` + `css-variables.ts`)
Neutrals (the workhorses):
| Token | Hex (computed) | Role |
| --- | --- | --- |
| `colors.primary.background[100]` | `#ffffff` | Page + card surface |
| `colors.primary.text[100]` | `#1c1c1c` | Headlines, primary text |
| `colors.primary.text[80]` | `#1c1c1ccc` | Body text |
| `colors.primary.text[60]` | `#1c1c1c99` | Eyebrows, meta, captions |
| `colors.primary.text[40]` | `#1c1c1c66` | Disabled / placeholder |
| `colors.primary.text[20]` | `#1c1c1c33` | Subtle separators |
| `colors.primary.text[10]` | `#1c1c1c1a` | Hairline borders |
| `colors.primary.text[5]` | `#1c1c1c0d` | Subtle fills (rates panel, skill chips) |
| `colors.primary.border[10]` | `#1c1c1c1a` | Default border |
| `colors.primary.border[20]` | `#1c1c1c33` | Hover border |
Reverse palette (for dark CTAs):
| Token | Role |
| --- | --- |
| `colors.secondary.background[100]` | Filled CTA background (deep ink) |
| `colors.secondary.text[100]` | Filled CTA text (white) |
Brand accents (currently absent from partner pages; available if needed):
- `colors.accent.blue``#4a38f5` / `#8174f8`
- `colors.accent.pink``#ed87fc` / `#f3abfd`
- `colors.accent.yellow``#feffb7` / `#feffd9`
- `colors.accent.green``#89fc9a` / `#b0fdbe`
- `colors.highlight` — same hue as blue accent
**Do not introduce gradients, glass blurs, or saturated fills on partner pages.** Color is conviction here, not decoration.
## Typography
Three families, each load-balanced via CSS variables:
| Family | Var | Use |
| --- | --- | --- |
| `theme.font.family.serif` | `--font-serif` | Headlines, partner names, headline values |
| `theme.font.family.sans` | `--font-sans` | Body, prose, interactive labels |
| `theme.font.family.mono` | `--font-mono` | Eyebrows, meta, currency labels, tabular numerics |
| `theme.font.family.retro` | `--font-retro` | Reserved (not used on partner pages) |
### Weight + Size Contrast
Weights: `light: 300`, `regular: 400`, `medium: 500`. No bold. Hierarchy is driven by scale and family contrast, never by weight alone.
Scale (`theme.font.size(n)``calc(var(--font-base) * n)`, where `--font-base: 0.25rem` ≈ 4px):
- Display / h1: size 912 (3648px)
- h2 / section heads: size 78 (2832px)
- h3 / card heads: size 56 (2024px)
- Body / prose: size 45 (1620px)
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.060.08em` and `text-transform: uppercase`
Body line length: cap at 6575ch (the existing `PartnerProfileIntro` uses `max-width: 62ch` — keep that order of magnitude).
### Hierarchy contract
- A serif `<h1>` at size 9 light reads as a partner's name on the detail page.
- A mono eyebrow above or below it locates the partner (region · city · country).
- A serif size 6 light reads as a section head.
- Body prose is sans regular.
- Currency values are serif (they read as headline numbers, not stats).
- Currency labels and meta are mono.
## Spacing & Layout
Base unit `4px`. Spacing helper `theme.spacing(n)` returns `n * 4px`. Common rhythms on the partner pages:
- Inter-section gap on the detail page: `theme.spacing(1014)` — generous, editorial breathing room.
- Inter-element gap inside a section: `theme.spacing(35)`.
- Card padding: `theme.spacing(6)`.
- Page horizontal padding: `theme.spacing(4)` mobile, `theme.spacing(10)` ≥ md breakpoint.
### Radius
`theme.radius(n)` returns `n * 2px`. The default card radius is `theme.radius(2)` = 4px. Pills use `999px`. No softer rounding than that.
### Borders
Borders are hairline (`1px solid theme.colors.primary.border[10]`). They define edges quietly. On hover they step to `border[20]`. Never use a chunky border as decoration.
## Components
### Card (PartnerCard, RatesPanel)
White surface, hairline border, 4px radius, 24px padding, soft shadow on hover only:
```css
background-color: ${theme.colors.primary.background[100]};
border: 1px solid ${theme.colors.primary.border[10]};
border-radius: ${theme.radius(2)};
padding: ${theme.spacing(6)};
&:hover {
border-color: ${theme.colors.primary.border[20]};
box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
```
### Chip / Pill
Rounded `999px`, 1px border, subtle background fill (`primary.text[5]` for filter pills, transparent for chip rows), `text[80]` color, mono or sans font.
### Button / LinkButton
Lives in `@/design-system/components`. Two color modes: `primary` (deep ink fill, white text) and `secondary` (transparent fill, ink text + 1px border). `variant="contained"` is what partner pages use.
### Avatar
`PartnerAvatar` is a deterministic generated mark from name + slug. Used as fallback when `profilePictureUrl` is missing. The real photo overlays it at 120px circle on the detail page, 56px on the list card.
## Motion
- Hover transitions: 250ms, ease-out (cubic-bezier curve in `PartnerCard`: `0.25s ease`).
- Card entrance: 700ms cubic-bezier `0.22, 1, 0.36, 1` (ease-out-quart), 90ms stagger per index.
- All motion respects `@media (prefers-reduced-motion: reduce)` — animations stop, hover translate disabled.
- **No bounce, no elastic, no parallax.** Editorial restraint.
## Iconography
`@tabler/icons-react`, 1416px on body-level chips, 1824px on buttons. Always `aria-hidden="true"` when decorative. Stroke width `2` (default).
## Accessibility Defaults
- Focus ring: `outline: 2px solid theme.colors.primary.text[100]; outline-offset: 4px` (already used on the card link).
- Touch target ≥ 40×40px on mobile.
- `aria-label` on icon-only buttons, `aria-labelledby` on sectioned regions.
- All `<a target="_blank">` includes `rel="noopener noreferrer"`.
- Color is never the sole carrier of meaning. The money pills carry both an icon and a text label.
## Anti-patterns (project-specific)
In addition to the impeccable shared absolute bans:
- **Do not use the brand accent colors (blue/pink/yellow/green) on partner pages** unless we have a stronger reason than "to add color".
- **No skeuomorphic shadows on cards.** The hover shadow is `0 12px 32px -16px rgba(0,0,0,0.18)` — that's the ceiling.
- **No gradients on anything.** Including text, borders, and backgrounds.
- **No floating "Trusted by" logo bars** on partner pages.
+80
View File
@@ -0,0 +1,80 @@
# Twenty Website — Product & Brand Context
> Strategic context for design work on the Twenty marketing site (`packages/twenty-website`). Loaded by every `impeccable` invocation.
## Register
**Brand.** The marketing site is a public-facing surface where the design itself is part of the credibility argument. Prospects evaluate Twenty partly by how the site feels. The product app (`packages/twenty-front`) is a separate product-register surface, governed elsewhere.
## Users & Purpose
The primary audience varies by route, but the working assumption for partner-related pages is:
- **Who:** A budget-holding decision maker (founder, RevOps lead, or COO) shopping for a CRM implementation partner. Already on Twenty's site, evaluating a shortlist of partners.
- **Context:** Doing a side-by-side comparison across 25 candidates over a single browsing session. Will spend 3090 seconds on each profile before deciding whether to book a call.
- **Decision being made:** "Is this partner credible, the right size, the right specialty, and within budget? Do I trust them enough to commit 30 minutes to a discovery call?"
What the partner pages must do, in priority order:
1. Communicate credibility (real firm, real person, real work).
2. Surface fit signals fast (skills, region, languages, deployment expertise, budget range).
3. Give the visitor a confident "next step" affordance (book a call or vet via LinkedIn) without pressure.
## Desired Outcome
The redesign should make `/partners/profile/[slug]` feel like a *thoughtfully curated profile of a top-tier partner*, not a generic templated card. A visitor should leave thinking "this firm is serious" even if they don't book a call this session.
Specifically:
- **Confidence over information density.** A short, well-typeset profile beats a packed-but-busy one.
- **Editorial restraint.** White space, deliberate type hierarchy, and a few well-chosen details say more than dozens of small components.
- **Quiet conviction.** No hype copy, no growth-hack patterns, no "Trusted by" logo strips. The partner's own work and intro speak for themselves.
## Brand Personality
**Editorial · Founder-led · Considered.**
The site reads like a thoughtful indie publication, not a SaaS landing page. Serif headlines, plenty of whitespace, deliberate typographic rhythm. Quietly opinionated — Twenty has a point of view about CRM (open-source, customizable, well-designed) and the site reflects that without shouting.
Tonal anchors:
- Stripe's documentation for clarity, Linear's marketing for restraint, an editorial print magazine for typography choices.
## Anti-references
**Reject these patterns. They make the work read as generic AI / generic SaaS:**
- **Generic SaaS landing.** Big-number heroes, identical icon-grid cards, gradient text, navy + lime accent color schemes, "supercharge your workflow" language.
- **Corporate enterprise tone.** Stock photos of diverse handshakes. "Trusted by Fortune 500" logo strips as the primary credibility move. Trust-badge bars.
- **Bento templates.** Repetitive same-size cards. Vercel-style scroll-pin animations on every section.
- **Side-stripe borders, gradient text, glassmorphism, hero-metric templates, identical card grids** — see impeccable's shared absolute bans.
## Strategic Design Principles
1. **Typography carries the design.** The brand has a serif/sans/mono trio. Hierarchy is set by scale + weight contrast, not by color or borders.
2. **Restrained palette.** Tinted neutrals (black/white via CSS variables, with alpha-tone variants for text and borders) carry 90%+ of the surface. Accent color used sparingly when it appears at all.
3. **Whitespace is a feature.** Tight cards feel cheap. Pages should breathe.
4. **Asymmetry over grid.** A 12-col bento is the wrong shape for a profile page. Use asymmetric two-column layouts where one column does heavy lifting.
5. **One opinionated detail per page.** Each surface should have one moment of editorial conviction (a typographic flourish, a precise micro-interaction, a deliberate space) rather than five generic flourishes.
## Accessibility
**WCAG AA + keyboard + screen reader baseline:**
- All interactive elements reachable by keyboard, focus visible (`outline: 2px solid`, not just color shift).
- Semantic landmarks: `<header>`, `<main>`, `<nav>`, `<section aria-labelledby=…>`, headings in order.
- All images with informational content have alt text. Decorative icons have `aria-hidden="true"`.
- Body text ≥ 4.5:1 contrast; large text (≥18pt or 14pt bold) ≥ 3:1.
- Respect `prefers-reduced-motion`. Animations stop, don't slow.
- Forms have explicit labels. Errors are announced.
## Tech & Constraints
- Next.js 16 app router (Server Components by default, `'use client'` for interactivity).
- Linaria styled-components (`@linaria/react`) for zero-runtime CSS-in-JS.
- Lingui (`@lingui/react`) for i18n; never hardcode user-visible strings.
- Theme tokens in `packages/twenty-website/src/theme/`. Colors are CSS variables resolved to OKLCH-tinted neutrals.
- `@tabler/icons-react` for iconography (no Heroicons, no custom SVGs unless purposeful).
- `@radix-ui/react-*` for primitives (Popover etc) where headless behavior is needed.
## Out of Scope for This File
- Detailed visual tokens (colors, type scale, motion specs) live in `DESIGN.md`.
- Per-page IA decisions live in shape briefs (`docs/superpowers/specs/`).
+44 -44
View File
@@ -1,19 +1,19 @@
<p align="center">
<a href="https://www.twenty.com">
<img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
</a>
</p>
<h2 align="center" >The #1 Open-Source CRM</h2>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website-new/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website-new/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
<p align="center">
<a href="https://www.twenty.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/github-cover-light.png" alt="Twenty banner" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.webp" />
<img src="./packages/twenty-website/public/images/readme/github-cover-light.webp" alt="Twenty banner" />
</picture>
</a>
</p>
@@ -24,17 +24,17 @@
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
<a href="https://twenty.com/why-twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
<br />
# Installation
### <img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
### <img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
Scaffold a new app with the Twenty CLI:
@@ -63,12 +63,12 @@ export default defineObject({
Then ship it to your workspace:
```bash
npx twenty deploy
npx twenty app:publish --private
```
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
### <img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
@@ -79,61 +79,61 @@ Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
<table align="center">
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.png" alt="Create your apps" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-version-control-light.png" alt="Stay on top with version control" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website-new/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.png" alt="All the tools you need to build anything" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" />
</picture>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-tools-light.png" alt="Customize your layouts" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td>
</tr>
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.png" alt="AI agents and chats" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
</td>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.png" alt="Plus all the tools of a good CRM" />
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.webp" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" />
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" />
</picture>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td>
</tr>
</table>
@@ -142,23 +142,23 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Stack
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website-new/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website-new/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website-new/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website-new/public/images/readme/chromatic.png" height="28" alt="Chromatic" /></a>
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website-new/public/images/readme/greptile.png" height="28" alt="Greptile" /></a>
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website-new/public/images/readme/sentry.png" height="28" alt="Sentry" /></a>
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://crowdin.com/"><img src="./packages/twenty-website-new/public/images/readme/crowdin.png" height="28" alt="Crowdin" /></a>
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
@@ -166,4 +166,4 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Join the Community
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website-new/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website-new/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website-new/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

+10 -13
View File
@@ -44,12 +44,12 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
}
},
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
@@ -57,13 +57,14 @@
"lint:diff-with-main": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": ["twenty-oxlint-rules:build"],
"options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)"
}
}
},
@@ -72,18 +73,14 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "prettier {args.files} --check --cache {args.cache} --cache-location {args.cacheLocation} --write {args.write} --cache-strategy {args.cacheStrategy}",
"cache": true,
"cacheLocation": "../../.cache/prettier/{projectRoot}",
"cacheStrategy": "metadata",
"write": false
"command": "npx oxfmt --check {args.files} {args.write}",
"files": ".",
"write": ""
},
"configurations": {
"ci": {
"cacheStrategy": "content"
},
"ci": {},
"fix": {
"write": true
"command": "npx oxfmt {args.files}"
}
},
"dependsOn": ["^build"]
+6 -4
View File
@@ -13,6 +13,7 @@
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"nx": "22.5.4",
"oxfmt": "0.50.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1"
},
@@ -27,7 +28,7 @@
"resolutions": {
"graphql": "16.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"typescript": "5.9.3",
"nodemailer": "8.0.4",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"@lingui/core": "5.1.2",
@@ -53,18 +54,19 @@
"packages/twenty-ui",
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website-new",
"packages/twenty-website",
"packages/twenty-docs",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
"packages/twenty-sdk",
"packages/twenty-front-component-renderer",
"packages/twenty-client-sdk",
"packages/twenty-apps",
"packages/twenty-cli",
"packages/create-twenty-app",
"packages/twenty-codex-plugin",
"packages/twenty-oxlint-rules",
"packages/twenty-companion"
"packages/twenty-companion",
"packages/twenty-claude-skills"
]
},
"prettier": {
+16 -24
View File
@@ -1,7 +1,7 @@
<div align="center">
<a href="https://twenty.com">
<picture>
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website-new/public/images/core/logo.svg" height="128">
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website/public/images/core/logo.svg" height="128">
</picture>
</a>
<h1>Create Twenty App</h1>
@@ -25,39 +25,31 @@ yarn twenty dev
The scaffolder will:
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
2. Optionally start a local Twenty server (Docker)
3. Open the browser for OAuth authentication
2. Start a local Twenty server via Docker (pulls the latest image automatically)
3. Authenticate with the development API key
## Options
| Flag | Description |
| ------------------------------ | --------------------------------------- |
| `--example <name>` | Initialize from an example |
| `--name <name>` | Set the app name (skips the prompt) |
| `--display-name <displayName>` | Set the display name (skips the prompt) |
| `--description <description>` | Set the description (skips the prompt) |
| `--skip-local-instance` | Skip the local server setup prompt |
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
```bash
npx create-twenty-app@latest my-twenty-app --example hello-world
```
Examples are sourced from [twentyhq/twenty/packages/twenty-apps/examples](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples).
| Flag | Description |
| ---------------------------------- | --------------------------------------------------------------------- |
| `--name <name>` | Set the app name |
| `--display-name <displayName>` | Set the display name |
| `--description <description>` | Set the description |
| `--url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
## Documentation
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started)**:
Full documentation is available at **[docs.twenty.com/developers/extend/apps](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)**:
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started) — step-by-step setup, project structure, server management, CI
- [Building Apps](https://docs.twenty.com/developers/extend/apps/building) — entity definitions, API clients, testing
- [Publishing](https://docs.twenty.com/developers/extend/apps/publishing) — deploy, npm publish, marketplace
- [Quick Start](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start) — scaffold, run a local server, sync your code
- [Concepts](https://docs.twenty.com/developers/extend/apps/getting-started/concepts) — how apps work: entity model, sandboxing, lifecycle
- [Operations](https://docs.twenty.com/developers/extend/apps/operations/overview) — CLI, testing, CI, deploy and publish
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty docker:logs`.
- Auth not working: run `yarn twenty remote:add --local` to re-authenticate.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+2 -4
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.3.0",
"version": "2.9.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -32,7 +32,6 @@
"chalk": "^5.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"inquirer": "^10.0.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
@@ -43,7 +42,6 @@
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.7",
@@ -52,7 +50,7 @@
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
+36 -20
View File
@@ -1,7 +1,10 @@
#!/usr/bin/env node
import chalk from 'chalk';
import { Command, CommanderError } from 'commander';
import { CreateAppCommand } from '@/create-app.command';
import {
type AuthenticationMethod,
CreateAppCommand,
} from '@/create-app.command';
import packageJson from '../package.json';
const program = new Command(packageJson.name)
@@ -12,32 +15,26 @@ const program = new Command(packageJson.name)
'Output the current version of create-twenty-app.',
)
.argument('[directory]')
.option('--example <name>', 'Initialize from an example')
.option('-n, --name <name>', 'Application name (skips prompt)')
.option('-n, --name <name>', 'Application name')
.option('-d, --display-name <displayName>', 'Application display name')
.option('--description <description>', 'Application description')
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option(
'-d, --display-name <displayName>',
'Application display name (skips prompt)',
'--authentication-method <method>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
)
.option(
'--description <description>',
'Application description (skips prompt)',
)
.option(
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
)
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
directory?: string,
options?: {
example?: string;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
url?: string;
apiUrl?: string;
authenticationMethod?: AuthenticationMethod;
},
) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -54,14 +51,33 @@ const program = new Command(packageJson.name)
process.exit(1);
}
if (
options?.authenticationMethod &&
!['oauth', 'apiKey'].includes(options.authenticationMethod)
) {
console.error(
chalk.red(
'Error: --authentication-method must be "oauth" or "apiKey".',
),
);
process.exit(1);
}
if (options?.apiUrl) {
console.warn(
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
);
}
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
await new CreateAppCommand().execute({
directory,
example: options?.example,
name: options?.name,
displayName: options?.displayName,
description: options?.description,
skipLocalInstance: options?.skipLocalInstance,
yes: options?.yes,
serverUrl,
authenticationMethod: options?.authenticationMethod,
});
},
);
@@ -0,0 +1,67 @@
## Base documentation
- Getting started:
- https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md
- https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md
- https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md
- https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md
- https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md
- https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md
- Config:
- https://docs.twenty.com/developers/extend/apps/config/overview.md
- https://docs.twenty.com/developers/extend/apps/config/application.md
- https://docs.twenty.com/developers/extend/apps/config/roles.md
- https://docs.twenty.com/developers/extend/apps/config/install-hooks.md
- https://docs.twenty.com/developers/extend/apps/config/public-assets.md
- Data:
- https://docs.twenty.com/developers/extend/apps/data/overview.md
- https://docs.twenty.com/developers/extend/apps/data/objects.md
- https://docs.twenty.com/developers/extend/apps/data/extending-objects.md
- https://docs.twenty.com/developers/extend/apps/data/relations.md
- Logic:
- https://docs.twenty.com/developers/extend/apps/logic/overview.md
- https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md
- https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md
- https://docs.twenty.com/developers/extend/apps/logic/connections.md
- Layout:
- https://docs.twenty.com/developers/extend/apps/layout/overview.md
- https://docs.twenty.com/developers/extend/apps/layout/views.md
- https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md
- https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md
- https://docs.twenty.com/developers/extend/apps/layout/front-components.md
- https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md
- Operations:
- https://docs.twenty.com/developers/extend/apps/operations/overview.md
- https://docs.twenty.com/developers/extend/apps/operations/cli.md
- https://docs.twenty.com/developers/extend/apps/operations/testing.md
- https://docs.twenty.com/developers/extend/apps/operations/publishing.md
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
## UUID requirement
- All generated UUIDs must be valid UUID v4.
## Common Pitfalls
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
## Best practice
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
This helps automatically generate required IDs etc.
@@ -2,10 +2,21 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw
## Getting Started
This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020).
Login with the default development credentials: `tim@apple.dev` / `tim@apple.dev`.
Run `yarn twenty help` to list all available commands.
## Useful Commands
- `yarn twenty dev` - Start the development server and sync your app
- `yarn twenty docker:status` - Check the local Twenty server status
- `yarn twenty docker:start` - Start the local Twenty server
- `yarn test` - Run integration tests
## Learn More
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started)
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)
- [twenty-sdk CLI reference](https://www.npmjs.com/package/twenty-sdk)
- [Discord](https://discord.gg/cx5n4Jzs57)
@@ -11,7 +11,7 @@ permissions:
contents: read
env:
TWENTY_DEPLOY_URL: http://localhost:3000
TWENTY_DEPLOY_URL: http://localhost:2020
concurrency:
group: cd-${{ github.ref }}
@@ -17,8 +17,7 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "TO-BE-GENERATED",
"twenty-sdk": "TO-BE-GENERATED"
"twenty-client-sdk": "TO-BE-GENERATED"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -26,6 +25,7 @@
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"twenty-sdk": "TO-BE-GENERATED",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
<rect width="80" height="80" rx="16" fill="#141414"/>
<rect x="20" y="20" width="16" height="16" rx="4" fill="#FAFAFA"/>
<rect x="44" y="20" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="20" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.6"/>
<rect x="44" y="44" width="16" height="16" rx="4" fill="#FAFAFA" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 454 B

@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}
@@ -4,12 +4,10 @@ import {
APP_DESCRIPTION,
APP_DISPLAY_NAME,
APPLICATION_UNIVERSAL_IDENTIFIER,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
@@ -2,3 +2,10 @@ export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
export const APP_DESCRIPTION = 'DESCRIPTION-TO-BE-GENERATED';
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER = 'UUID-TO-BE-GENERATED';
export const MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =
'UUID-TO-BE-GENERATED';
@@ -1,11 +1,11 @@
import { defineRole } from 'twenty-sdk/define';
import { defineApplicationRole } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineRole({
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: `${APP_DISPLAY_NAME} default function role`,
description: `${APP_DISPLAY_NAME} default function role`,
@@ -0,0 +1,291 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
Avatar,
IconBox,
IconHierarchy,
IconLayout,
IconSettingsAutomation,
} from 'twenty-sdk/ui';
import { useState } from 'react';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
const DOCS_BASE_URL = 'https://docs.twenty.com/developers/extend/apps';
const CATEGORIES = [
{
title: 'Data model',
color: '#73D08D',
items: [
{ label: 'CUSTOM OBJECT', href: `${DOCS_BASE_URL}/data/objects` },
{
label: 'CUSTOM FIELDS',
href: `${DOCS_BASE_URL}/data/extending-objects`,
},
],
rotation: '2.4deg',
},
{
title: 'Logic',
color: '#F4D345',
items: [
{
label: 'TOOLS',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'LOGIC FUNCTION',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'SKILLS',
href: `${DOCS_BASE_URL}/logic/skills-and-agents`,
},
],
rotation: '0deg',
},
{
title: 'Layout',
color: '#C4A2E0',
items: [
{ label: 'VIEWS', href: `${DOCS_BASE_URL}/layout/views` },
{ label: 'WIDGETS', href: `${DOCS_BASE_URL}/layout/page-layouts` },
{
label: 'LAYOUT PAGES',
href: `${DOCS_BASE_URL}/layout/page-layouts`,
},
{
label: 'COMMANDS',
href: `${DOCS_BASE_URL}/layout/command-menu-items`,
},
],
rotation: '-2.8deg',
},
] as const;
const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path
d="M4.5 3.5H10.5V9.5M10.5 3.5L3.5 10.5"
stroke={color}
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const CategoryCard = ({
title,
color,
items,
rotation,
}: {
title: string;
color: string;
items: ReadonlyArray<{ label: string; href: string }>;
rotation: string;
}) => {
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const CategoryIcon = () => {
if (title === 'Data model') {
return <IconHierarchy color={color} size={'20px'} />;
}
if (title === 'Logic') {
return <IconSettingsAutomation color={color} size={'20px'} />;
}
if (title === 'Layout') {
return <IconLayout color={color} size={'20px'} />;
}
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
border: `1px solid ${color}80`,
borderRadius: '12px',
overflow: 'hidden',
width: '240px',
background: '#FFFFFF',
transform: `rotate(${rotation})`,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.04)',
}}
>
<div
style={{
padding: '16px 20px',
background: `${color}22`,
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<CategoryIcon />
<span
style={{
fontSize: '16px',
fontWeight: 600,
color: color,
}}
>
{title}
</span>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '8px',
gap: '4px',
}}
>
{items.map((item) => {
const isHovered = hoveredItem === item.label;
return (
<a
key={item.label}
href={item.href}
target="_blank"
rel="noopener noreferrer"
onMouseEnter={() => setHoveredItem(item.label)}
onMouseLeave={() => setHoveredItem(null)}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
textDecoration: 'none',
cursor: 'pointer',
padding: '10px 12px',
borderRadius: '8px',
background: isHovered ? '#0000000A' : 'transparent',
transition: 'background 0.15s',
}}
>
<IconBox color={color} size={'20px'} />
<span
style={{
fontSize: '13px',
fontWeight: 300,
color: '#333',
letterSpacing: '0.5px',
flex: 1,
}}
>
{item.label}
</span>
{isHovered && <ArrowUpRight />}
</a>
);
})}
</div>
</div>
);
};
const MainPage = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
gap: '8px',
padding: '40px',
}}
>
<Avatar
placeholder={APP_DISPLAY_NAME}
placeholderColorSeed={APP_DISPLAY_NAME}
size="xl"
/>
<span
style={{
fontSize: '24px',
fontWeight: 600,
color: '#333',
marginTop: '8px',
}}
>
{APP_DISPLAY_NAME}
</span>
<span
style={{
fontSize: '13px',
color: '#888',
textAlign: 'center',
lineHeight: '1.5',
}}
>
Was installed successfully.
<br />
You can now add content to your app.
</span>
<a
href="/settings/applications#installed"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
marginTop: '16px',
fontSize: '13px',
color: '#333',
textDecoration: 'none',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
background: '#fafafa',
transition: 'background 0.15s, border-color 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#f0f0f0';
e.currentTarget.style.borderColor = '#ccc';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#fafafa';
e.currentTarget.style.borderColor = '#e0e0e0';
}}
>
Open app settings
<ArrowUpRight color="#333" />
</a>
<div
style={{
display: 'flex',
gap: '16px',
marginTop: '32px',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'flex-start',
}}
>
{CATEGORIES.map((category) => (
<CategoryCard
key={category.title}
title={category.title}
color={category.color}
items={category.items}
rotation={category.rotation}
/>
))}
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
description: `${APP_DISPLAY_NAME} front component displaying the app logo and name`,
component: MainPage,
});
@@ -0,0 +1,19 @@
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default defineNavigationMenuItem({
universalIdentifier: MAIN_PAGE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
icon: 'IconFile',
position: -1,
type: NavigationMenuItemType.PAGE_LAYOUT,
pageLayoutUniversalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
});
@@ -0,0 +1,37 @@
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
import {
APP_DISPLAY_NAME,
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
} from 'src/constants/universal-identifiers';
export default definePageLayout({
universalIdentifier: MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
name: APP_DISPLAY_NAME,
type: 'STANDALONE_PAGE',
tabs: [
{
universalIdentifier: MAIN_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
title: 'Overview',
position: 0,
icon: 'IconApps',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [
{
universalIdentifier: MAIN_PAGE_WIDGET_UNIVERSAL_IDENTIFIER,
title: ' ',
type: 'FRONT_COMPONENT',
gridPosition: { row: 0, column: 0, rowSpan: 12, columnSpan: 12 },
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier:
MAIN_PAGE_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
},
},
],
},
],
});
@@ -1,96 +1,172 @@
import { copyBaseApplicationProject } from '@/utils/app-template';
import { downloadExample } from '@/utils/download-example';
import { convertToLabel } from '@/utils/convert-to-label';
import { install } from '@/utils/install';
import { tryGitInit } from '@/utils/try-git-init';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import kebabCase from 'lodash.kebabcase';
import * as path from 'path';
import { basename } from 'path';
import { spawn } from 'node:child_process';
import {
authLogin,
authLoginOAuth,
checkDockerRunning,
ConfigService,
containerExists,
detectLocalServer,
DEV_API_KEY,
DEV_API_URL,
serverStart,
type ServerStartResult,
} from 'twenty-sdk/cli';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import {
getDockerInstallInstructions,
isDockerInstalled,
} from '@/utils/docker-install';
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
const IMAGE = 'twentycrm/twenty-app-dev:latest';
export type AuthenticationMethod = 'oauth' | 'apiKey';
type CreateAppOptions = {
directory?: string;
example?: string;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
yes?: boolean;
serverUrl?: string;
authenticationMethod?: AuthenticationMethod;
};
export class CreateAppCommand {
private stepCounter = 0;
private totalSteps = 0;
async execute(options: CreateAppOptions = {}): Promise<void> {
const { appName, appDisplayName, appDirectory, appDescription } =
await this.getAppInfos(options);
this.getAppInfos(options);
const serverUrl = options.serverUrl ?? DEV_API_URL;
const skipLocalInstance = serverUrl !== DEV_API_URL;
if (!skipLocalInstance && !isDockerInstalled()) {
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
process.exit(1);
}
if (skipLocalInstance && options.authenticationMethod === 'apiKey') {
console.log(
chalk.yellow(
'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.',
),
);
}
const authenticationMethod = skipLocalInstance
? 'oauth'
: (options.authenticationMethod ?? 'apiKey');
try {
await this.validateDirectory(appDirectory);
this.logCreationInfo({ appDirectory, appName });
this.totalSteps = this.computeTotalSteps({
skipLocalInstance,
});
this.stepCounter = 0;
const dockerPullPromise =
!skipLocalInstance && checkDockerRunning()
? this.pullImageInBackground()
: Promise.resolve(false);
this.logPlan({ appName, appDisplayName, appDescription, appDirectory });
this.logNextStep('Creating project directory');
await fs.ensureDir(appDirectory);
if (options.example) {
const exampleSucceeded = await this.tryDownloadExample(
options.example,
appDirectory,
);
this.logDetail(appDirectory);
if (!exampleSucceeded) {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
this.logNextStep('Scaffolding project files');
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
onProgress: (message) => this.logDetail(message),
});
this.logNextStep('Installing dependencies');
await install(appDirectory, (message) => this.logDetail(message));
this.logNextStep('Initializing Git repository');
const gitInitialized = await tryGitInit(appDirectory);
if (gitInitialized) {
this.logDetail('Initialized on branch main');
this.logDetail('Created initial commit');
} else {
this.logDetail(
'Skipped (Git unavailable, initialization failed, or already in a repository)',
);
}
console.log('');
let authSucceeded = false;
let resolvedServerUrl = serverUrl;
let serverReady = skipLocalInstance;
if (!skipLocalInstance) {
this.logNextStep('Starting Twenty server');
const serverResult = await this.ensureDockerServer(dockerPullPromise);
if (isDefined(serverResult.url)) {
resolvedServerUrl = serverResult.url;
serverReady = true;
}
}
if (serverReady) {
this.logNextStep('Authenticating');
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
if (authSucceeded) {
this.logDetail('Reusing existing credentials');
} else if (authenticationMethod === 'oauth') {
this.logDetail('Starting OAuth flow');
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
} else {
this.logDetail('Using development API key');
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
}
}
this.logNextStep('Installing application');
let syncSucceeded = false;
if (serverReady && authSucceeded) {
syncSucceeded = await this.syncApplication(appDirectory);
if (!syncSucceeded) {
this.logDetail('Sync failed. Run `yarn twenty dev --once` manually.');
return;
}
} else {
await copyBaseApplicationProject({
appName,
appDisplayName,
appDescription,
appDirectory,
});
this.logDetail('Skipped (server or authentication not available)');
}
await install(appDirectory);
await tryGitInit(appDirectory);
let serverResult: ServerStartResult | undefined;
if (!options.skipLocalInstance) {
const shouldStartServer = await this.shouldStartServer(options.yes);
if (shouldStartServer) {
const startResult = await serverStart({
onProgress: (message: string) => console.log(chalk.gray(message)),
});
if (startResult.success) {
serverResult = startResult.data;
await this.promptConnectToLocal(serverResult.url);
} else {
console.log(chalk.yellow(`\n${startResult.error.message}`));
}
}
if (syncSucceeded) {
await this.openMainPage(appDirectory, resolvedServerUrl);
}
this.logSuccess(appDirectory, serverResult);
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
} catch (error) {
console.error(
chalk.red('\nCreate application failed:'),
@@ -100,66 +176,42 @@ export class CreateAppCommand {
}
}
private async getAppInfos(options: CreateAppOptions): Promise<{
private computeTotalSteps({
skipLocalInstance,
}: {
skipLocalInstance: boolean;
}): number {
let steps = 4; // directory, scaffold, install, git
if (!skipLocalInstance) {
steps += 1; // start server
}
steps += 1; // authenticate (oauth or apiKey)
steps += 1; // sync application
return steps;
}
private getAppInfos(options: CreateAppOptions): {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}> {
const { directory } = options;
const hasName = isDefined(options.name) || isDefined(directory);
const hasDisplayName = isDefined(options.displayName);
const hasDescription = isDefined(options.description);
const hasExample = isDefined(options.example);
const { name, displayName, description } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Application name:',
when: () => !hasName && !hasExample,
default: 'my-twenty-app',
validate: (input) => {
if (input.length === 0) return 'Application name is required';
return true;
},
},
{
type: 'input',
name: 'displayName',
message: 'Application display name:',
when: () => !hasDisplayName && !hasExample,
default: (answers: { name?: string }) => {
return convertToLabel(
answers?.name ?? options.name ?? directory ?? '',
);
},
},
{
type: 'input',
name: 'description',
message: 'Application description (optional):',
when: () => !hasDescription && !hasExample,
default: '',
},
]);
} {
const appName = (
options.name ??
name ??
directory ??
options.example ??
options.directory ??
'my-twenty-app'
).trim();
const appDisplayName =
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
options.displayName?.trim() || convertToLabel(appName);
const appDescription = (options.description ?? description ?? '').trim();
const appDescription = (options.description ?? '').trim();
const appDirectory = directory
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
const appDirectory = options.directory
? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory)
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
return { appName, appDisplayName, appDirectory, appDescription };
@@ -178,118 +230,333 @@ export class CreateAppCommand {
}
}
private async tryDownloadExample(
example: string,
appDirectory: string,
): Promise<boolean> {
try {
await downloadExample(example, appDirectory);
private logPlan({
appName,
appDisplayName,
appDescription,
appDirectory,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
}): void {
console.log(chalk.blue('\nCreating Twenty Application\n'));
console.log(chalk.white(` Name: ${appName}`));
console.log(chalk.white(` Display name: ${appDisplayName}`));
return true;
} catch (error) {
console.error(
chalk.red(
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
if (appDescription) {
console.log(chalk.white(` Description: ${appDescription}`));
}
console.log(chalk.white(` Directory: ${appDirectory}`));
console.log('');
}
private logNextStep(title: string): void {
this.stepCounter++;
console.log(
chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) +
chalk.white(` ${title}...`),
);
}
private logDetail(message: string): void {
console.log(chalk.gray(`${message}`));
}
private pullImageInBackground(): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' });
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async ensureDockerServer(
dockerPullPromise: Promise<boolean>,
): Promise<{ url?: string }> {
if (!checkDockerRunning()) {
console.log(
chalk.yellow(
'\n Docker is installed but not running.\n' +
' Please start Docker and run this command again.\n',
),
);
const { useTemplate } = await inquirer.prompt([
{
type: 'confirm',
name: 'useTemplate',
message: 'Would you like to create a default template app instead?',
default: true,
},
]);
return {};
}
if (!useTemplate) {
process.exit(1);
this.logDetail('Ensuring latest Twenty server image...');
const pullSucceeded = await dockerPullPromise;
if (!pullSucceeded) {
this.logDetail(
'Image pull failed, continuing with cached image if available...',
);
}
const startResult = await serverStart({
onProgress: (message: string) => this.logDetail(message),
});
if (startResult.success) {
return { url: startResult.data.url };
}
console.log(chalk.yellow(`\n ${startResult.error.message}`));
return {};
}
private async openMainPage(
appDirectory: string,
serverUrl: string,
): Promise<void> {
try {
const configService = new ConfigService();
const config = await configService.getConfig();
const token = config.twentyCLIAccessToken ?? config.apiKey;
if (!token) {
return;
}
// Clean up any partial files from the failed download
await fs.emptyDir(appDirectory);
const [universalIdentifier, frontUrl] = await Promise.all([
this.readMainPageLayoutUniversalIdentifier(appDirectory),
this.resolveWorkspaceFrontUrl(serverUrl, token),
]);
if (!universalIdentifier || !frontUrl) {
return;
}
const pageLayoutId = await this.resolvePageLayoutId(
serverUrl,
universalIdentifier,
token,
);
if (!pageLayoutId) {
return;
}
const url = `${frontUrl}/page/${pageLayoutId}`;
this.logDetail(`Opening app welcome page: ${url}`);
this.openInBrowser(url);
} catch {
// Best-effort — don't fail the scaffold if browser open fails
}
}
private async resolveWorkspaceFrontUrl(
serverUrl: string,
token: string,
): Promise<string | null> {
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
currentWorkspace?: {
workspaceUrls?: { subdomainUrl?: string; customUrl?: string };
};
};
};
const urls = body.data?.currentWorkspace?.workspaceUrls;
if (!urls) {
return null;
}
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
return frontUrl ? normalizeUrl(frontUrl) : null;
}
private async readMainPageLayoutUniversalIdentifier(
appDirectory: string,
): Promise<string | null> {
const filePath = path.join(
appDirectory,
'src',
'constants',
'universal-identifiers.ts',
);
const content = await fs.readFile(filePath, 'utf-8');
const match = content.match(
/MAIN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER\s*=\s*'([^']+)'/,
);
return match?.[1] ?? null;
}
private async resolvePageLayoutId(
serverUrl: string,
universalIdentifier: string,
token: string,
): Promise<string | null> {
const query = `{ getPageLayouts { id universalIdentifier } }`;
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
return null;
}
const body = (await response.json()) as {
data?: {
getPageLayouts?: { id: string; universalIdentifier: string }[];
};
};
const matching = body.data?.getPageLayouts?.find(
(layout) => layout.universalIdentifier === universalIdentifier,
);
return matching?.id ?? null;
}
private sanitizeBrowserUrl(url: string): string | null {
if (/[^\u0020-\u007E]/.test(url)) {
return null;
}
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
private openInBrowser(url: string): void {
const safeUrl = this.sanitizeBrowserUrl(url);
if (!safeUrl) {
return;
}
const isWindows = process.platform === 'win32';
const command = isWindows
? 'rundll32'
: process.platform === 'darwin'
? 'open'
: 'xdg-open';
const args = isWindows
? ['url.dll,FileProtocolHandler', safeUrl]
: [safeUrl];
const child = spawn(command, args, {
stdio: 'ignore',
detached: !isWindows,
});
child.on('error', () => undefined);
if (!isWindows) {
child.unref();
}
}
private async syncApplication(appDirectory: string): Promise<boolean> {
this.logDetail('Running `yarn twenty dev --once`...');
return new Promise((resolve) => {
const child = spawn('yarn', ['twenty', 'dev', '--once'], {
cwd: appDirectory,
stdio: ['inherit', 'pipe', 'pipe'],
});
child.stdout?.resume();
child.stderr?.resume();
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
});
}
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
try {
const configService = new ConfigService();
const remoteNames = await configService.getRemotes();
for (const remoteName of remoteNames) {
const remoteConfig = await configService.getConfigForRemote(remoteName);
if (remoteConfig.apiUrl !== serverUrl) {
continue;
}
const token = remoteConfig.twentyCLIAccessToken ?? remoteConfig.apiKey;
if (!token) {
continue;
}
const response = await fetch(`${serverUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: '{ currentWorkspace { id } }',
}),
});
if (!response.ok) {
continue;
}
const body = (await response.json()) as {
data?: { currentWorkspace?: { id: string } };
errors?: unknown[];
};
if (isDefined(body.data?.currentWorkspace) && !body.errors) {
ConfigService.setActiveRemote(remoteName);
await configService.setDefaultRemote(remoteName);
return true;
}
}
return false;
} catch {
return false;
}
}
private logCreationInfo({
appDirectory,
appName,
}: {
appDirectory: string;
appName: string;
}): void {
console.log(
chalk.blue('\n', 'Creating Twenty Application\n'),
chalk.gray(`- Directory: ${appDirectory}\n`, `- Name: ${appName}\n`),
);
}
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
return true;
}
if (checkDockerRunning() && containerExists()) {
if (autoConfirm) {
return true;
}
const { startExisting } = await inquirer.prompt([
{
type: 'confirm',
name: 'startExisting',
message:
'An existing Twenty server container was found. Would you like to start it?',
default: true,
},
]);
return startExisting;
}
if (autoConfirm) {
return true;
}
const { startDocker } = await inquirer.prompt([
{
type: 'confirm',
name: 'startDocker',
message:
'No running Twenty instance found. Would you like to start one using Docker?',
default: true,
},
]);
return startDocker;
}
private async promptConnectToLocal(serverUrl: string): Promise<void> {
const { shouldAuthenticate } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldAuthenticate',
message: `Would you like to authenticate to the local Twenty instance (${serverUrl})?`,
default: true,
},
]);
if (!shouldAuthenticate) {
console.log(
chalk.gray(
'Authentication skipped. Run `yarn twenty remote add --local` manually.',
),
);
return;
}
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
try {
const result = await authLoginOAuth({
const result = await authLogin({
apiKey: DEV_API_KEY,
apiUrl: serverUrl,
remote: 'local',
});
@@ -298,41 +565,111 @@ export class CreateAppCommand {
const configService = new ConfigService();
await configService.setDefaultRemote('local');
} else {
console.log(
chalk.yellow(
'Authentication failed. Run `yarn twenty remote add --local` manually.',
),
);
this.logDetail('Authenticated as tim@apple.dev (development API key)');
return true;
}
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
),
);
return false;
} catch {
console.log(
chalk.yellow(
'Authentication failed. Run `yarn twenty remote add` manually.',
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
),
);
return false;
}
}
private deriveRemoteName(url: string): string {
try {
return new URL(url).hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
}
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
try {
const remoteName = this.deriveRemoteName(serverUrl);
ConfigService.setActiveRemote(remoteName);
this.logDetail('Opening browser for OAuth...');
const result = await authLoginOAuth({ apiUrl: serverUrl });
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote(remoteName);
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
return true;
}
console.log(
chalk.yellow(
` OAuth failed: ${result.error.message}\n` +
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
} catch {
console.log(
chalk.yellow(
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
),
);
return false;
}
}
private logSuccess(
appDirectory: string,
serverResult?: ServerStartResult,
serverUrl: string,
authSucceeded: boolean,
): void {
const dirName = basename(appDirectory);
console.log(chalk.blue('\nApplication created. Next steps:'));
console.log(chalk.gray(`- cd ${dirName}`));
console.log(chalk.green('\nApplication created successfully!\n'));
if (!serverResult) {
console.log(chalk.white(' Next steps:\n'));
let stepNumber = 1;
console.log(chalk.white(` ${stepNumber}. Navigate to your project`));
console.log(chalk.cyan(` cd ${dirName}\n`));
stepNumber++;
if (!authSucceeded) {
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
console.log(
chalk.gray(
'- yarn twenty remote add # Authenticate with Twenty',
),
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
);
stepNumber++;
}
console.log(chalk.white(` ${stepNumber}. Start developing`));
console.log(chalk.cyan(' yarn twenty dev\n'));
stepNumber++;
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
console.log(chalk.cyan(` ${serverUrl}\n`));
console.log(
chalk.gray('- yarn twenty dev # Start dev mode'),
chalk.gray(
' Documentation: https://docs.twenty.com/developers/extend/capabilities/apps',
),
);
}
}
@@ -17,6 +17,7 @@ const UNIVERSAL_IDENTIFIERS_PATH = join(
'constants',
'universal-identifiers.ts',
);
const YARNRC_PATH = 'yarnrc.yml';
// Template content matching template/src/constants/universal-identifiers.ts
const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
@@ -32,9 +33,11 @@ const TEMPLATE_PACKAGE_JSON = {
license: 'MIT',
scripts: { twenty: 'twenty' },
dependencies: {
'twenty-sdk': '0.0.0',
'twenty-client-sdk': '0.0.0',
},
devDependencies: {
'twenty-sdk': '0.0.0',
},
};
describe('copyBaseApplicationProject', () => {
@@ -76,11 +79,16 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
});
expect(fs.copy).toHaveBeenCalledTimes(1);
// Two fs.copy calls: (1) the template directory, (2) AGENTS.md → CLAUDE.md
expect(fs.copy).toHaveBeenCalledTimes(2);
expect(fs.copy).toHaveBeenCalledWith(
expect.stringContaining('template'),
testAppDirectory,
);
expect(fs.copy).toHaveBeenCalledWith(
join(testAppDirectory, 'AGENTS.md'),
join(testAppDirectory, 'CLAUDE.md'),
);
});
it('should replace placeholders in universal-identifiers.ts with real values', async () => {
@@ -141,7 +149,7 @@ describe('copyBaseApplicationProject', () => {
join(testAppDirectory, 'package.json'),
);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.dependencies['twenty-sdk']).toBe(
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.dependencies['twenty-client-sdk']).toBe(
@@ -168,6 +176,27 @@ describe('copyBaseApplicationProject', () => {
expect(publicDirectoryContents).toHaveLength(0);
});
it('should rename yarnrc.yml to .yarnrc.yml in the scaffolded project', async () => {
await fs.writeFile(
join(testAppDirectory, YARNRC_PATH),
'nodeLinker: node-modules',
);
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
expect(await fs.pathExists(join(testAppDirectory, YARNRC_PATH))).toBe(
false,
);
expect(await fs.pathExists(join(testAppDirectory, '.yarnrc.yml'))).toBe(
true,
);
});
it('should handle empty description', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
@@ -3,7 +3,6 @@ import { join } from 'path';
import { v4 } from 'uuid';
import createTwentyAppPackageJson from 'package.json';
import chalk from 'chalk';
const SRC_FOLDER = 'src';
@@ -12,25 +11,33 @@ export const copyBaseApplicationProject = async ({
appDisplayName,
appDescription,
appDirectory,
onProgress,
}: {
appName: string;
appDisplayName: string;
appDescription: string;
appDirectory: string;
onProgress?: (message: string) => void;
}) => {
console.log(chalk.gray('Generating application project...'));
onProgress?.('Copying base template');
await fs.copy(join(__dirname, './constants/template'), appDirectory);
onProgress?.('Configuring dotfiles (.gitignore, .github, .yarnrc.yml)');
await renameDotfiles({ appDirectory });
onProgress?.('Mirroring AGENTS.md to CLAUDE.md');
await mirrorAgentsToClaude({ appDirectory });
await addEmptyPublicDirectory({ appDirectory });
onProgress?.('Generating unique application identifiers');
await generateUniversalIdentifiers({
appDisplayName,
appDescription,
appDirectory,
});
onProgress?.('Updating package.json');
await updatePackageJson({ appName, appDirectory });
};
@@ -40,6 +47,7 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' },
{ from: 'yarnrc.yml', to: '.yarnrc.yml' },
];
for (const { from, to } of renames) {
@@ -51,6 +59,19 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
}
};
// AGENTS.md is the cross-tool standard; Claude Code prefers CLAUDE.md and only
// falls back to AGENTS.md, so we mirror the file to keep a single source of truth.
const mirrorAgentsToClaude = async ({
appDirectory,
}: {
appDirectory: string;
}) => {
await fs.copy(
join(appDirectory, 'AGENTS.md'),
join(appDirectory, 'CLAUDE.md'),
);
};
const addEmptyPublicDirectory = async ({
appDirectory,
}: {
@@ -99,7 +120,8 @@ const updatePackageJson = async ({
const packageJson = await fs.readJson(join(appDirectory, 'package.json'));
packageJson.name = appName;
packageJson.dependencies['twenty-sdk'] = createTwentyAppPackageJson.version;
packageJson.devDependencies['twenty-sdk'] =
createTwentyAppPackageJson.version;
packageJson.dependencies['twenty-client-sdk'] =
createTwentyAppPackageJson.version;
@@ -0,0 +1,33 @@
import { execSync } from 'node:child_process';
const DOCKER_DOWNLOAD_URLS: Record<string, string> = {
darwin: 'https://docs.docker.com/desktop/setup/install/mac-install/',
win32: 'https://docs.docker.com/desktop/setup/install/windows-install/',
linux: 'https://docs.docker.com/engine/install/',
};
export const isDockerInstalled = (): boolean => {
try {
execSync('docker --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
export const getDockerInstallInstructions = (): string => {
const url =
DOCKER_DOWNLOAD_URLS[process.platform] ?? DOCKER_DOWNLOAD_URLS.linux;
return [
' Docker is required but not installed.',
'',
` Install Docker: ${url}`,
'',
' Then run this command again.',
'',
' Alternatively, connect to an existing Twenty instance:',
' npx create-twenty-app@latest my-twenty-app --url <your-instance-url>',
].join('\n');
};
@@ -1,179 +0,0 @@
import { execSync } from 'node:child_process';
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'node:os';
import chalk from 'chalk';
const TWENTY_REPO_OWNER = 'twentyhq';
const TWENTY_REPO_NAME = 'twenty';
const TWENTY_FALLBACK_REF = 'main';
const TWENTY_EXAMPLES_PATH = 'packages/twenty-apps/examples';
const TWENTY_EXAMPLES_URL = `https://github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tree/${TWENTY_FALLBACK_REF}/${TWENTY_EXAMPLES_PATH}`;
// Fetches the latest release tag from the repo, or falls back to main
const resolveRef = async (): Promise<string> => {
const response = await fetch(
`https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/releases/latest`,
{ headers: { Accept: 'application/vnd.github.v3+json' } },
);
if (response.ok) {
const release = (await response.json()) as { tag_name: string };
return release.tag_name;
}
return TWENTY_FALLBACK_REF;
};
// Uses the GitHub Contents API to list directories — fast and doesn't download the repo
const fetchGitHubDirectoryContents = async (
path: string,
ref: string,
): Promise<{ name: string; type: string }[] | null> => {
const apiUrl = `https://api.github.com/repos/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/contents/${path}?ref=${ref}`;
const response = await fetch(apiUrl, {
headers: { Accept: 'application/vnd.github.v3+json' },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (!Array.isArray(data)) {
return null;
}
return data as { name: string; type: string }[];
};
const listAvailableExamples = async (ref: string): Promise<string[]> => {
const contents = await fetchGitHubDirectoryContents(
TWENTY_EXAMPLES_PATH,
ref,
);
if (!contents) {
return [];
}
return contents
.filter((entry) => entry.type === 'dir')
.map((entry) => entry.name);
};
const validateExampleExists = async (
exampleName: string,
ref: string,
): Promise<void> => {
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
const contents = await fetchGitHubDirectoryContents(examplePath, ref);
if (contents !== null) {
return;
}
const availableExamples = await listAvailableExamples(ref);
throw new Error(
`Example "${exampleName}" not found.\n\n` +
(availableExamples.length > 0
? `Available examples:\n${availableExamples.map((name) => ` - ${name}`).join('\n')}\n\n`
: '') +
`Browse all examples: ${TWENTY_EXAMPLES_URL}`,
);
};
export const downloadExample = async (
exampleName: string,
targetDirectory: string,
): Promise<void> => {
if (
exampleName.includes('/') ||
exampleName.includes('\\') ||
exampleName.includes('..')
) {
throw new Error(
`Invalid example name: "${exampleName}". Example names must be simple directory names (e.g., "hello-world").`,
);
}
const ref = await resolveRef();
const examplePath = `${TWENTY_EXAMPLES_PATH}/${exampleName}`;
console.log(chalk.gray(`Resolving examples from ref '${ref}'...`));
await validateExampleExists(exampleName, ref);
console.log(chalk.gray(`Example '${examplePath}' validated successfully.`));
const tarballUrl = `https://codeload.github.com/${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME}/tar.gz/${ref}`;
const tempDir = join(
tmpdir(),
`create-twenty-app-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
await fs.ensureDir(tempDir);
console.log(chalk.gray(`Downloading tarball from ${tarballUrl}...`));
const response = await fetch(tarballUrl);
if (!response.ok) {
if (response.status === 404) {
throw new Error(
`Could not find repository: ${TWENTY_REPO_OWNER}/${TWENTY_REPO_NAME} (ref: ${ref})`,
);
}
throw new Error(
`Failed to download from GitHub: ${response.status} ${response.statusText}`,
);
}
console.log(chalk.gray('Tarball downloaded. Writing to disk...'));
const tarballPath = join(tempDir, 'archive.tar.gz');
const buffer = Buffer.from(await response.arrayBuffer());
await fs.writeFile(tarballPath, buffer);
console.log(
chalk.gray(
`Tarball saved (${(buffer.length / 1024 / 1024).toFixed(1)} MB). Extracting...`,
),
);
execSync(`tar xzf "${tarballPath}" -C "${tempDir}"`, {
stdio: 'pipe',
});
// GitHub tarballs extract to a directory named {repo}-{ref}/
const extractedEntries = await fs.readdir(tempDir);
const extractedDir = extractedEntries.find(
(entry) => entry !== 'archive.tar.gz',
);
if (!extractedDir) {
throw new Error('Failed to extract archive: no directory found');
}
const sourcePath = join(tempDir, extractedDir, examplePath);
if (!(await fs.pathExists(sourcePath))) {
throw new Error(
`Example directory not found in archive: "${examplePath}"`,
);
}
await fs.copy(sourcePath, targetDirectory);
} finally {
await fs.remove(tempDir);
}
};
@@ -4,14 +4,18 @@ import { exec } from 'child_process';
const execPromise = promisify(exec);
export const install = async (root: string) => {
console.log(chalk.gray('Installing yarn dependencies...'));
export const install = async (
root: string,
onProgress?: (message: string) => void,
) => {
onProgress?.('Enabling corepack');
try {
await execPromise('corepack enable', { cwd: root });
} catch (error: any) {
console.warn(chalk.yellow('corepack enabled failed:'), error.stderr);
console.warn(chalk.yellow('corepack enable failed:'), error.stderr);
}
onProgress?.('Running yarn install');
try {
await execPromise('yarn install', { cwd: root });
} catch (error: any) {
@@ -93,7 +93,7 @@ yarn install
# Register your local Twenty server as a remote (interactive prompt).
# When asked for the URL use http://localhost:2021 and paste an API key
# from Settings -> Developers in the Twenty UI.
yarn twenty remote add
yarn twenty remote:add
# Build, install, and watch for changes.
yarn twenty dev
@@ -107,8 +107,8 @@ watching `src/`. Edit any file and the change is re-synced within seconds.
```bash
cd packages/twenty-apps/community/github-connector
yarn install
yarn twenty remote add # same prompts as above
yarn twenty install # builds and installs once
yarn twenty remote:add # same prompts as above
yarn twenty app:install # builds and installs once
```
## Configure authentication
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
frontComponentUniversalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
frontComponentUniversalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -1,10 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
useRecordId,
} from 'twenty-sdk/front-component';
import { enqueueSnackbar, useRecordId } from 'twenty-sdk/front-component';
import { THEME } from 'src/modules/github/contributor/components/theme';
import { callAppRoute } from 'src/modules/shared/call-app-route';
@@ -682,12 +678,4 @@ export default defineFrontComponent({
description:
'Displays time-bucketed charts of PRs authored (merged only), merged and reviewed by a contributor over the last week, month, 3 months or year.',
component: ContributorStats,
command: {
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -95,12 +94,4 @@ export default defineFrontComponent({
'Fetches contributors from every configured GitHub repo (GITHUB_REPOS) and upserts them as Contributor records.',
isHeadless: true,
component: FetchContributors,
command: {
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
frontComponentUniversalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -98,12 +97,4 @@ export default defineFrontComponent({
description: 'Fetches issues from GitHub repos',
isHeadless: true,
component: FetchIssues,
command: {
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
frontComponentUniversalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -97,12 +96,4 @@ export default defineFrontComponent({
description: 'Fetches project items from GitHub Projects V2',
isHeadless: true,
component: FetchProjectItems,
command: {
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
},
});
@@ -0,0 +1,11 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
frontComponentUniversalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
});
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -101,12 +100,4 @@ export default defineFrontComponent({
description: 'Fetches pull requests and reviews from GitHub repos',
isHeadless: true,
component: FetchPrs,
command: {
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
},
});
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote add --api-url http://localhost:2020 --as local
yarn twenty remote:add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
```
## Integration Tests
@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}
@@ -28,12 +28,6 @@ yarn install
yarn twenty dev
```
Or use it as a template via create-twenty-app:
```bash
npx create-twenty-app@latest my-app --example postcard
```
## Learn more
- [Getting Started](https://docs.twenty.com/developers/extend/apps/getting-started)
@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty server start\n' +
'Start a local server: yarn twenty docker:start\n' +
'Or set them in vitest env config.',
);
}
@@ -80,8 +80,8 @@ export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
throw new Error(
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
);
}
}

Some files were not shown because too many files have changed in this diff Show More