Compare commits

...
Author SHA1 Message Date
prastoin 6e27b5ec62 remove vite 2026-05-23 12:37:46 +02:00
prastoin 5751cacb4d nit 2026-05-23 12:36:24 +02:00
prastoin 64f9e07d5e enrich blob patterns 2026-05-23 12:32:09 +02:00
prastoin a6aeb10484 chore(server): include package.json and lock file 2026-05-23 12:30:03 +02:00
prastoin 2327c5dd30 chore(server): downscope code owners 2026-05-23 12:29:25 +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
af76e04f8d i18n - translations (#20340)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 10:10:15 +02:00
afb1f8b983 i18n - translations (#20338)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 10:06:49 +02:00
EtienneandGitHub 81f351c90c Ai provider - fix (#20318) 2026-05-07 10:02:54 +02:00
43b6f32080 i18n - website translations (#20337)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 10:01:46 +02:00
martmullandGitHub 948ed964bb Add isConfigured to application registration in App admin panel (#20326)
## After
Added "Configured" column in admin panel apps tab
<img width="1171" height="611" alt="image"
src="https://github.com/user-attachments/assets/e6850b39-5789-4ad2-b3df-192a28cb03e2"
/>

Add a banner to ask to configure the application 
<img width="1218" height="483" alt="image"
src="https://github.com/user-attachments/assets/1491363c-3479-41ca-97b7-c7dc9e07ff16"
/>
2026-05-07 09:59:01 +02:00
eddd2c979a Add Workspace Created and Payment Received ClickHouse events (#20277)
## Summary
- Adds two new workspace audit events for the AARRR funnel tracked in
ClickHouse
- **Workspace Created**: emitted in `signUpOnNewWorkspace` after the
transaction commits, capturing every new workspace creation
- **Payment Received**: emitted in `processInvoicePaid` on every Stripe
`invoice.paid` webhook, with `stripeInvoiceId`, `amountPaid`, and
`billingReason` properties. First payment per workspace can be derived
at query time via `min(timestamp)` grouped by `workspaceId`

## Test plan
- [x] Verify `Workspace Created` event appears in ClickHouse after
signing up on a new workspace
- [x] Verify `Payment Received` event appears in ClickHouse after a
Stripe `invoice.paid` webhook fires
- [x] Confirm no event is emitted if the billing customer cannot be
resolved from `stripeCustomerId`
- [x] Run existing `SignInUpService` unit tests pass with the new
`AuditService` mock


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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 09:57:30 +02:00
Thomas TrompetteandGitHub be4b466234 Remove broken total count from workflow version (#20324)
## Problem

Opening a workflow run with a form step in the side panel, closing the
form and reopening it crashes the app: `<SidePanelWorkflowStepInfo>`
blows up on `workflow.versions.find` because `versions` is `null` in the
Apollo cache.

## Root cause

`useWorkflowVersion` was selecting:

```ts
workflow: { id, name, statuses, versions: { totalCount: true } }
```
Twenty's GraphQL field generator doesn't support connection-level
scalars — { totalCount: true } is interpreted as fields on the inner
WorkflowVersion node, gets filtered out, and the query collapses to:
versions { edges { node { __typename } } }
The server returns versions: null for that empty-node selection. 

Why now
The selection has always been wrong, but two recent changes made it
consistently surface:

Apollo Client v4 upgrade (#18584): stricter normalized writes, null
always wins.
#20242: WorkflowRunSSESubscribeEffect in the form filler keeps SSE
flowing, which re-fires useWorkflowVersion more often, making the bad
query consistently the last writer.
2026-05-07 09:57:10 +02:00
neo773andGitHub 1faf725498 Fix NestJS CLI pin chokidar to v3 (#20316)
fixes `EMFILE` by downgrading chokidar to v3
root cause is v4 removed kernel level FSEvents on macOS and instead uses
`node:fs.watch` which doesn't scales for a repo of our size

Seems to be working well, even survives multiple hot reloads after
editing files
2026-05-07 09:55:53 +02:00
Abdullah.andGitHub 552016a4d0 [Website] Add articles section with index and article pages, matching customers page design (#20315)
Bare-bone structure for the blog/articles on website.
2026-05-07 09:54:28 +02:00
Charles BochetandGitHub 10876138d2 refactor: stop reading joinColumnName from relation field settings (#20304)
## Summary

`joinColumnName` on relation field settings is always derivable from the
field name (and the target object name for morph relations). This PR
stops reading it from settings anywhere in production code; the stored
value is no longer used.

The settings field is **not** removed from data yet — a follow-up can
drop it once we are confident nothing depends on the stored value.

## Helpers

The helpers are split by layer because frontend and backend hold morph
relations differently: the frontend has a base name plus a
`morphRelations[]` array, the backend has one row per target with the
name already morph-resolved.

| Helper | Layer | When to use |
|---|---|---|
| `computeRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Non-morph relation on the frontend. |
| `computeMorphRelationGqlFieldName` | Shared / frontend (`gqlField`) |
Need the per-target morph gqlField name (e.g. `targetCompany`). |
| `computeMorphRelationGqlFieldJoinColumnName` | Shared / frontend
(`gqlField`) | Per-target morph join column on the frontend. Prefer over
the non-morph helper for any morph field — it forces the per-target
inputs. |
| `computeMorphOrRelationFieldJoinColumnName` | Backend
(`FlatFieldMetadata.name`) | Any backend read or write — the flat name
is already morph-resolved, so one helper covers both cases. |
| `computeMorphRelationFlatFieldName` | Backend
(`FlatFieldMetadata.name`) | **Mutation paths only** (create / update /
object rename). Reads consume the stored `field.name` and never call
this. |

## Test plan

- [x] Typecheck and lint (front, server, shared)
- [x] Existing unit tests pass
- [ ] CI green
2026-05-07 09:53:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b56ad0607 chore(deps-dev): bump verdaccio from 6.3.1 to 6.5.2 (#20334)
Bumps [verdaccio](https://github.com/verdaccio/verdaccio) from 6.3.1 to
6.5.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/verdaccio/verdaccio/releases">verdaccio's
releases</a>.</em></p>
<blockquote>
<h2>v6.5.2</h2>
<h3><a
href="https://github.com/verdaccio/verdaccio/compare/v6.5.1...v6.5.2">6.5.2</a>
(2026-04-19)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>avoid sharing default security object across configs (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5812">#5812</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/9cca86ee8ac7b64f9011cdc6ac44b995ae025fc8">9cca86e</a>)</li>
<li>Missing package refresh after logging into WebUI (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5825">#5825</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/e6bbea44c56f904e850b883394ef4ffca6c93439">e6bbea4</a>),
closes <a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5814">#5814</a></li>
<li>remove basic header on login error 401 (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5821">#5821</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/1c1723dcbe2fcb1fd64d070d6d357ab7e24ece0a">1c1723d</a>)</li>
<li>update ui-theme dependency (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5822">#5822</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/c4f2cd99d57672792cda175b4674a1310e18fa38">c4f2cd9</a>)</li>
</ul>
<h2>v6.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: enable ui e2e test by <a
href="https://github.com/juanpicado"><code>@​juanpicado</code></a> in <a
href="https://redirect.github.com/verdaccio/verdaccio/pull/5803">verdaccio/verdaccio#5803</a></li>
<li>fix: web validate password issue by <a
href="https://github.com/juanpicado"><code>@​juanpicado</code></a> in <a
href="https://redirect.github.com/verdaccio/verdaccio/pull/5811">verdaccio/verdaccio#5811</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/verdaccio/verdaccio/compare/v6.5.0...v6.5.1">https://github.com/verdaccio/verdaccio/compare/v6.5.0...v6.5.1</a></p>
<h2>v6.5.0</h2>
<h2><a
href="https://github.com/verdaccio/verdaccio/compare/v6.4.0...v6.5.0">6.5.0</a>
(2026-04-11)</h2>
<h3>Features</h3>
<ul>
<li>update ui to major (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5794">#5794</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/b957c6f0edd909d2c04ba4643d224ef0022c6416">b957c6f</a>)
<a href="https://github.com/juanpicado"><code>@​juanpicado</code></a>
<ul>
<li>Big UI refactoring <a
href="https://redirect.github.com/verdaccio/verdaccio/pull/5563">verdaccio/verdaccio#5563</a></li>
</ul>
</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>package-filter:</strong> fix O(n²) complexity in
cleanupDistFiles (<a
href="https://github.com/verdaccio/verdaccio/commit/b15f62279d86f16a916f4de849cc9376327849f1">b15f622</a>)
<a
href="https://redirect.github.com/verdaccio/verdaccio/pull/5797">verdaccio/verdaccio#5797</a>
by <a
href="https://github.com/plottodev"><code>@​plottodev</code></a></li>
<li>ui search returns no output <a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5798">#5798</a>
(<a
href="https://github.com/verdaccio/verdaccio/commit/3edd3ee8fab6e75c0ee4f3be5ae812dc8893459b">3edd3ee</a>)
<a
href="https://github.com/juanpicado"><code>@​juanpicado</code></a></li>
</ul>
<h2>v6.4.0</h2>
<h2>Features</h2>
<h3>Package Filter Plugins (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5786">#5786</a>,
<a
href="https://redirect.github.com/verdaccio/verdaccio/pull/5548">verdaccio/verdaccio#5548</a>)
by <a href="https://github.com/vsugrob"><code>@​vsugrob</code></a>, <a
href="https://github.com/pyhp2017"><code>@​pyhp2017</code></a> <a
href="https://github.com/juanpicado"><code>@​juanpicado</code></a></h3>
<blockquote>
<p>⚠️ Please help us to test this feature (it is pretty new and might be
not perfect) ref <a
href="https://github.com/orgs/verdaccio/discussions/5796">https://github.com/orgs/verdaccio/discussions/5796</a>
The <code>@verdaccio/package-filter</code> package is bundled by default
but must be enabled by the user.</p>
</blockquote>
<p><code>@verdaccio/package-filter</code> is a built-in plugin that
intercepts package metadata from uplinks and removes versions matching
configurable rules. With no rules configured, it acts as a no-op
passthrough.</p>
<h4>Block a compromised package version</h4>
<pre lang="yaml"><code>filters:
  '@verdaccio/package-filter':
    block:
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/verdaccio/verdaccio/blob/v6.5.2/CHANGELOG.md">verdaccio's
changelog</a>.</em></p>
<blockquote>
<h3><a
href="https://github.com/verdaccio/verdaccio/compare/v6.5.1...v6.5.2">6.5.2</a>
(2026-04-19)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>avoid sharing default security object across configs (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5812">#5812</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/9cca86ee8ac7b64f9011cdc6ac44b995ae025fc8">9cca86e</a>)</li>
<li>Missing package refresh after logging into WebUI (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5825">#5825</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/e6bbea44c56f904e850b883394ef4ffca6c93439">e6bbea4</a>),
closes <a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5814">#5814</a></li>
<li>remove basic header on login error 401 (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5821">#5821</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/1c1723dcbe2fcb1fd64d070d6d357ab7e24ece0a">1c1723d</a>)</li>
<li>update ui-theme dependency (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5822">#5822</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/c4f2cd99d57672792cda175b4674a1310e18fa38">c4f2cd9</a>)</li>
</ul>
<h3><a
href="https://github.com/verdaccio/verdaccio/compare/v6.5.0...v6.5.1">6.5.1</a>
(2026-04-16)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>web validate password issue (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5811">#5811</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/b66c872d2f1367fc204e96343747ff7a6d8ae601">b66c872</a>)</li>
</ul>
<h2><a
href="https://github.com/verdaccio/verdaccio/compare/v6.4.0...v6.5.0">6.5.0</a>
(2026-04-11)</h2>
<h3>Features</h3>
<ul>
<li>update ui to major (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5794">#5794</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/b957c6f0edd909d2c04ba4643d224ef0022c6416">b957c6f</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>package-filter:</strong> fix O(n²) complexity in
cleanupDistFiles (<a
href="https://github.com/verdaccio/verdaccio/commit/b15f62279d86f16a916f4de849cc9376327849f1">b15f622</a>)</li>
<li>ui search returns no output <a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5798">#5798</a>
(<a
href="https://github.com/verdaccio/verdaccio/commit/3edd3ee8fab6e75c0ee4f3be5ae812dc8893459b">3edd3ee</a>)</li>
</ul>
<h2><a
href="https://github.com/verdaccio/verdaccio/compare/v6.3.2...v6.4.0">6.4.0</a>
(2026-04-06)</h2>
<h3>Features</h3>
<ul>
<li>add package filter (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5786">#5786</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/458a9f2973ff018f2151386725ee36b4b012a69f">458a9f2</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> update core verdaccio dependencies (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5674">#5674</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/4d655079eac09cb32d0f3b072a829e7c24945117">4d65507</a>)</li>
<li><strong>deps:</strong> update core verdaccio dependencies (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5780">#5780</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/b58287b1416291b34f1330fe0fd4653ae3f35c99">b58287b</a>)</li>
<li><strong>deps:</strong> update dependency lodash to v4.18.1 (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5777">#5777</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/797ae530d33a565948166cfd1f45f27ddb33d4ba">797ae53</a>)</li>
</ul>
<h3><a
href="https://github.com/verdaccio/verdaccio/compare/v6.3.1...v6.3.2">6.3.2</a>
(2026-03-14)</h3>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> update core verdaccio dependencies (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5636">#5636</a>)
(<a
href="https://github.com/verdaccio/verdaccio/commit/3da63a4d0bda7dd3bf86378992b05c67b0f1eda5">3da63a4</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/6edeabe00d3b2607aaa287e420badbb938c603ef"><code>6edeabe</code></a>
chore(release): 6.5.2</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/e6bbea44c56f904e850b883394ef4ffca6c93439"><code>e6bbea4</code></a>
fix: Missing package refresh after logging into WebUI (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5825">#5825</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/c4f2cd99d57672792cda175b4674a1310e18fa38"><code>c4f2cd9</code></a>
fix: update ui-theme dependency (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5822">#5822</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/1c1723dcbe2fcb1fd64d070d6d357ab7e24ece0a"><code>1c1723d</code></a>
fix: remove basic header on login error 401 (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5821">#5821</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/9cca86ee8ac7b64f9011cdc6ac44b995ae025fc8"><code>9cca86e</code></a>
fix: avoid sharing default security object across configs (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5812">#5812</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/f01311279f59eb8b93386dbeef367d2ee323a49f"><code>f013112</code></a>
chore(release): 6.5.1</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/b66c872d2f1367fc204e96343747ff7a6d8ae601"><code>b66c872</code></a>
fix: web validate password issue (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5811">#5811</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/f25003c682346fd74cf56b3c9c2352d567faaa40"><code>f25003c</code></a>
chore: update cypress config</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/6d792e739d5db118596ebfe361e020e89d3642b4"><code>6d792e7</code></a>
chore: enable ui e2e test (<a
href="https://redirect.github.com/verdaccio/verdaccio/issues/5803">#5803</a>)</li>
<li><a
href="https://github.com/verdaccio/verdaccio/commit/4dd0083722620f8efaa3af0f916dd8f38f8acd17"><code>4dd0083</code></a>
chore(release): 6.5.0</li>
<li>Additional commits viewable in <a
href="https://github.com/verdaccio/verdaccio/compare/v6.3.1...v6.5.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=verdaccio&package-manager=npm_and_yarn&previous-version=6.3.1&new-version=6.5.2)](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-07 09:52:39 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
de92dd7838 chore(deps): bump papaparse from 5.5.2 to 5.5.3 (#20335)
Bumps [papaparse](https://github.com/mholt/PapaParse) from 5.5.2 to
5.5.3.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/mholt/PapaParse/blob/master/CHANGELOG.md">papaparse's
changelog</a>.</em></p>
<blockquote>
<h2>5.5.3</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Avoid infinite loop with duplicate header counting (<a
href="https://redirect.github.com/mholt/PapaParse/issues/1095">#1095</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/mholt/PapaParse/commits">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=papaparse&package-manager=npm_and_yarn&previous-version=5.5.2&new-version=5.5.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-07 09:52:27 +02:00
Charles BochetandGitHub 9ac503e3af fix(front): defer default home redirect when object metadata is not loaded (#20330)
## Summary

Fixes the merge-queue E2E failures introduced after #20308. After login,
users were being silently redirected to `/settings/profile` instead of
their workspace home, which broke every dependent E2E test that re-uses
the post-login URL (`workflow-creation.spec.ts`,
`authentication/signup_invite_email.spec.ts`, etc.).

## Root cause

`useDefaultHomePagePath` falls back to `/settings/profile` when
`readableNonSystemObjectMetadataItems` is empty. That list is empty in
two cases:

1. The user genuinely has no readable objects → `/settings/profile` is
the intended fallback.
2. Object metadata simply hasn't been loaded yet (transient post-login
window).

Before #20308 the frontend always loaded mocked metadata for
authenticated users, so case (2) never happened. After #20308 mocked
metadata is gone, and during the post-verify window
(`handleLoadWorkspaceAfterAuthentication` finishes,
`setIsAppEffectRedirectEnabled(true)` re-enables redirects,
`PageChangeEffect` fires) the metadata store is still empty. The hook
then returns `/settings/profile`. Because that path is not in
`ONBOARDING_PATHS` / `ONGOING_USER_CREATION_PATHS`,
`usePageChangeEffectNavigateLocation` doesn't fire a corrective redirect
once metadata finally loads — the user is stranded.

`login.setup.ts` captures `process.env.LINK = page.url()` after verify,
so subsequent tests `goto(LINK)` end up in Settings looking for app
navigation that isn't there → click timeouts.

## Fix

Distinguish the two empty cases by reading
`metadataStoreState('objectMetadataItems').status`. If it isn't
`'up-to-date'` we defer to `AppPath.Index` instead of
`/settings/profile`. The memo recomputes when the status flips, and the
user is then routed to their actual home page.

A regression test is added in `useDefaultHomePagePath.test.ts` for the
not-loaded-yet case.

## Test plan

- [x] Unit: `npx jest
src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts`
(5/5 pass, including new regression case)
- [ ] CI: Playwright E2E (`workflow-creation.spec.ts`,
`authentication/signup_invite_email.spec.ts`) pass on this branch
- [ ] Manual: log in to a fresh local instance and confirm landing page
is the workspace home, not `/settings/profile`
2026-05-07 09:51:55 +02:00
Charles BochetandGitHub 83c40bb8cc fix(server): bypass workspace cache in onboardingStatus resolver (#20322)
## Summary

In multi-instance deployments, `coreEntityCacheService` memoizes the
workspace entity per server for ~10s, bypassing Redis hash invalidation.
After `activateWorkspace`, if the next `currentUser` query is routed to
a stale replica, the server returns `onboardingStatus:
WORKSPACE_ACTIVATION` and `workspaceMember: null`, the client redirects
to `/create/profile`, and submitting the form throws "User is not logged
in". Reproduces on prod/staging only (local dev = single instance).

Fix: in `OnboardingService.getOnboardingStatus({ user, workspaceId })`,
read the workspace directly from `WorkspaceEntity` repository (bypassing
the per-instance core entity cache) so `onboardingStatus` reflects the
freshest `activationStatus` right after `activateWorkspace`, even when
the request hits a replica with a stale cached workspace.

## Test plan

- Prod/staging: sign up + create workspace, verify `/create/profile`
works and form submits.
- Local: regression on the full onboarding flow.
2026-05-06 17:41:46 +02:00
Abdullah.andGitHub b94b198a3b fix: server.fs.deny bypassed with queries (#20323)
Resolves [Dependabot Alert
886](https://github.com/twentyhq/twenty/security/dependabot/886).
2026-05-06 16:29:30 +02:00
martmullandGitHub 2158266fcc Fix migration (#20321)
as title
2026-05-06 15:05:13 +02:00
3f307bd192 i18n - translations (#20317)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-06 11:56:58 +02:00
Charles BochetandGitHub ee6c0ef904 Replace sign-in mocked metadata with hardcoded BackgroundMock (#20308)
## Summary

When the user is logged out, we render the auth modal on top of a sample
table to make the empty page feel alive. So far this was achieved by
**loading a full set of mocked object / field / view / navigation-menu
metadata into the runtime metadata store** and then mounting the real
`RecordTable` and `AppNavigationDrawer` behind the modal. This had a few
downsides:

- Significant bundle weight pulled in for unauthenticated users (mocked
GraphQL fixtures + the real `RecordTable` virtualization stack).
- Plenty of code paths that had to know about the "showAuthModal" case
(`useRecordIndexTableQuery`, `useTriggerInitialRecordTableDataLoad`,
`MainContextStoreProvider`, `IsMinimalMetadataReadyEffect`...).
- Any change to metadata-store internals or to the record-table runtime
risked breaking the logged-out background.

This PR replaces the entire flow with a small, self-contained
`BackgroundMock` component tree that **does not consume any metadata**
and **does not load any mocked metadata at runtime**.

### What changed

- New module under `sign-in-background-mock`:
- `BackgroundMockPage` + `BackgroundMockViewBar` + `BackgroundMockTable`
+ `BackgroundMockTableRow` render a hardcoded "Companies" table that
visually mirrors the real one.
- `BackgroundMockNavigationDrawer` renders a hardcoded sidebar with
People / Companies / Opportunities / Tasks / Notes (with their standard
colors).
- Hardcoded constants in `BackgroundMockCompanies.ts`,
`BackgroundMockColumns.ts`, `BackgroundMockNavigationItems.ts`.
- `MinimalMetadataLoadEffect` no longer calls `loadMockedMetadataAtomic`
for unauthenticated users — it just doesn't load anything.
- `IsMinimalMetadataReadyEffect` now reports ready immediately when
there is no access token pair, so the skeleton loader doesn't hang
waiting for metadata that will never come.
- `MainContextStoreProvider`, `useRecordIndexTableQuery`, and
`useTriggerInitialRecordTableDataLoad` drop their `showAuthModal`
branches — the real `RecordTable` is no longer mounted behind the modal.
- `DefaultLayout` and `NotFound` now lazily load `BackgroundMockPage` /
`BackgroundMockNavigationDrawer` instead of the deleted
`SignInBackgroundMockPage` / `SignInAppNavigationDrawerMock`.
- Removed: `SignInBackgroundMockPage`, `SignInBackgroundMockContainer`,
`SignInBackgroundMockContainerEffect`, `SignInAppNavigationDrawerMock`,
`SignInBackgroundMockColumnDefinitions`,
`SignInBackgroundMockCompanies`, `SignInBackgroundMockViewFields`.

`useLoadMockedMetadata` and `preloadMockedMetadata` are kept on purpose:
Storybook decorators (`ObjectMetadataItemsDecorator`,
`WorkflowStepDecorator`) still rely on the mocked metadata fixtures, but
**production** unauthenticated runtime no longer touches them.

### Visual parity

Side-by-side at 1440×900 on `/sign-in`:

**Before** (loads mocked metadata + real RecordTable):

![before](https://github.com/user-attachments/assets/before-placeholder)

**After** (purely hardcoded BackgroundMock):

![after](https://github.com/user-attachments/assets/after-placeholder)

## Test plan

- [ ] `npx nx typecheck twenty-front`  (passes locally)
- [ ] `npx nx lint:diff-with-main twenty-front`  (oxlint + prettier
clean)
- [ ] `npx jest useRecordIndexTableQuery` 
- [ ] Manually verify `/sign-in` renders the table + nav drawer behind
the modal
- [ ] Manually verify `/not-found` still renders the background
- [ ] Verify CI: storybook, unit tests, e2e tests
2026-05-06 11:49:24 +02:00
Paul RastoinandGitHub 26874c3603 Nest command unhandled error process exit 1 (#20312)
# Introduction
When running the `run-instance-commands` on a migration failure the
process wouldn't throw at all
Leading to conditional flow to keep going whereas it should have stopped
This update is very invasive and impacts all the nest commander
registered commands
We should keep in mind that it impacts the way we create and init
database and so on

But I think that's for the best, as cli that never exit 1 is
counterintuitive
2026-05-06 09:26:42 +00:00
Charles BochetandGitHub 0608bae9ae fix(front): resolve labelIdentifier per target for morph relation depth=1 (#20305)
## Summary

On the show page, morph relations were showing "Untitled" entries for
targets whose `labelIdentifier` is not `name` (for example
`Note.title`). The GraphQL response only contained `id` for those
records.

`generateDepthRecordGqlFieldsFromFields` was hardcoding the morph
depth=1 sub-selection to `{ id, name }` for every target instead of
resolving each target's `labelIdentifier` (and `imageIdentifier`) from
`objectMetadataItems`, the way the non-morph relation branch already
does. The morph branch was also ignoring
`shouldOnlyLoadRelationIdentifiers`.

<img width="1300" height="860" alt="image"
src="https://github.com/user-attachments/assets/ebdb5287-0b4c-4a96-95a2-33b19b31446e"
/>
2026-05-06 09:03:22 +00:00
88988e5a55 i18n - translations (#20313)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-06 10:42:40 +02:00
martmullandGitHub 617f571400 20215 convert application variable to a syncable entity (#20269)
##  Summary

- Converts applicationVariable from a bespoke sync path to a proper
SyncableEntity,
unifying it with the workspace migration pipeline used by all other
manifest-managed
  entities (agent, skill, frontComponent, webhook, etc.)
- Removes the upsertManyApplicationVariableEntities method and its
direct-DB-mutation
approach in favor of the standard validate → build → run action handler
pipeline
- Adds universalIdentifier, deletedAt columns and makes applicationId
NOT NULL via an
  instance command migration

##  Motivation

Before this change, applicationVariable was the only manifest-managed
entity that bypassed
ApplicationManifestMigrationService.syncMetadataFromManifest(). It used
a bespoke service
method called directly from syncApplication(), creating two mental
models, two validation
styles, and two cache invalidation patterns. Now there's one unified
pipeline for all
  manifest entities.

##  What changed

###  Entity refactor:
- ApplicationVariableEntity now extends SyncableEntity (gains
universalIdentifier,
  non-nullable applicationId with CASCADE, soft-delete via deletedAt)

###  New flat entity layer (flat-application-variable/):
- Type, maps type, editable properties constant, entity-to-flat
converter, cache service,
  module

###  New migration pipeline wiring:
- Manifest converter
(fromApplicationVariableManifestToUniversalFlatApplicationVariable)
  - Validator service (FlatApplicationVariableValidatorService)
- Builder service
(WorkspaceMigrationApplicationVariableActionsBuilderService)
  - Create/Update/Delete action handlers with secret encryption hooks
- Registered in orchestrator, builder module, runner module, and all
type registries

###  Removed bespoke path:
- Deleted upsertManyApplicationVariableEntities from
ApplicationVariableEntityService
  - Removed its call from ApplicationSyncService.syncApplication()
- Kept update() (operator-set value at runtime) and getDisplayValue()
(runtime display)

###  Database migration:
- Instance command to add columns, backfill universalIdentifier, enforce
NOT NULL
  constraints, and update indexes

##  Test plan

  - npx nx typecheck twenty-server passes (0 errors)
- Unit tests pass (application-variable.service.spec.ts,
build-env-var.spec.ts)
- Install an app with applicationVariables in its manifest → variables
appear with correct
  universalIdentifier
- Update app manifest (add/remove/modify a variable) → migration
pipeline handles diff
  correctly
- Operator-set value via update endpoint persists correctly with
encryption
  - Uninstall app → variables cascade-deleted
  - app dev --once on example app syncs without errors
2026-05-06 08:23:53 +00:00
2a97e77303 fix(server): handle Redis idle disconnects in session-store client (#20143)
## Summary

The session-store node-redis client doesn't attach an `'error'` event
listener, so when Redis closes an idle connection (server-side `timeout`
setting), node-redis emits an unhandled `'error'` event and the entire
Node process crashes with `SocketClosedUnexpectedlyError`.

## Reproduction

1. Deploy twenty-server against a Redis instance with `timeout 300` (5
min idle close).
2. Don't log in (or otherwise keep the session store completely idle).
3. ~5 minutes after `Nest application successfully started`, the process
crashes:

```
node:events:487
      throw er; // Unhandled 'error' event
      ^

SocketClosedUnexpectedlyError: Socket closed unexpectedly
    at Socket.<anonymous> (/app/node_modules/@redis/client/dist/lib/client/socket.js:194:118)
    ...
Emitted 'error' event on Commander instance at:
    at RedisSocket._RedisSocket_onSocketError (/app/node_modules/@redis/client/dist/lib/client/socket.js:218:10)
```

Kubernetes restarts the pod and the loop repeats every ~5 minutes (12
restarts in 95 min in our environment).

`twenty-worker` is unaffected — BullMQ's ioredis client has its own
keep-alive and the queue keeps it busy.

## Root cause


`packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts`
constructs the node-redis client with no error listener:

```ts
const redisClient = createClient({ url: connectionString });

redisClient.connect().catch((err) => {
  throw new Error(`Redis connection failed: ${err}`);
});
```

In Node.js, an unhandled `'error'` event on an `EventEmitter` becomes an
uncaught exception. node-redis emits `'error'` on socket close. With no
listener, the process exits 1 — even though node-redis would otherwise
reconnect on its own.

## Fix

1. Attach a `client.on('error', ...)` listener so disconnect errors are
logged. node-redis' built-in `reconnectStrategy` then takes over.
2. Set `pingInterval: 60_000` so the connection is never idle long
enough to be reaped by any reasonable Redis `timeout`. Defense in depth.

## Verification

Reproduced locally with Redis `CONFIG SET timeout 30` (30s for fast
reproduction). Without the fix: process exits 30s after boot. With the
fix: client logs the disconnect, reconnects, and the process keeps
running.

## Notes / out of scope

- `cache-storage.module-factory.ts` uses `cache-manager-redis-yet`
(which wraps node-redis under the hood). It may exhibit the same
vulnerability under sufficiently idle conditions; recommend a follow-up
to confirm and similarly harden it.
- `redis-client.service.ts` uses ioredis, which has built-in keepalive
and reconnect — no immediate crash risk, but adding error logging there
would be a nice consistency win.

## Test plan

- [ ] Existing tests still pass
- [ ] Manual: deploy with low Redis `timeout` (e.g. `30`), confirm
process survives
- [ ] Manual: kill Redis briefly, confirm twenty-server reconnects
instead of exiting

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 22:09:13 +00:00
bbd9720ab3 [Dashboards] [Warning] Remove gauge chart support and delete existing widgets (#20172)
## Summary

Removes gauge chart from the chart-type picker and deletes existing
gauge widgets via a workspace migration. The gauge was rendering a
hardcoded `0.7 / "Progress"` stub regardless of configuration -- never
wired to real data.

The contract stays in place. We keep
`WidgetConfigurationType.GAUGE_CHART`, the DTO, the GraphQL union
member, and the gauge folder -- so stored gauge JSON still resolves
through the schema. The render path falls through to `default: return
null`, so any un-migrated gauge widget renders as an empty cell, not a
crash.

This PR just removes existing gauge widgets if there are any (via
`upgrade:2-3:delete-gauge-widgets`). The deliberate cleanup -- deleting
the type definitions, the gauge folder, the DTO -- comes in a follow-up
PR after the migration has run.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 21:36:45 +00:00
6ebeedba0a i18n - docs translations (#20303)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:54:25 +02:00
a3c026f1ce i18n - translations (#20302)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:39:23 +02:00
e0563377b5 Fix unclear metadata validation errors (#20234)
https://github.com/user-attachments/assets/8f8f1122-3de1-4a9b-8bb4-a3c8d31e47ae

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 18:18:05 +00:00
a03c2647cf Fix unreliable SSE event stream updates during workflow form transitions (#20242)
Before - workflow run not up to date, needs refresh to see created
company in some cases


https://github.com/user-attachments/assets/28517e97-2404-4f75-8bce-cc33e3cbea20

After 


https://github.com/user-attachments/assets/60f930cb-1265-4c50-8ec5-aa4f978b1873

## Summary
- Split `SSEQuerySubscribeEffect`'s single debounced
`updateQueryListeners` into separate `syncAdditions` (leading edge, 1s
debounce) and `syncRemovals` (trailing edge, 200ms debounce) callbacks.
This prevents query unregistrations during component mount/unmount
transitions from creating gaps where events are missed, while keeping
new registrations immediate.
- Each sync path now updates `activeQueryListenersState` granularly
(append-only for additions, filter-only for removals) instead of
overwriting the entire state, eliminating a race condition where
removals could mark unregistered queries as active.
- Mount `WorkflowRunSSESubscribeEffect` inside
`WorkflowEditActionFormFiller` so the workflow-run query subscription
stays active during form steps.
- Extract `buildSortedConnectionEdges` util that builds the resulting
edge list of a cached record connection after new records are created.
Position placeholders (`'first'` / `'last'`) bypass orderBy and are
pinned to the front/back; sortable positions (numeric or undefined) are
merged into existing edges and sorted by the connection's actual
`orderBy`. This replaces the broken `length * position` insertion logic
in `triggerCreateRecordsOptimisticEffect` that treated the sortable
`position` field as a 0-1 ratio, causing new records from SSE to land at
invisible indices in the cached list. Also fixes `totalCount` increment
for batched creates, derives `pageInfo` cursors from the final array,
and gracefully skips records whose `toReference` returns null.

## Test plan
- [x] Run a workflow with a form step — verify the workflow status
updates live after form submission (no stuck "running" state)
- [x] Run the same workflow multiple times — verify company creation
events appear live on the record index page for every run, not just the
first
- [x] Click the "+" button to create a record in first position — verify
it appears immediately at the top
- [x] Verify other SSE-backed live updates (record creation, deletion,
updates) still work correctly

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 18:14:25 +00:00
6854dc549b i18n - website translations (#20301)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 20:12:54 +02:00
Abdul RahmanandGitHub cbd2a017da Improve app gallery image sizing (no cropping) (#20287)
<img width="908" height="808" alt="Screenshot 2026-05-05 at 7 38 46 PM"
src="https://github.com/user-attachments/assets/a6f0a9b7-f676-46a3-8642-48c4bd06c7f4"
/>
2026-05-05 17:28:39 +00:00
Abdullah.andGitHub 8253fb6e6d feat: improve SEO foundations and canonicalise locale URLs while adding language-switcher in Footer as planned (#20294)
#### SEO

- Heading default flipped from h1 → h2; only Hero.Heading defaults to
h1. Eliminates accidental multi-h1 pages, which was confusing search
engines about the primary topic.
- Titles and descriptions in static-website-routes.ts rewritten to be
keyword-led and unique per page.
- Added buildFaqPageJsonLd (used on /, /pricing) and
buildReleaseListJsonLd (used on /releases).
- ReleaseEntry now renders id={release} so JSON-LD @id fragments resolve
to anchors.


#### Footer language switcher
- New LocaleSwitcher.tsx (plain React popover — useState + useRef +
outside-click). Trigger renders globe icon + native language name
(Français); popover lists all enabled locales with native + English
names side-by-side.
- Intl.DisplayNames-based name resolution in locale-display-names.ts.
- Plumbed into the footer's bottom row next to copyright.

Translations have not been pulled from Crowdin yet, so French pages
currently show English copy.
2026-05-05 17:28:08 +00:00
d5c1f4e10a i18n - docs translations (#20297)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 18:54:42 +02:00
Paul RastoinandGitHub 3b180e7cb5 Fix root monorepo package json focused installation (#20292)
# Introduction
Running `yarn workspace focus twenty`( only installing root package.json
dependencies ) would fail because the yarn constraint expect the yarn
types to be installed
2026-05-05 15:13:31 +00:00
Abdul RahmanandGitHub 3d60e6dbfc Fix stale address coordinates after clearing autofill (#20264)
Closes #20082
2026-05-05 14:43:51 +00:00
e89b12488c i18n - translations (#20290)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 16:44:11 +02:00
31674253a1 i18n - translations (#20289)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 16:33:11 +02:00
633553f729 feat(sdk): add defineCommandMenuItem (#20256)
## Summary

- Add `defineCommandMenuItem` and `definePageLayoutWidget` as standalone
SDK defines, mirroring the existing `definePageLayoutTab` pattern. Both
entities can still be declared nested inside their parent
(`defineFrontComponent.command` / `definePageLayout.tabs[].widgets[]`).
- Add `CommandMenuItem` and `PageLayoutWidget` to the `SyncableEntity`
enum and the dev-mode UI labels.
- Wire the SDK manifest-build to extract the two new defines into
top-level `commandMenuItems` / `pageLayoutWidgets` arrays on the
manifest, and the server aggregator to consume them through the existing
flat-entity converters.
- On the server, expose `Application.commandMenuItems` (relation + DTO +
service hydration in `findOneApplication`).
- On the front, list command menu items in the application content tab
and add a dedicated detail page with a settings tab, mirroring how
`frontComponents` are surfaced.
- Add `twenty add` templates and Vitest unit tests for both new defines.
- Document the standalone-vs-nested pattern in
`packages/twenty-sdk/README.md`.

### Why

Until now, command menu items could only be declared as the nested
`command:` field on `defineFrontComponent` — there was no way to
register a command menu item from a separate file or from another
package. The `SyncableEntity` enum had 12 values, while the server
already synced 18 (including `commandMenuItem` and `pageLayoutWidget`).
The same gap existed for `pageLayoutWidget`, which had no top-level
define despite being synced server-side. This PR closes both gaps and
aligns the SDK surface with what the server actually accepts.

The standalone defines coexist with the nested form — pick one per
entity, never both with the same `universalIdentifier` (the manifest
aggregator will throw on duplicates). The README now documents this.

## Test plan

- [x] `npx nx typecheck twenty-sdk` / `twenty-server` / `twenty-front`
- [x] `npx nx lint:diff-with-main twenty-front` / `twenty-server`
- [x] `npx nx lint twenty-sdk` / `twenty-shared`
- [x] New unit tests: `define-command-menu-item.spec.ts`,
`define-page-layout-widget.spec.ts`
- [x] Existing manifest extract config tests still pass
- [ ] Codegen `npx nx run twenty-front:graphql:generate
--configuration=metadata` should be re-run after merge — the generated
`graphql.ts` was patched manually to include `commandMenuItems` on
`Application` and the `FindOneApplication` document.
- [ ] Smoke test: scaffold an app with `twenty add` for both new entity
types, run `twenty dev`, confirm the dev UI shows them in the sync list
and the settings page surfaces command menu items in the content tab.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 14:16:04 +00:00
neo773andGitHub 4dd08097ce CalDAV refactor (#20180)
Original CalDAV driver was written almost a year ago and code quality,
patterns were not up to the mark including having no test coverage, this
PR does the following:

- Splits the monolithic driver into isolated utilities with test
coverage

- Adds support for syncing legacy servers by checking if server supports
`syncCollection` and branches into two sync methods
`fetchEventsViaSyncCollection` or `fetchEventsViaCtagEtag` with this I
believe our driver is feature complete

Real testing report

| Provider  | Server            | Sync method          | Auth   |
| --------- | ----------------- | -------------------- | ------ |
| iCloud    | Apple's CalDAV    | sync-collection      | Basic  |
| Nextcloud | sabre/dav         | sync-collection      | Basic  |
| all-inkl  | sabre/dav (older) | ctag + etag fallback | Digest |
2026-05-05 14:15:05 +00:00
65ba36d475 i18n - translations (#20286)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 15:31:52 +02:00
Charles BochetandGitHub 2a4db16970 fix(website-new): inherit test target so twenty-shared builds in CI (#20285)
## Summary
The new `CI Website` workflow added in #20281 fails on the `test` matrix
job because tests cannot resolve `twenty-shared/translations` — a
subpath that requires `twenty-shared` to be built first.

Root cause: `packages/twenty-website-new/project.json` fully overrides
the `test` target, duplicating the executor/options/configurations from
`nx.json` `targetDefaults` but **losing `dependsOn: ["^build"]`** (and
`inputs` / `cache`). As a result, `nx affected -t test` for
`twenty-website-new` does not build `twenty-shared` first.

`twenty-front` works because its `project.json` declares `"test": {}`
and inherits the full default. This PR does the same for
`twenty-website-new`.

Verified the diagnosis from the failing run — `front-task (test)` logs
show `nx run twenty-shared:build` is invoked transitively, while
`website-task (test)` logs do not, leading to the missing-module error.
2026-05-05 15:30:56 +02:00
neo773andGitHub d040756fcf remove direction from messages (#20026)
This was a leftover column removed in
https://github.com/twentyhq/twenty/pull/6743 but was accidentally added
again when we migrated to `buildMessageStandardFlatFieldMetadatas` from
workspace decorator

/closes #20011
2026-05-05 15:24:25 +02:00
e50adaff2d feat(sdk): give Docker-not-running error an actionable next step (#20280)
## Summary

The current Docker-not-running message is unhelpful in two ways:

1. It doesn't tell users **how** to start Docker
2. "try again" is meaningless because a first-time user doesn't yet know
the command they just ran (they got here from `create-twenty-app`, not
from typing `yarn twenty server start` themselves)

**Before:**
```
Docker is not running. Please start Docker and try again.
```

**After (macOS example):**
```
Docker is not running.

Start Docker:
  Run: open -a Docker
  (or launch Docker Desktop from Applications)

Then retry:
  yarn twenty server start

Don't have Docker? Install from https://docs.docker.com/get-docker/
```

The platform-specific line is detected via `process.platform`:
- `darwin` → `open -a Docker` + Docker Desktop fallback
- `linux` → `sudo systemctl start docker` + Docker Desktop fallback
- `win32` → "Launch Docker Desktop from the Start menu"
- other → link to install docs

The retry command is computed at the call site so it preserves the
user's actual flags — `yarn twenty server start --test`, `yarn twenty
server upgrade 2.2.0 --test`, etc.

## Why

This came out of shadowing a first-time app developer who hit this error
during `npx create-twenty-app`. They were stuck — the CLI told them to
"try again" but they had only learned two commands so far
(`create-twenty-app` and `yarn dev`), neither of which was the right
one. Improving the message turns the error into a teaching moment.

## Test plan
- [x] `npx nx typecheck twenty-sdk` passes
- [x] `npx nx lint twenty-sdk` passes
- [x] Manually verified rendered output for both `server start` and
`server upgrade` flows on macOS
- [ ] Verify message renders correctly on Linux/Windows in practice

## Possible follow-ups (out of scope)

- Auto-launch Docker Desktop on macOS if installed (changes user state —
separate PR)
- Make the multi-line CLI error printer style only the first line in
red, so guidance reads as default text rather than red

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:24:12 +02:00
f9a24072b7 docs: restructure Getting Started around three explicit phases (#20283)
## Summary

Restructures the apps Getting Started doc around the three things a
developer actually has to do, so the mental model is visible upfront and
discoverable when something goes wrong.

**Why this matters:** the previous flow read as one continuous list of
bash commands and prompts, which made it easy to miss that scaffolding,
running a Twenty server, and live-syncing changes are three separate
concepts. When the user hits a failure (Docker not running, server not
up, auth not authorized), they have no mental map for which step they're
in — so they end up retrying `yarn twenty dev`, which is the only
command they remember.

## What changes


**[getting-started.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/developers/extend/apps/getting-started.mdx):**
- New summary table at the top showing the three-phase arc:

  | Phase | What you do | Tool | Result |
  |---|---|---|---|
| **1. Scaffold** | Generate the app's source code | `npx
create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker +
`yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` |
Your changes appear in the UI |

- Three top-level sections, one per phase, each ending with **"After
this phase: you have X"** so users can self-diagnose where they got
stuck.
- Phase 2 leads with the sentence that was missing before: *"Your app
needs a Twenty server to sync into. The server is a full Twenty instance
— UI, GraphQL API, PostgreSQL — running locally in Docker."* This is the
concept new users were missing.
- Removed the standalone *What are apps?* section — that's what the Core
Concepts page is for. Don't duplicate.
- Tightened wording throughout; same screenshots, same callouts, same
content depth.


**[core-concepts/apps.mdx](https://github.com/twentyhq/twenty/blob/docs/restructure-getting-started-three-phases/packages/twenty-docs/getting-started/core-concepts/apps.mdx):**
- Removed the install snippet (`npx create-twenty-app`, `cd`, `yarn
twenty dev`) — it duplicated Getting Started and the two examples used
different directory names.
- Updated the link card to reflect the new three-phase structure.

## Out of scope (mentioned for context, not done here)

- The "Docker is not running" message rewrite: separate PR
([#20280](https://github.com/twentyhq/twenty/pull/20280)).
- A `yarn twenty start` one-command bootstrap that auto-starts the
server before `dev`. Worth doing — keeping it out of this docs PR.
- Auto-offering to start the server when `yarn twenty dev` finds no
running one. Same.
- An "agent path" doc (single-page, imperative, for AI assistants) —
separate effort.

## Test plan
- [x] `npx nx lint twenty-docs` passes (no new warnings)
- [x] All `<Note>`, `<Warning>`, `<Card>`, image refs preserved
- [ ] Render and click through both pages once merged and previewed

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:23:27 +02:00
e6125c0e0d feat(sdk): move catalog-sync under server group (#20282)
## Summary

`catalog-sync` is a server-side admin action — it asks the connected
Twenty server to refresh its marketplace catalog from npm. It doesn't
operate on the local app code (like `build`, `deploy`, `publish`), so
having it sit at the same root level as those commands is a navigability
problem. With 13 commands at the root today, every needless one makes
the help output harder to scan.

This PR moves it under `server`:

```
# New (preferred)
yarn twenty server catalog-sync
yarn twenty server catalog-sync --remote production

# Old (still works, prints deprecation warning)
yarn twenty catalog-sync
```

Also slightly broadens the `server` group description from "Manage a
local Twenty server instance" to "Manage a Twenty server (local instance
and server-side actions)" since `catalog-sync` can target a remote.

## Help output (after)

```
$ yarn twenty --help
Commands:
  ...
  catalog-sync [options]   [Deprecated] Moved under server. Use `yarn twenty server catalog-sync`.
  ...
  server                   Manage a Twenty server (local instance and server-side actions)

$ yarn twenty server --help
Commands:
  start [options]              Start a local Twenty server
  stop [options]               Stop the local Twenty server
  logs [options]               Stream Twenty server logs
  status [options]             Show Twenty server status
  reset [options]              Delete all data and start fresh
  upgrade [options] [version]  Upgrade the twenty-app-dev Docker image
  catalog-sync [options]       Trigger a marketplace catalog sync on the server
```

## Backwards compatibility

The top-level `yarn twenty catalog-sync` still works and runs the same
logic. It prints a yellow warning suggesting the new path, then executes
normally. Plan is to remove it in a future release.

## Test plan
- [x] `npx nx typecheck twenty-sdk` passes
- [x] `npx nx lint twenty-sdk` passes
- [x] `yarn twenty --help` shows the deprecated entry
- [x] `yarn twenty server --help` lists the new subcommand
- [x] `yarn twenty catalog-sync --help` shows the deprecation message in
the description
- [ ] End-to-end: invoking either path triggers a sync against a running
server

## Possible follow-ups

This is one slice of the bigger CLI flattening discussed offline. Other
natural moves: group `build/deploy/publish/install/uninstall/typecheck`
under an `app` group, group `add/exec/logs` under `entity`. Doing those
in their own PRs to keep blast radius small.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:23:08 +02:00
Paul RastoinandGitHub 88394c25ef Bump twenty current version (#20241)
# Introduction
This PR introduces a workflow and nx command that allow bumping to a
given version or incrementing the current `TWENTY_CURRENT_VERSION`

Combined with accurate on point cd triggered and CI upgrade sequence
guard mutation workflow the window where a PR can corrupt an already
released twenty version is mitigated
2026-05-05 13:05:11 +00:00
Abdul RahmanandGitHub 90a1a9274f fix: show pinned commands in side panel search results (#20265)
Discord issue:
https://discord.com/channels/1130383047699738754/1498996539530412053
2026-05-05 12:58:40 +00:00
660f246076 i18n - translations (#20284)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 15:04:23 +02:00
53fdac1417 feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary

Replaces the bolted-on `isTool` + `toolInputSchema` fields on
`LogicFunctionManifest` with two distinct, opt-in triggers that align
with the existing `cron` / `databaseEvent` / `httpRoute` trigger
pattern:

- **`toolTriggerSettings`** — exposes the function as an AI tool (chat /
MCP / function calling). Uses standard JSON Schema (the format LLMs
natively understand).
- **`workflowActionTriggerSettings`** — exposes the function as a step
in the visual workflow builder. Uses Twenty's rich `InputSchema` so the
builder can render proper `FieldMetadataType`-aware editors, variable
pickers, labels, and an optional `outputSchema`.

A function can opt into none, one, or both. Each surface gets the schema
format appropriate for it.

### Why

`isTool: true` previously exposed the function as both an AI tool AND a
workflow node, with the same JSON Schema feeding both — but the workflow
builder really wants Twenty's `InputSchema` (with `CURRENCY`,
`RELATION`, `EMAILS`, etc.) and the AI surface really wants standard
JSON Schema. Today the workflow builder hacks around this by treating
JSON Schema as `InputSchema`, which silently breaks for any
non-primitive field type. Splitting the triggers fixes that and lets
each surface evolve independently.

### Migration

- **Fast** instance command adds the two new nullable columns.
- **Slow** instance command backfills `toolTriggerSettings` +
`workflowActionTriggerSettings` from `isTool=true` rows (preserving
today's both-surfaces behaviour) then drops the legacy columns.

### Stacked

Stacked on top of #20181. Merge that first, then this.

## Test plan

- [ ] CI green (oxlint, typecheck, jest, vitest)
- [ ] Run `--include-slow` upgrade against a workspace with existing
`isTool=true` logic functions; verify both new columns populated and old
columns dropped
- [ ] Verify AI chat sees migrated tool functions (Linear create-issue,
Exa search) and can call them with the JSON Schema
- [ ] Add an AI-tool function from the Settings UI (toggles
`toolTriggerSettings`) and verify it shows up in chat
- [ ] Add a workflow-action function from the Settings UI (toggles
`workflowActionTriggerSettings`) and verify it appears in the workflow
node picker
- [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify
input fields render (no more JSON-Schema-as-InputSchema hack)
- [ ] Try defining a function with no triggers in the SDK and verify
`defineLogicFunction` rejects it

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-05 14:56:09 +02:00
Charles BochetandGitHub 36452ecc8b fix: show 'Not shared' for RLS-hidden morph relation records (#20272)
## Summary

Follow-up to #20260. The `MorphRelationManyToOneFieldDisplay` component
(used for polymorphic MANY_TO_ONE relations) was missing the FK-presence
check that `RelationToOneFieldDisplay` already has.

When RLS hides a related record (e.g., a Rocket with a policy filtering
by name), the API response contains a populated FK
(`polymorphicOwnerRocketId`) but a `null` relation object. The component
was rendering an empty cell instead of the "Not shared" lock icon.

**Fix:**
- In `useMorphRelationToOneFieldDisplay`, read the record from the store
and check if any morph relation FK field is populated while the relation
value is null
- In `MorphRelationManyToOneFieldDisplay`, render
`<ForbiddenFieldDisplay />` when that condition is true

| Scenario | FK in response | Relation object | Frontend display |
|----------|---------------|-----------------|-----------------|
| Live record | "abc" | `{ id: "abc", ... }` | Record chip |
| Soft-deleted record | null | null | Empty cell |
| RLS-hidden record | "abc" | null | "Not shared" |

## Test plan

- Create a polymorphic MANY_TO_ONE relation (e.g., Pet → Rocket)
- Add an RLS policy on the target object (e.g., Rocket name contains
"Starship")
- Verify the morph relation field shows "Not shared" (lock icon) for
RLS-hidden records
- Verify live records still display normally as record chips
- Verify soft-deleted records still display as empty cells
2026-05-05 14:54:49 +02:00
Charles BochetandGitHub c983ac9f82 ci: add ci-website workflow for twenty-website-new (#20281)
## Summary
- Recreates the `ci-website.yaml` workflow that was removed alongside
`twenty-website` in #20270, now scoped to `twenty-website-new`.
- Replaces the old build-only job with a `[lint, typecheck, test]`
matrix run via `./.github/actions/nx-affected` on `tag:scope:website` —
same idiom used by `ci-shared.yaml`.
- Path filter watches `packages/twenty-website-new/**` and
`packages/twenty-shared/**` (since website-new depends on
`twenty-shared`), plus `package.json` / `yarn.lock`.

## Test plan
- [ ] CI Website workflow appears on this PR and the `lint`,
`typecheck`, `test` matrix jobs all pass
- [ ] `ci-website-status-check` is green
2026-05-05 14:54:11 +02:00
Paul RastoinandGitHub 820f97f53d [Headless Front component] Support multiple selected record (#20268)
# Introduction

Support multiple selected record ids for headless front components

### Changes

**Added:**
- `recordIds: string[]` field to `FrontComponentExecutionContext`
- `useRecordIds()` hook to get all selected record IDs

**Deprecated:**
- `recordId` field - use `recordIds` instead
- `useRecordId()` hook - use `useRecordIds()` instead

Backward compatibility is preserved
2026-05-05 14:53:22 +02:00
41a7d6928b docs: align example name to my-twenty-app across quickstarts (#20279)
## Summary

The example directory name in our scaffolding instructions was
inconsistent across docs:

| Source | Name used |
|--------|-----------|
| `create-twenty-app` README | `my-twenty-app` |
| Getting Started (developer docs) | `my-twenty-app` |
| Core Concepts → Apps (intro doc) | `my-app` ⚠️ |
| `twenty-sdk` README | `my-app` ⚠️ |

This means a user reading the high-level Apps intro sees `my-app`, then
the official Getting Started guide and the scaffold use `my-twenty-app`.
Small but eroding for confidence on the very first command.

This PR aligns the two outliers to `my-twenty-app`. The `twenty-my-app`
example in `publishing.mdx` is left alone — that's an npm package name
example, not a directory name (different concept).

## Test plan
- [x] `grep -rn "my-app\b"` over source docs returns no other
directory-name occurrences
- [ ] Verify rendered docs after merge

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 14:52:52 +02:00
neo773andGitHub 8d001eb33f fix: don't mark IMAP channel as failed on transient server errors (#20273)
Map RFC 5530 codes to `TEMPORARY_ERROR` so sync retries instead of
terminally flagging `FAILED_INSUFFICIENT_PERMISSIONS` when the server is
briefly unavailable.

prod Logs

```
	2026-05-05 03:53:42.129	
    authenticationFailed: true
	2026-05-05 03:53:42.129	
    serverResponseCode: 'UNAVAILABLE',
	2026-05-05 03:53:42.129	
    responseText: 'Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
	2026-05-05 03:53:42.129	
    response: '2 NO [UNAVAILABLE] Account is temporarily unavailable.',
	2026-05-05 03:53:42.129	
  cause: Error: Command failed
	2026-05-05 03:53:42.129	
  code: 'INSUFFICIENT_PERMISSIONS',
Caused by: Error: Command failed

[Nest] 35  - 05/04/2026, 10:23:42 PM   ERROR [ImapGetAllFoldersService] MessageImportDriverException: IMAP authentication error: Command failed
```
2026-05-05 14:30:57 +02:00
neo773andGitHub a3f2fafce6 fix smtp outbound persist message (#20276)
`APPEND` used display name `Sent` instead of `INBOX.Sent`
Fix is to use mailbox path, extreacted this as a utility, all services
are consistent now.

/closes #20267
2026-05-05 14:28:03 +02:00
nitinandGitHub ff65b5001d fix: show AI chat filter button only on hover in navigation drawer (#20274) 2026-05-05 14:25:29 +02:00
dd3b6f2a2f i18n - translations (#20278)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-05 14:11:21 +02:00
e3be1f4971 Make ConnectionProvider a true SyncableEntity (#20232)
## Summary

PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but
bypassing the standard sync pipeline — manifest sync called the bespoke
`ApplicationOAuthProviderService.upsertManyFromManifest()` instead of
going through the workspace-migration orchestrator like every other
SyncableEntity. Anything that assumed *"all SyncableEntity values flow
through the same pipeline"* (dev UI sync tracking, verification tooling)
was wrong about ConnectionProvider — that's the inconsistency this PR
closes.

This PR follows the `.cursor/skills/syncable-entity-*` guides
religiously, all six steps.

## What changes

**Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`)
- Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared)
- Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops
the ad-hoc columns since the base class provides them, adds `deletedAt`,
drops the old `(applicationId, universalIdentifier)` unique in favour of
SyncableEntity's `(workspaceId, universalIdentifier)`)
- `FlatConnectionProvider`, `FlatConnectionProviderMaps`,
`FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`,
`UniversalFlatConnectionProvider`, six action types
- Register in **all** the central registries:
`AllFlatEntityTypesByMetadataName`,
`ALL_METADATA_ENTITY_BY_METADATA_NAME`,
`ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`,
`ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`,
`ALL_METADATA_SERIALIZED_RELATION`,
`ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`,
`WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`),
`METADATA_EVENTS_TO_EMIT`
- `case 'connectionProvider':` in seven discriminated-union switches
(`derive-metadata-events-*`, `optimistically-apply-*`,
`enrich-create-*`)

**Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`)
- `WorkspaceFlatConnectionProviderMapCacheService` (extends
`WorkspaceCacheProvider`, decorated with `@WorkspaceCache`,
soft-delete-aware)
- `fromConnectionProviderEntityToFlatConnectionProvider` util
- `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util
- `FlatConnectionProviderModule` wires the cache service
- Wired the manifest converter into
`compute-application-manifest-all-universal-flat-entity-maps`

**Step 3 — Builder & Validation**
(`@syncable-entity-builder-and-validation`)
- `FlatConnectionProviderValidatorService` — never throws, returns error
arrays; uses indexed `byUniversalIdentifier` for the (name,
applicationUniversalIdentifier) uniqueness check (no
`Object.values().find()` on the hot path)
- `WorkspaceMigrationConnectionProviderActionsBuilderService`
- Registered in both validators-module + builder-module
- **Wired into the orchestrator** (the most-commonly-forgotten step per
the rule) — constructor inject, destructure
`flatConnectionProviderMaps`, `validateAndBuild`, append actions to the
final migration

**Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`)
- Three handlers (create / update / delete) using the canonical
`WorkspaceMigrationRunnerActionHandler` mixin
- Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule`

**Step 5 — Integration** (`@syncable-entity-integration`)
- Delete the `upsertManyFromManifest` bypass on
`ApplicationOAuthProviderService`
- Remove the bypass call from `ApplicationSyncService` — manifest sync
now flows through the standard pipeline
- Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule`
(no longer needed)
- Import `FlatConnectionProviderModule` from
`ApplicationOAuthProviderModule` to keep the cache discoverable
- 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`,
`CONNECTION_PROVIDER_NOT_FOUND`,
`CONNECTION_PROVIDER_NAME_ALREADY_EXISTS`

**Migration**
- Generated via `database:migrate:generate` (instance command
`1777896012579`): drops the old `(applicationId, universalIdentifier)`
unique constraint, adds `deletedAt` column, adds the `(workspaceId,
universalIdentifier)` unique index that `SyncableEntity` requires.
- Verified clean — a second `migrate:generate` pass produces zero drift.

**Step 6 — Tests** (`@syncable-entity-testing`)
- 3 new specs for the manifest converter (defaults, optional fields,
all-fields)
- All 32 existing OAuth-provider tests still pass
- ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven
only), so the GraphQL integration suite that other SyncableEntities ship
doesn't apply here

**Codegen**
- Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk)
against the live schema

## Why this matters

Before:
- `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum)
- But the entity didn't extend `SyncableEntity`
- And the manifest sync bypassed the standard pipeline
- → Verification tooling, dev UI sync tracking, anything iterating over
`ALL_METADATA_NAME` got inconsistent behaviour

After:
- `ConnectionProvider` is a `SyncableEntity` end-to-end
- Single sync path through the workspace-migration orchestrator (same as
`agent`, `skill`, `frontComponent`, `webhook`, …)
- One mental model

## Out of scope (deliberate)

- **Renaming the table** from `applicationOAuthProvider` to
`connectionProvider` — the `metadataName` is `connectionProvider` (what
consumers see in code); the table name is internal. A rename would
balloon this PR with mechanical churn unrelated to the sync-pipeline
wiring. Worth doing as a follow-up.
- **`applicationVariable` SyncableEntity conversion** — the other
manifest-sync holdout. Tracked in #20215.

## Test plan

- [ ] Migration up/down clean against fresh DB
- [ ] Install an app whose manifest declares connection providers —
providers appear in the workspace
- [ ] Re-deploy the app with one provider added, one removed, one
renamed → all reconciled correctly via the sync pipeline
- [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider
entries the same way it shows agents/skills/etc
- [ ] OAuth flow still works (existing connections, new connections,
reconnect, list/get from SDK) — should be unchanged since the runtime
code path didn't move

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:04:15 +02:00
Abdullah.andGitHub 59107b5b23 Remove twenty-website package. (#20270) 2026-05-05 12:45:02 +02:00
7c4302d02a fix: show empty cell instead of 'Not shared' for soft-deleted related records (#20260)
## Summary

Fixes #20076 (supersedes #20250)

When a related record is soft-deleted, the frontend displays "Not
shared" (lock icon) because it sees a populated FK but a null relation
object. This is misleading -- the record was deleted, not
permission-restricted.

**Backend fix** (`process-nested-relations-v2.helper.ts`):
- For MANY_TO_ONE relations, widen the relation query with
`.withDeleted()` and include `deletedAt` in the select
- In `assignRelationResults`, if the matched record has `deletedAt` set,
nullify both the FK and the relation object in the API response
- Records filtered by RLS are still not returned (even with
`withDeleted()`), so they correctly continue to show "Not shared"
- Strip `deletedAt` from relation results before returning to the client

**Frontend fix** (`RelationFromManyFieldDisplay.tsx`):
- For ONE_TO_MANY junction relations, return `null` instead of
`<ForbiddenFieldDisplay />` when junction records exist but target
records are unavailable

### Three cases now handled correctly:

| Scenario | FK in response | Relation object | Frontend display |
|---|---|---|---|
| **Live record** | `"abc"` | `{ id: "abc", ... }` | Record chip |
| **Soft-deleted record** | `null` | `null` | Empty cell |
| **RLS-hidden record** | `"abc"` | `null` | "Not shared" |

## Test plan

- [ ] Create a record with a MANY_TO_ONE relation (e.g., a person linked
to a company)
- [ ] Soft-delete the related record (the company)
- [ ] Verify the relation field shows an empty cell, not "Not shared"
- [ ] Restore the related record and verify the relation reappears
- [ ] Verify that RLS-hidden relations still show "Not shared"

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 09:32:16 +00:00
fda2295beb feat: expose upgrade status as Prometheus gauge metrics (#20262)
## Summary

- Adds `UpgradeGaugeService` that exposes three observable Prometheus
gauges based on the recently merged upgrade status service:
- `twenty_upgrade_instance_health` — 1 (up-to-date), 0 (behind), -1
(failed)
- `twenty_upgrade_workspaces_behind_total` — count of workspaces with
pending upgrade commands
- `twenty_upgrade_workspaces_failed_total` — count of workspaces with a
failed upgrade command
- Follows the existing gauge pattern (`WorkspaceGaugeService`,
`BillingGaugeService`, `DatabaseGaugeService`)

### Caching & QPS design

Prometheus scrapes every **15s** via `ServiceMonitor`. Each gauge uses
the `MetricsService.createObservableGauge({ cacheValue: true })` pattern
which caches the value in Redis for **60 seconds**. Under that,
`UpgradeStatusService.getInstanceAndAllWorkspacesStatus()` uses
`UpgradeStatusCacheService` with a **1-hour TTL** in Redis.

Result: at most 1 DB query per hour regardless of scrape frequency.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 07:56:13 +00:00
df63dbff05 fix: Invalid configuration instead of related notes (#20251)
## Summary
The Notes widget (and other record-bound widgets like Tasks, Files,
Calendar, Emails) incorrectly displayed "Invalid Configuration" when
rendered on dashboard or standalone page contexts. The root cause was an
overly broad `ErrorBoundary` that caught all runtime errors uniformly
and displayed a misleading error message.
## Related issue
Fixes: #20118 
## Problem Analysis
**Proximate Cause:**
- `NotesCard` calls `useTargetRecord()` which throws a generic `Error`
when `targetRecordIdentifier` is undefined
- `ErrorBoundary` in `WidgetCardShell.tsx` catches this error and
renders `PageLayoutWidgetInvalidConfigDisplay`
- This displays "Invalid Configuration" which is factually misleading

**Triggering Cause:**
- Commit 5cd8b7899d removed the feature flag gate on page layouts,
making them standard for all workspaces
- This exposed record-bound widgets to dashboard contexts where
`targetRecordIdentifier` is intentionally undefined

**Error Propagation Chain:**
```
WidgetContentRenderer → NoteWidget → NotesCard → useTargetRecord()
useTargetRecord() throws Error('useTargetRecord must be used within a record page context')
ErrorBoundary catches error → PageLayoutWidgetInvalidConfigDisplay renders misleading UI
```
## Solution
Introduced a distinction between **configuration errors** and **record
context requirement errors** by:
1. Creating a custom error class `RecordContextRequiredError`
2. Updating `useTargetRecord()` to throw this specific error type
3. Creating a dedicated display component for record context errors
4. Updating the `ErrorBoundary` fallback to handle error types
appropriately
## User Impact
| Before | After |
|--------|-------|
| "Invalid Configuration" (red badge) | "Record Required" (gray badge) |
| Misleading error message | Accurate context-aware message |
| Users think widget is broken | Users understand widget needs record
context |

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 21:10:40 +00:00
01b4754e62 New name not appearing when renaming "Stages" in data-model settings (#20246)
## Summary
- Resolve standard field `label`, `description`, and `icon` overrides
through dedicated GraphQL field resolvers.
- Fall back to the source locale safely when the request locale is
missing, and allow direct overrides to apply for non-source locales when
translations are absent.
- Enrich metadata subscription payloads for both `before` and `after`,
reusing the same override application path for field and object
metadata.
- Update and extend tests to cover the revised override behavior.

## Testing
- Updated unit coverage for standard override resolution, including the
non-source-locale fallback path.
- Not run (not requested).

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 20:16:00 +00:00
e6399b180e Fix/workspace member avatars 20193 (#20200)
Fixes #20193

**Bug Description:**
Previously, workspace member avatars failed to render correctly in table
views and relation chips (such as the Account Owner field). While the
avatar picker dropdown correctly fetched fresh GraphQL data, table views
and chips relied on the cached defaultAvatarUrl or avatarUrl fields,
which were frequently resolving to empty strings or failing to parse
external OAuth URLs correctly.

**Root Cause:**

- Empty String Defaults: Deleting an avatar or failing to retrieve one
defaulted the database state to an empty string ("") instead of null,
which caused frontend image components to break rather than render their
fallback states.

- Missing Permanent URLs: The WorkspaceMemberTranspiler was strictly
expecting internal signed URLs. If an avatar was an external OAuth URL,
it incorrectly returned an empty string, breaking SSO profile pictures.

- Missing Fallbacks: New users lacked a proper Gravatar fallback
assignment upon workspace creation.

**Changes Made:**

- user-workspace.service.ts: Updated the avatar computation logic during
user creation to implement a reliable Gravatar fallback and correctly
set missing avatars to null instead of empty strings. Updated the
storage to use permanent file URLs.
- file-url.service.ts: Implemented a getRawFileUrl method to support
rendering permanent, non-expiring file URLs for avatars.
- workspace-member-transpiler.service.ts: Refactored the URL
transpilation logic to gracefully pass through external OAuth URLs
(e.g., Google/Microsoft profile pictures) instead of stripping them.
- WorkspaceMemberPictureUploader.tsx: Fixed the frontend removal logic
so that deleting a profile picture sets the avatarUrl to null
(consistent with the backend) rather than an empty string.

**Testing:**

- Verified that avatars correctly display in relation chips and table
views.
- Verified that external OAuth avatars load properly.
- Verified that deleting an avatar correctly resets the UI to the
fallback initials component.

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:42:12 +00:00
8c2885f9ed i18n - docs translations (#20248)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 20:52:46 +02:00
a76047f28b i18n - docs translations (#20243)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 18:53:30 +02:00
Sri Hari Haran SharmaandGitHub 281eaa3721 fix rest filter default conjunction detection (#20133)
Fixes #20128 

## Summary

Fix REST API filter parsing when bare filters are mixed with explicit
conjunctions.

## What changed

- Replaced the loose parentheses check in
`addDefaultConjunctionIfMissing` with proper root conjunction detection.
- Shared the root conjunction regex with `parseFilter`.
- Added regression tests for mixed filters like
`status[eq]:'TODO',and(title[ilike]:'%test%')`.

## Validation

- `npx nx test twenty-server
--testPathPatterns=add-default-conjunction.util.spec.ts --runInBand
--coverage=false`
- `npx prettier --check ...`
2026-05-04 16:18:56 +00:00
martmullandGitHub c804f27846 Add check for manifest uuid version (#20239)
As title

<img width="1059" height="203" alt="image"
src="https://github.com/user-attachments/assets/c6840c4e-792b-45da-b450-addd77af0de7"
/>
2026-05-04 16:06:50 +00:00
martmullandGitHub 54e22423df Improve twenty deploy cli logs (#20237)
## Before
<img width="1074" height="562" alt="image"
src="https://github.com/user-attachments/assets/a2fbe902-d34e-40e4-87c9-f344a06fd6ae"
/>

## After

<img width="1107" height="605" alt="image"
src="https://github.com/user-attachments/assets/af78276a-f4c7-42f9-9347-01d562b1a779"
/>
2026-05-04 15:25:49 +00:00
a0dd7d9e22 i18n - translations (#20240)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 17:31:07 +02:00
97ec720d2c fix: show active advanced filter count badge in dropdown button (#20229)
## Summary

Replaces the hardcoded `0` in
`ViewBarFilterDropdownAdvancedFilterButton` with the actual count of
active advanced filter rules, matching the behavior of
`AdvancedFilterChip` in the view bar.

## What changed

In
`packages/twenty-front/src/modules/views/components/ViewBarFilterDropdownAdvancedFilterButton.tsx`:

- Imported `useAtomComponentSelectorValue` and
`rootLevelRecordFilterGroupComponentSelector`
- Imported `useChildRecordFiltersAndRecordFilterGroups`
- Replaced `const advancedFilterQuerySubFilterCount = 0; // TODO` with
the real computed count via the same hook pattern used in
`AdvancedFilterChip.tsx`

The pill badge will now appear on the "Advanced filter" dropdown menu
item showing the number of active advanced filter rules (e.g. "2" when
two rules are active).

## References

- Fixes #20207

---------

Co-authored-by: wadeKeith <wade@twenty.app>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 15:08:30 +00:00
Thomas TrompetteandGitHub 95a34d8517 Return false instead of throwing when event stream does not exist (#20165)
## Summary

- When an event stream expires (TTL), `addQueryToEventStream` and
`removeQueryFromEventStream` now return `false` instead of throwing
`EVENT_STREAM_DOES_NOT_EXIST` as an `InternalServerError`
- Frontend checks the mutation return value and triggers the
destroy/recreate cycle, same recovery behavior without the error path
- Removes `EVENT_STREAM_DOES_NOT_EXIST` from exception code, exception
filter, and frontend graceful error check since it's no longer thrown

## Test plan

- [x] Verify that when an event stream TTL expires, the frontend
silently recreates the stream without error noise in logs/Sentry
- [x] Verify that `NOT_AUTHORIZED` errors still throw correctly on both
mutations
- [ ] Verify that the subscription `onEventSubscription` still works
end-to-end with stream creation, query registration, and heartbeat TTL
refresh

Made with [Cursor](https://cursor.com)
2026-05-04 15:06:18 +00:00
MarieandGitHub 4852ac401a Add server upgrade status on admin panel (#20107)
## Summary

Adds an admin upgrade-status panel that surfaces per-instance and
per-workspace migration health, backed by a Redis-cached aggregate to
keep the page snappy on large fleets.

<img width="827" height="880" alt="Screenshot 2026-04-28 at 10 21 03"
src="https://github.com/user-attachments/assets/8f88baa9-7268-4eff-bf6a-906a7f06ca91"
/>
<img width="804" height="892" alt="Screenshot 2026-04-28 at 10 21 11"
src="https://github.com/user-attachments/assets/1e6decf8-766a-4d0e-96b1-03a9962bba3c"
/>


## Computed metrics

**Instance** (`InstanceUpgradeStatus`)
- `inferredVersion` — version derived from the latest non-initial
instance command name
- `health` — `upToDate` | `behind` | `failed`, derived from the latest
attempt vs. the last expected instance step in the upgrade sequence
- `latestCommand` — `{ name, status, executedByVersion, errorMessage,
createdAt }` from the most recent attempt

**Per-workspace** (`WorkspaceUpgradeStatus`)
- `workspaceId`, `displayName`
- `inferredVersion`, `health`, `latestCommand` (same shape as instance),
computed against the latest expected step in the sequence

**Aggregate** (`AllWorkspacesUpgradeStatus`, only across `ACTIVE` /
`SUSPENDED` workspaces)
- `instanceUpgradeStatus`
- `totalCount`, `upToDateCount`, `behindCount`, `failedCount`
- `workspacesBehindIds[]`, `workspacesFailedIds[]`
- `computedAt`

## Fetching strategy

All reads go through `UpgradeStatusCacheService` (cache namespace:
`EngineHealth`).

- **Aggregate read** (`getAllWorkspacesStatus` →
`getAllWorkspacesUpgradeStatus` query):
reads summary + behind-ids + failed-ids in parallel; if any of the three
keys is missing, full recompute (`recomputeAllWorkspaces`) is triggered,
which also primes per-workspace entries.
- **Per-workspace read** (`getWorkspacesStatus(ids)` →
`getUpgradeStatus(ids)` query):
`mget` on workspace keys; misses are recomputed individually
(`recomputeWorkspace`), and aggregates are reconciled in place (count +
id list deltas) without a full recompute.
- **Recompute on demand**: `refreshUpgradeStatus` mutation calls
`recomputeAllWorkspaces` to bypass cache and rewrite all keys.
- **Auto-invalidation**: `InstanceCommandRunnerService` (fast + slow
paths) and `WorkspaceCommandRunnerService` invalidate after every run
via `safeInvalidateUpgradeStatusCache()`
(`flushByPattern('upgrade-status:*')`). Failures in cache invalidation
are swallowed and logged so they never break the migration runner.
- **TTL**: `60 * 60 * 1000` ms (1 hour) on every key — protects against
stale data even if a runner crashes before invalidating.

## Introduced cache keys

All under the `EngineHealth` cache-storage namespace:

| Key | Type | Purpose |
| --- | --- | --- |
| `upgrade-status:all-workspaces:summary` |
`CachedAllWorkspacesStatusSummary` | Counts + instance status +
`computedAt` |
| `upgrade-status:all-workspaces:behind-ids` | `string[]` | Workspace
ids in `behind` state |
| `upgrade-status:all-workspaces:failed-ids` | `string[]` | Workspace
ids in `failed` state |
| `upgrade-status:workspace:<workspaceId>` |
`CachedWorkspaceUpgradeStatus` | Per-workspace status (one key per
workspace) |

Full invalidation uses the pattern `upgrade-status:*`.

## Index added on `upgradeMigration` (already added on prod)

Migration
`2-2-instance-command-fast-1777308014234-addUpgradeMigrationWorkspaceIdIndex.ts`:

```sql
CREATE INDEX "IDX_upgradeMigration_workspaceId_name_attempt"
  ON "core"."upgradeMigration" ("workspaceId", "name", "attempt")
  WHERE "workspaceId" IS NOT NULL;
2026-05-04 15:06:07 +00:00
1cd983a330 fix: handle missing file entity in avatar deletion listener (#20192)
## Problem
When a `workspaceMember` is updated (e.g., theme/locale/avatar changes),
the `WorkspaceMemberAvatarFileDeletionListener` triggers file deletion.
If the referenced file entity doesn't exist in the database, an
unhandled `EntityNotFoundError` crashes the NestJS server, causing a 502
loop.

## Change
Wrap the file deletion call in a try-catch that gracefully handles
`EntityNotFoundError` as a no-op — if the file doesn't exist, there's
nothing to delete.

Fixes #20191.

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

Co-authored-by: martmull <martmull@hotmail.fr>
2026-05-04 14:58:56 +00:00
37ca09e8f9 i18n - docs translations (#20238)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 17:06:43 +02:00
91124a3cb8 AI - Add azure foundry provider (#20170)
[Merge this before](https://github.com/twentyhq/twenty-infra/pull/655)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-04 14:45:06 +00:00
Paul RastoinandGitHub 41ad63a8ab [DockerFile] Optimize twenty-server deps and build (#20132)
# Introduction

Aiming for faster cd process

## Splitting front end server deps
Reduce dependencies bloating when target is server only, installing only
root repo dev deps and server dev and prod deps

Still pruning before copying to prod node_modules

## Server only remove twenty-ui

Also removing twenty-ui from server build as it was not consumed at all

Depends on https://github.com/twentyhq/twenty/pull/20140
2026-05-04 14:24:52 +00:00
9ddf9af4c4 i18n - translations (#20236)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 16:18:30 +02:00
3ffda0a29e Add twenty version validation (#20227)
as title, server version is checked before app deploy, and app install
commands

### New section in publishing doc
<img width="1344" height="912" alt="image"
src="https://github.com/user-attachments/assets/2a9335e7-0a7a-4973-a2db-f30f03181001"
/>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:55:31 +00:00
WeikoandGitHub 0bb345b75f Fix empty record page on system objects for non-English workspace members (#20235)
## Context
Since #19890 (Translate standard page layouts), the server's
`PageLayoutTab.title` resolver translates standard tab titles at query
time. A workspace member in French viewing a `messageChannel` (or any
system object with a standard page layout) receives `tab.title =
"Accueil"` / `"Chronologie"` instead of `"Home"` / `"Timeline"`.

The `SYSTEM_OBJECT_TABS` guard in `PageLayoutTabsRenderer` was comparing
against an English-only literal allow-list, so every tab was dropped,
`sortedTabs` became empty, and `<PageLayoutMainContent />` never mounted
— the record page rendered blank (no fields, timeline, email thread,
etc.).

## Fix
Only run the allow-list filter when the resolved layout is the synthetic
`DEFAULT_RECORD_PAGE_LAYOUT` (the client-side fallback for the few
system objects with no server-side standard page layout config, e.g.
`workspaceMember`, `attachment`, `message`). That layout ships hardcoded
English tabs, so the English allow-list still works in every locale.

System objects that do have a server-side standard page layout
(`messageChannel`, `connectedAccount`, `workflowRun`, …) are no longer
filtered at all, the server only ever persists Home/Timeline/Flow tabs
for them, so no filter is needed.

## Before
<img width="1262" height="722" alt="Screenshot 2026-05-04 at 15 15 54"
src="https://github.com/user-attachments/assets/348c9e9d-0ae1-4046-8ead-470ed8263cb5"
/>


## After
<img width="1281" height="658" alt="Screenshot 2026-05-04 at 15 15 36"
src="https://github.com/user-attachments/assets/7a9953b1-7320-4f1a-8c02-1688d3eda3ae"
/>


## Note
Next step should be to backfill those system objects with real record
page layouts so we can remove this filter logic
2026-05-04 13:36:36 +00:00
3c7c62c79f fix(server): deduplicate @opentelemetry/api to fix NoopMeterProvider (#20231)
## Summary

**All OTel metrics in twenty-server have been silently dropped since
April 30.**

### Root cause

PR #20149 (`bump @sentry/profiling-node 10.27→10.51`) pulled in
`@sentry/node@10.51.0`, which declares `@opentelemetry/api: ^1.9.1` as a
**dependency** (not peer). Yarn installed it as a **nested** copy at
`1.9.1`, while the hoisted copy stayed at `1.9.0`.

At startup in `instrument.ts`:
1. `Sentry.init()` uses the **nested `1.9.1`** to register `trace`,
`propagation`, `context` on the OTel global → global version becomes
**`1.9.1`**
2. `setGlobalMeterProvider()` uses the **hoisted `1.9.0`** →
`registerGlobal` sees version mismatch (`1.9.1` ≠ `1.9.0`) → **silently
returns `false`**
3. Global stays `NoopMeterProvider` → every counter, gauge, and
histogram in the server is a no-op

### What this PR does

1. **Reverts three troubleshooting PRs** that are no longer needed now
that the root cause is identified:
   - #20230 — heartbeat gauge
   - #20228 — OTLP export lifecycle logs
- #20221 — Sentry revert to 10.27 (which never actually downgraded in
`yarn.lock` since `^10.27.0` resolved to `10.51.0`)

2. **Fixes the root cause**:
- Root Yarn resolution pinning `@opentelemetry/api` to `1.9.1` → single
copy in the entire tree, Sentry and Twenty share the same instance
- Named import in `instrument.ts` (`import { metrics as otelMetrics }`
instead of default import) as defense-in-depth against CJS interop
issues

### Verified on dev cluster

Exec'd into the running pod and confirmed:
- `@sentry/node` nests `@opentelemetry/api@1.9.1`, hoisted is `1.9.0`
- `Sentry.init()` → global version `1.9.1` → `setGlobalMeterProvider`
with VERSION `1.9.0` → returns `false` → `NoopMeterProvider`
- Same-version registration returns `true` → `MeterProvider` ✓

## Test plan
- [ ] CI passes (lint, typecheck, build)
- [ ] Deploy to dev cluster and verify metrics flow to collector
- [ ] Confirm `node_modules/@opentelemetry/api/package.json` shows
`1.9.1` with no nested copy under `@sentry/`

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 15:15:00 +02:00
Paul RastoinandGitHub 596ce32bd6 Fix front unit test on main (#20233)
Jest mocks runs before the const is defined
2026-05-04 15:14:43 +02:00
d2cfbf319b [Website] Implement translations. (#20171)
As title.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:41:46 +00:00
a3cf565ba3 feat(server): add heartbeat gauge + first-export-attempt log for OTLP (#20230)
## Summary
- Adds a trivial always-on observable gauge (`twenty.heartbeat = 1`) so
the OTLP exporter fires on every 10s collection tick, even on idle pods.
Without this, `PeriodicExportingMetricReader` skips the export when
`scopeMetrics` is empty, so the process-log wrapper never runs and we
can't prove OTLP connectivity.
- Adds a one-time "first periodic export attempt" log line inside the
wrapped exporter, completing the startup log sequence: `OTLP reader
enabled` → `first periodic export attempt` → `first export ok` / `export
failed`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 12:30:59 +02:00
Abdul RahmanandGitHub fa8304d323 Fix record table dashboard save (#20202)
## Summary

Fixes `METADATA_VALIDATION_FAILED` / “view field already exists” when
saving dashboard **record table** widgets after the first persist (e.g.
several table widgets on one dashboard).

## Problem

Draft view columns used **client-generated** `viewField` ids. After
save, the API stored **different** ids. The next upsert still sent the
old draft ids as `viewFieldId`. The server only matched on that id,
missed every row, and tried to **create** columns that already existed
for the same `fieldMetadataId` + view.
2026-05-04 09:57:21 +00:00
467ddaa27a feat(server): OTLP metrics export logs for troubleshooting (#20228)
## Summary

Adds **grep-friendly** `console` logging around the OpenTelemetry
metrics OTLP exporter in
[`packages/twenty-server/src/instrument.ts`](packages/twenty-server/src/instrument.ts)
so production / staging can confirm whether the app is exporting metrics
and why exports fail.

## Log format

- Prefix: **`[Twenty OTEL metrics]`** (easy to filter in Loki / `kubectl
logs | grep`).
- **Startup:** whether the OTLP reader is enabled, `exportIntervalMs`,
and endpoint as `protocol//host/path` only (no credentials).
- **First successful export:** one `console.log` per process (`first
export ok`) with metric data point count — avoids spamming every 10s.
- **Each failed export:** `console.warn` with result code, point count,
and serialized error (including nested `AggregateError` causes when
present).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:44:51 +02:00
fc4cf7fe09 i18n - translations (#20226)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-04 11:34:51 +02:00
9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

App developers can now declare third-party OAuth integrations (GitHub,
Linear, Slack, etc.) in their manifest and the platform handles the full
authorize → callback → token-exchange → refresh → injection lifecycle.
The dev writes ~10 lines of config and reads tokens via
`useOAuth('linear')` inside any logic function.

```ts
// app/src/oauth-providers/linear.ts
export default defineOAuthProvider({
  universalIdentifier: '...',
  name: 'linear',
  displayName: 'Linear',
  authorizationEndpoint: 'https://linear.app/oauth/authorize',
  tokenEndpoint: 'https://api.linear.app/oauth/token',
  scopes: ['read', 'write'],
  connectionMode: 'per-user',
  clientIdVariable: 'LINEAR_CLIENT_ID',
  clientSecretVariable: 'LINEAR_CLIENT_SECRET',
  tokenRequestContentType: 'form-urlencoded',
});

// app/src/logic-functions/handlers/...
const { accessToken } = useOAuth('linear'); // throws OAuthNotConnectedError if missing
```

## Architecture

- **Storage**: extends the existing `connectedAccount` table — new
nullable `applicationOAuthProviderId` FK + new `app` value on the
`ConnectedAccountProvider` enum. Existing Google/Microsoft flows are
untouched.
- **OAuth flow**: a single `/apps/oauth/authorize` +
`/apps/oauth/callback` controller pair handles every app provider. State
travels in a JWT signed via the existing `JwtWrapperService` (new
`APP_OAUTH_STATE` token type).
- **Token exchange**: goes through
`SecureHttpClientService.createSsrfSafeFetch()` (so an installed app
can't point `tokenEndpoint` at internal hosts).
- **Refresh**: piggybacks on the existing
`ConnectedAccountRefreshTokensService` dispatch — Google/Microsoft
drivers untouched, new app driver lives engine-side under
`application-oauth-provider/refresh/`.
- **Injection**: the executor injects refreshed tokens as env vars
(`OAUTH_<NAME>_ACCESS_TOKEN`, `_HANDLE`, `_SCOPES`, `_CONNECTED`); the
SDK helpers `useOAuth` / `useOptionalOAuth` read them.
- **Frontend**: auto-rendered "OAuth Connections" section under each
app's settings tab (no custom front component needed). App-managed
connections are filtered out of `/settings/accounts` so the
email/calendar page stays focused.
- **Disconnect**: best-effort revoke against the manifest's
`revokeEndpoint` before deleting the row.

## Reference app

`packages/twenty-apps/internal/twenty-linear/` exercises the full
pipeline:

- `defineOAuthProvider` for Linear
- `POST /linear/create-issue` and `GET /linear/teams` HTTP-route logic
functions
- Vitest tests for the handlers

## Tests

- 14 server-side Jest tests: token-exchange util (form-urlencoded vs
JSON, PKCE, error paths), flow service (authorize URL shape, state
binding, ConnectedAccount upsert on first/reconnect, per-workspace mode,
invalid state)
- 8 app-level Vitest tests: handler error paths, GraphQL request shape,
Linear error propagation
- All 4 packages clean: `npx nx lint:diff-with-main` and `npx tsc
--noEmit`

## Test plan

- [ ] Apply migration on a dev DB: `npx nx run
twenty-server:database:migrate:prod`
- [ ] Regenerate frontend types: `npx nx run
twenty-front:graphql:generate --configuration=metadata`
- [ ] Create a Linear OAuth app at
https://linear.app/settings/api/applications/new with redirect URI
`<SERVER_URL>/apps/oauth/callback`
- [ ] Deploy + install `twenty-linear` on a workspace, paste the Linear
client id/secret into the app's variables
- [ ] Click "Connect Linear" in the app's settings tab → complete OAuth
→ verify `connectedAccount` row created with `provider = 'app'`
- [ ] Trigger `POST /linear/create-issue` with a valid teamId → verify
issue lands in Linear
- [ ] Disconnect → verify the row is deleted and (if Linear's revoke
endpoint is configured in the manifest) the revoke call fires
- [ ] Verify `/settings/accounts` does NOT show the Linear connection —
it appears only under the Linear app's settings tab

## Out of scope (deliberately)

- **Cron + per-user providers**: a cron-triggered function with a
per-user OAuth provider currently returns `CONNECTED=false` (no user
context). The follow-up design is `useOAuthForUser(name,
userWorkspaceId)` paired with a `POST /apps/oauth/connection-token`
endpoint, deferred to keep this PR focused.
- **Token encryption at rest**: tokens stored as plain `varchar`
matching the existing Google/Microsoft pattern. Worth a separate
cross-cutting PR.
- **Manifest endpoint pinning**: a malicious app upgrade could change
`tokenEndpoint` silently. Same trust model as logic-function source code
(which already runs arbitrary server-side); worth tightening across the
whole upgrade pipeline rather than just OAuth.
- **CLI helpers** (`twenty oauth show-callback-url`, `twenty oauth
connect`): manual setup for v1.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 11:26:34 +02:00
ff22988caf revert: Sentry #20064 + @sentry 10.27 (prod bisect) (#20221)
## Summary

Reverts **#20064** (`feat(sentry): propagate workspace context to all
spans`) and downgrades **@sentry** packages from **10.51** back to
**10.27** (reversing **#20149**), to validate in production whether
recent Sentry/instrumentation changes correlate with OTLP/metrics
issues.

## Changes

1. **Revert #20064** — removes `beforeSendSpan` from `instrument.ts`,
restores `WorkspaceAuthContextMiddleware` / `BullMQDriver` behavior, and
deletes the three `apply-workspace-sentry-*` utils added in that PR.
2. **Sentry versions** — `packages/twenty-server` (`@sentry/nestjs`,
`@sentry/node`, `@sentry/profiling-node`) and `packages/twenty-front`
(`@sentry/react`) set to `^10.27.0`; `yarn.lock` regenerated via `yarn
install`.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 11:09:48 +02:00
Paul RastoinandGitHub 74cc2b4c87 Twenty email deps (#20223) 2026-05-04 11:09:34 +02:00
Charles BochetandGitHub 7aa2afc67e fix(shared): add uuid, @types/uuid, @types/qs for Docker CD (#20222)
## Summary

- `twenty-shared` imports `uuid` (in `actor.composite-type.ts` and
`createAnyFieldRecordFilterBaseProperties.ts`) and `qs` (in
`getAppPath.ts`, `getSettingsPath.ts`), but `uuid` was not declared in
`twenty-shared/package.json` and `@types/uuid` / `@types/qs` were
missing as devDependencies.
- After scoped/hoisted deps (#20140) those types/runtime came from the
root `package.json` and are no longer guaranteed in the Docker
`common-deps` graph, so `twenty-shared:build` (pulled in before
`twenty-website-new` build) fails with `TS7016: Could not find a
declaration file for module 'uuid' / 'qs'` in the CD pipeline (see
[twenty-infra run
25309442711](https://github.com/twentyhq/twenty-infra/actions/runs/25309442711)).
- Same shape of fix as #20219 which added `@types/lodash.camelcase`.

## Test plan

- [x] `npx nx build twenty-shared` succeeds locally
- [ ] CD pipeline succeeds for `Build website-new`
2026-05-04 11:03:23 +02:00
a025dc368b fix(shared): @types/lodash.camelcase for Docker CD (#20219)
Adds `@types/lodash.camelcase` to `twenty-shared`.

**Why:** `lodash.camelcase` has no bundled types. Those types used to
come from the root `devDependencies`; after scoped/hoisted deps
(#20140), they are no longer guaranteed in the Docker `common-deps`
graph, so `twenty-shared:build` (pulled in before server Lingui) fails
with TS7016. Declaring the types on the package that imports
`lodash.camelcase` fixes CD.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:40:27 +02:00
5bf7e8b101 test(upgrade): assert sequence-runner error structurally instead of snapshot (#20213)
## Summary

The integration test `should throw when cursor command is not found in
the sequence` in `failing-sequence-runner.integration-spec.ts` used
`toThrowErrorMatchingSnapshot()`. The captured snapshot included the
literal `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` list from
`upgrade-sequence-reader.service.ts`, which grows by one entry on every
Twenty release. As a result, the snapshot drifted and broke whenever a
new instance command landed (noticed during PR #20181), creating
recurring "snapshot needs updating" churn with no real signal value.

This PR replaces the snapshot assertion with a regex match on the
structural part of the error message:

```ts
).rejects.toThrow(/Step "RemovedCommand" not found in upgrade sequence/);
```

The regex still catches the same class of regressions (the runner
failing to surface a missing-step error) without pinning the version
list. The now-empty snap file is removed (it had only this one entry).

## Test plan

- [ ] CI integration tests pass on this branch
- [ ] No remaining references to the deleted snapshot

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:52:27 +02:00
276d4f6e84 fix(code-interpreter): three correctness fixes for the TwentyMCP helper + tool output shape (#20103)
## Summary

Three independently-useful correctness fixes for the `code_interpreter`
tool, all surfaced while standing up a self-hosted code interpreter
against MCP. Each is isolated to one bug and applies regardless of
`CODE_INTERPRETER_TYPE`.

### 1. UI: unwrap `execute_tool` envelope when rendering
`code_interpreter` output

When `code_interpreter` is invoked through MCP's `execute_tool`
meta-tool, the result arrives wrapped: `{success, result: {stdout,
exitCode, files, ...}, ...}`. `ToolStepRenderer` reads `exitCode` at the
top level, which is `undefined` → the step renders as "Failed" even on a
clean `exitCode === 0`. Symmetric to the input-side unwrap that already
exists; the fix lifts `outputObj.result` when `rawToolName ===
'execute_tool'`.

### 2. Helper: route `TwentyMCP.call_tool` through `execute_tool` for
catalog tools

The `TwentyMCP` helper injected into every code-interpreter sandbox
exposes a `call_tool(name, arguments)` method. Direct MCP calls only
work for the 5 meta-tools (`get_tool_catalog`, `learn_tools`,
`execute_tool`, `load_skills`, `search_help_center`); the 250+ catalog
tools are accessed through `execute_tool`. Today
`twenty.call_tool('find_companies', {...})` raises "Unknown tool". This
commit detects catalog tools and auto-routes them through
`execute_tool`. It also flattens the nested `{catalog: {category:
[...]}}` shape returned by `list_tools()` and propagates `{success:
false}` envelopes as explicit exceptions (they were being silently
returned as dicts).

### 3. Prompt: stop the agent from hallucinating \`import twenty\`

Despite the helper being pre-injected, models frequently emitted
\`import twenty\` and crashed with \`ModuleNotFoundError\`. Two
contributing sources: the helper docstring did not explicitly say "do
not import," and the code-interpreter skill template's example block
referenced placeholder tool names. Fix: explicit "DO NOT import twenty"
block in the helper + rewritten skill examples using real tool names and
real response shapes.

## Test plan

- [ ] Existing \`code_interpreter\` tests still pass.
- [ ] Run a chat turn that invokes \`code_interpreter\` indirectly via
MCP \`execute_tool\` and confirm the UI no longer flips to "Failed" on
\`exitCode === 0\`.
- [ ] From inside the sandbox, run \`twenty.call_tool('find_companies',
{limit: 5})\` and confirm records return (was raising "Unknown tool").
- [ ] Confirm \`twenty.list_tools()\` returns a flat list, not the
nested \`{catalog: {...}}\` envelope.
- [ ] Trigger a tool error from inside the sandbox and confirm it raises
rather than returning a \`{success: false}\` dict.
- [ ] Ask an LLM to use \`code_interpreter\`; confirm it does not emit
\`import twenty\`.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-03 11:16:53 +02:00
nitinandGitHub 4f439bbe43 [AI] Prefer batch tools in system prompts (#20173) 2026-05-01 14:55:15 +02:00
4fbd8b207d chore: sync AI model catalog from models.dev (#20178)
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-01 08:50:48 +02:00
f7d812fca6 fix: disable sync on seeded message and calendar channels (#20168)
## Summary

The dev seeder creates `ConnectedAccount` records with fake OAuth tokens
(`'exampleRefreshToken'` / `'exampleAccessToken'`) and points
`MessageChannel` / `CalendarChannel` records at them with
`isSyncEnabled: true`. When the sync cron jobs run in the demo
workspace, they:

1. Pick up these seeded channels (filter is `isSyncEnabled: true` +
pending sync stage)
2. Try to refresh the fake OAuth tokens
3. Mark the channels as `FAILED_INSUFFICIENT_PERMISSIONS`
4. Surface a "Sync lost with mailbox X — please reconnect" banner in the
UI

This banner appears every time the demo workspace is loaded, even though
nothing is actually broken.

## Fix

Set `isSyncEnabled: false` on all 12 seeded channels (6 message, 6
calendar). This is the canonical "don't sync this channel" mechanism —
the same state a real user lands in when they toggle sync off in account
settings.

## Why this approach

- **ConnectedAccount records stay**: the demo workspace still shows Tim,
Jony, Phil, Jane as having connected their email/calendar — realistic
- **Pre-seeded messages and calendar events stay visible**: those don't
depend on `isSyncEnabled`
- **Crons no longer pick them up**: they filter on `isSyncEnabled:
true`, so `false` short-circuits the entire sync attempt — no failure,
no banner
- **Semantically correct**: the seeded accounts have fake tokens that
were never going to sync successfully; `isSyncEnabled: true` was
effectively a lie
- **No production code touched**: no `isDemo` flags, no magic-string
detection, no workspace-ID filters in the cron path

## Alternatives considered and rejected

- **Add an `isDemo` flag**: schema change, leaks demo knowledge into
production tables
- **Skip channels with fake tokens (`example*` pattern)**: hacky
magic-string detection in the auth refresh path
- **Filter demo workspace IDs in the cron**: production paths shouldn't
reference demo IDs
- **Don't activate demo workspaces**: breaks the demo workspace UX
entirely

## Test plan

- [ ] Reset the database and reseed (`npx nx database:reset
twenty-server`)
- [ ] Load the demo workspace — confirm no "Sync lost with mailbox"
banner appears
- [ ] Confirm seeded connected accounts still show in Settings →
Accounts
- [ ] Confirm pre-seeded messages and calendar events still appear in
the UI
- [ ] Confirm a real connected account (added via OAuth) still syncs
normally — its channel will have `isSyncEnabled: true` and the cron will
pick it up

## Related

Companion to https://github.com/twentyhq/twenty/pull/20167, which
removes the `--dev-mode` cron filter that was masking this banner issue
in the dev image.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:52:01 +02:00
c3a320c27b fix: register all cron jobs in twenty-app-dev image (#20167)
## Summary

The `twenty-app-dev` Docker image previously passed `--dev-mode` to
`cron:register:all`, which skipped all calendar, messaging, and workflow
sync cron jobs (only 4 generic crons were registered). This caused
periodic sync to silently stop after the initial import for community
members using the dev image as their actual instance.

## What changed

- Removed `--dev-mode` flag from
`packages/twenty-docker/twenty-app-dev/rootfs/etc/s6-overlay/scripts/register-crons.sh`
so the dev image registers all cron jobs (matching production behavior)
- Removed the now-unused `--dev-mode` option, `DEV_MODE_COMMANDS` set,
and conditional filtering logic from `cron-register-all.command.ts`

## Why this is safe

- **No log noise**: cron jobs gracefully no-op when no connected
accounts exist — they query for pending channels, find zero, and exit
early
- **No false banner**: the "reconnect account" banner only shows when a
user explicitly connected an account whose OAuth later fails, which is
correct behavior. No seed/demo data creates connected accounts, so a
fresh dev instance won't see any banner
- **Hiding crons just hid the symptom**: silently breaking sync with no
user feedback is worse than showing the banner if OAuth is misconfigured

## Context

Surfaced by a community member who reported that calendar sync cron jobs
never appeared in the queue after restarting the dev image, and only the
initial import worked. `--dev-mode` was added in #19138 as an
optimization for development but it doesn't match how the dev image is
actually used by community members deploying Twenty.

## Test plan

- [ ] Build/run the `twenty-app-dev` image
- [ ] Confirm worker logs show all cron jobs registering (calendar,
messaging, workflow, etc.)
- [ ] With no connected accounts: confirm no errors or log noise
- [ ] With a connected Google calendar: confirm periodic sync triggers
after ~5 minutes

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-01 07:51:21 +02:00
WeikoandGitHub 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00
8a0225e974 Dispatch root package.json hoisted deps and devDeps (#20140)
# Introduction
Dispatching root package.json devDeps, prod deps
Taking care of keeping non imported module used at build/ci level in the
root package.json

## Motivation
Avoid redundant deps declaration, better scoping allow better workspace
deps granularity installation.

<img width="385" height="247" alt="image"
src="https://github.com/user-attachments/assets/9d7162ec-ba01-4f58-8563-38333733fdf0"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-30 16:50:22 +00:00
abc67efd40 i18n - docs translations (#20166)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 18:50:57 +02:00
636deffb93 i18n - translations (#20164)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 18:01:45 +02:00
nitinandGitHub e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

### Summary
- Add AI chat thread actions: rename, archive (soft-delete via
`deletedAt`), and hard-delete with confirmation.
- Add chat thread filtering by status (active/archived/all), group-by
mode, and last activity.
- Rework drawer/side-panel thread lists to share thread sections, item
menus, archive icons, and empty-state behavior.
- Extend server chat thread model/API with `deletedAt`, mutations,
broadcasts, and archive-aware stream guards.

### Decisions
- Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a
separate action on archived threads that hard-deletes the row. Aligns
with Twenty's soft-delete convention (Felix's suggestion).
- `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read,
not stored. List query does inline aggregation for sort; `@ResolveField`
covers single-thread / mutation paths so the schema contract is honest
everywhere. Matches `timeline-messaging.service.ts` precedent and the
existing `totalInputCredits` / `totalOutputCredits` `@ResolveField`
pattern in the same resolver.
- Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a
custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats
threads as a flat collection and filters/sorts client-side, so cursor
pagination was performative.
- Sending in an archived chat unarchives it optimistically on the client
and authoritatively on the server.
- Grouping and last-activity filtering use `lastMessageAt ?? updatedAt`
so archive/rename don't bump threads in the list.
- Kept metadata-store core API unchanged; AI chat uses the same local
cast pattern already used by other metadata-store partial updates.


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00
WeikoandGitHub 4b76457217 Select application excluding logo (#20159)
## Context
This is a temporary fix for cross-version upgrade process, a better fix
would be to expose an hasInstanceCommandBeenRun() util (and later a
decorator)
2026-04-30 15:13:54 +00:00
martmullandGitHub d8bd717f1f Update doc screenshots (#20160)
fixes
https://discord.com/channels/1130383047699738754/1496579088889024542
2026-04-30 14:54:49 +00:00
b44fb1ad23 fix(security): reject ?token= URL query parameter for authentication (#20154)
## Summary

Removes the `?token=` URL query-parameter fallback from JWT
authentication. Every authenticated route (`/graphql`, `/metadata`,
REST, etc.) used to accept a full workspace JWT in the URL alongside the
`Authorization` header. The fallback was intended for the REST API
Playground only, but it was wired into the global Passport JWT extractor
and applied to every route.

URL-borne tokens leak into:
- Server access logs (nginx / Apache / CDN / proxy / load balancer)
- Log aggregators (Datadog, CloudWatch, Loki, Sumo, …)
- Browser history (and synced across devices)
- `Referer` headers when navigating to external pages
- Browser extensions with `tabs`/`webNavigation` permissions

A leaked log line was equivalent to a leaked workspace credential for
the lifetime of the token.

## What changed

- **`jwt-wrapper.service.ts`** — `extractJwtFromRequest()` is now
header-only (`ExtractJwt.fromAuthHeaderAsBearerToken()`). No URL
fallback anywhere in the system.
- **`open-api.service.ts` / `base-schema.utils.ts`** — Dropped the
`token?: string` plumbing that propagated the URL token into the schema
description. The "Authentication" section gains a "Never put your token
in a URL" warning. The "Usage with LLMs" section is rewritten to point
at the **Twenty MCP server** (header-authenticated, exposes typed tools
— the right tool for AI agents) instead of telling users to paste
tokenized OpenAPI URLs into Cursor/ChatGPT.
- **`RestPlayground.tsx`** — Now fetches the OpenAPI schema with
`Authorization: Bearer ${playgroundApiKey}` and passes the JSON document
to Scalar via `spec.content` instead of constructing a URL with
`?token=`. Aborts in-flight fetches on unmount/key change.
- **New integration test** — Asserts `?token=` is rejected on `/rest/*`,
`/graphql`, `/metadata`, and that `/rest/open-api/core?token=` returns
the unauthenticated base schema (no workspace object paths).

## Why not keep `?token=` scoped to the OpenAPI endpoint only

The first instinct was to narrow the fallback to just
`/rest/open-api/*`, since that endpoint is what the Scalar playground
component fetches. But the same log-leakage attack still applies to that
endpoint — the workspace JWT would still sit in access logs, just from
one URL pattern instead of all of them. The cleaner long-term fix is to
remove the URL pattern entirely and let the playground fetch with a
header (Scalar supports `spec.content` natively). For LLM agent use, the
MCP server is a strictly better path — typed tools, OAuth or
header-based API key auth, no tokens in URLs anywhere.

## Not affected

File downloads at `file-url.service.ts` also use `?token=` URLs but with
separate, short-lived `FILE`-typed tokens validated by
`file-by-id.guard.ts` directly (not via `extractJwtFromRequest`). That
mechanism is scoped per-file with limited TTL and is acceptable.

## Action required for users

Anyone who previously pasted `?token=` URLs into LLM tools, scripts,
bookmarks, or shared configs should rotate their workspace API keys.
Those tokens are likely captured in server logs / chat histories
somewhere.

## Test plan

- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx nx typecheck twenty-front` — clean
- [x] `npx nx lint:diff-with-main twenty-server` — clean
- [x] `npx nx lint:diff-with-main twenty-front` — clean
- [x] OpenAPI utils unit tests + snapshots — 11/11 pass
- [ ] Run the new integration test against a live server: `nx run
twenty-server:test:integration:with-db-reset` and verify
`url-token-auth-rejection.integration-spec.ts` passes
- [ ] Manually open Settings → Playground → REST, confirm the schema
loads (now via Bearer header instead of `?token=` URL)
- [ ] Manually verify `POST /metadata?token=<jwt>` (no Authorization
header) returns Forbidden, and the same request with the token in the
header returns the user

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 17:00:26 +02:00
f32b03a3ec i18n - docs translations (#20161)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 16:58:55 +02:00
martmullandGitHub c8e405cb4e Add twenty sdk server upgrade command (#20158)
##
The command pulls the image, compares it against the one the container
was created from, and only recreates the container if the image actually
changed. Your data volumes are preserved — only the container is
replaced.
2026-04-30 13:03:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
83db37d33f chore(deps): bump @sentry/profiling-node from 10.27.0 to 10.51.0 (#20149)
Bumps
[@sentry/profiling-node](https://github.com/getsentry/sentry-javascript)
from 10.27.0 to 10.51.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/releases"><code>@​sentry/profiling-node</code>'s
releases</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md"><code>@​sentry/profiling-node</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>10.51.0</h2>
<h3>Important Changes</h3>
<ul>
<li>
<p><strong>feat(cloudflare): Add trace propagation for RPC method calls
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20343">#20343</a>)</strong></p>
<p>Trace context is now propagated across Cloudflare Workers RPC calls,
connecting traces between Workers and Durable Objects.
This feature is opt-in and requires setting
<code>enableRpcTracePropagation: true</code> in your SDK
configuration:</p>
<pre lang="ts"><code>// Worker
export default Sentry.withSentry(
  env =&gt; ({
    dsn: env.SENTRY_DSN,
    enableRpcTracePropagation: true,
  }),
  handler,
);
<p>// Durable Object<br />
export const MyDurableObject =
Sentry.instrumentDurableObjectWithSentry(<br />
env =&gt; ({<br />
dsn: env.SENTRY_DSN,<br />
enableRpcTracePropagation: true,<br />
}),<br />
MyDurableObjectBase,<br />
);<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(hono)!: Change setup for <code>@sentry/hono/node</code>
(<code>init</code> in external file) (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/20497">#20497</a>)</strong></p>
<p>To improve Node.js instrumentation, the <code>sentry()</code>
middleware exported from <code>@sentry/hono/node</code> no longer
accepts configuration options.
Instead, you must configure the SDK by calling
<code>Sentry.init()</code> in a dedicated instrumentation file that runs
before your application code (read more in the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/hono/README.md">Hono
SDK readme</a>:</p>
<pre lang="ts"><code>// instrument.mjs (or instrument.ts)
import * as Sentry from '@sentry/hono/node';
<p>Sentry.init({<br />
dsn: '<strong>DSN</strong>',<br />
tracesSampleRate: 1.0,<br />
});<br />
</code></pre></p>
</li>
<li>
<p><strong>feat(nitro): Add <code>@sentry/nitro</code> SDK (<a
href="https://redirect.github.com/getsentry/sentry-javascript/pull/19224">#19224</a>)</strong></p>
<p>A new <code>@sentry/nitro</code> package provides first-class Sentry
support for <a href="https://nitro.build/">Nitro</a> applications, with
HTTP handler and error instrumentation, middleware tracing, request
isolation, and build-time source map uploading via
<code>withSentryConfig</code>.
Read more in the <a
href="https://docs.sentry.io/platforms/javascript/guides/nitro/">Nitro
SDK docs</a> and the <a
href="https://github.com/getsentry/sentry-javascript/blob/develop/packages/nitro/README.md">Nitro
SDK readme</a>.</p>
</li>
</ul>
<h3>Other Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/dc0b839ff4896cf90a02f5c1a6de54a31302dcf3"><code>dc0b839</code></a>
release: 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/b3cabee9a9348b9e67332262d44d3d1900424199"><code>b3cabee</code></a>
Merge pull request <a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20599">#20599</a>
from getsentry/prepare-release/10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/3be99a9afa77e49578e6839e4b32f97fb04fb0f8"><code>3be99a9</code></a>
meta(changelog): Update changelog for 10.51.0</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/bea1aad42277db894d5a299bfec3cdd633d6baf0"><code>bea1aad</code></a>
test(browser): Unflake some more tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20591">#20591</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/50aa0859b3a188d34d0317dab3ad57f2140f02fe"><code>50aa085</code></a>
test(node): Unflake postgres tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20593">#20593</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/1166839112c4766f210124dc0486ebbfd6db104b"><code>1166839</code></a>
fix(hono): Distinguish <code>.use()</code> middleware in sub-apps from
<code>.all()</code> handlers...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/217ad4a69554281806eccbfeac1b27c4f43f6ffa"><code>217ad4a</code></a>
test(node): Fix flaky ANR test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20592">#20592</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/91ffb3fac90835ab160f8152527a54a5d64f3250"><code>91ffb3f</code></a>
test(node): Fix flaky worker thread integration test (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20588">#20588</a>)</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c4e3902c9297147158e730f017aba96e83ef619e"><code>c4e3902</code></a>
chore(ci): Do not report flaky test issues if we cannot find a test name
(<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20">#20</a>...</li>
<li><a
href="https://github.com/getsentry/sentry-javascript/commit/c0005cd387f3a7ea6fbb2e85041562c7f32e0484"><code>c0005cd</code></a>
test(node): Update timeout for cron integration tests (<a
href="https://redirect.github.com/getsentry/sentry-javascript/issues/20586">#20586</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/getsentry/sentry-javascript/compare/10.27.0...10.51.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/profiling-node&package-manager=npm_and_yarn&previous-version=10.27.0&new-version=10.51.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>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-30 12:02:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Charles Bochet
5bdbfe651e chore(deps): bump postal-mime from 2.6.1 to 2.7.4 (#20150)
Bumps [postal-mime](https://github.com/postalsys/postal-mime) from 2.6.1
to 2.7.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/releases">postal-mime's
releases</a>.</em></p>
<blockquote>
<h2>v2.7.4</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2>v2.7.3</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2>v2.7.2</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2>v2.7.1</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2>v2.7.0</h2>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postalsys/postal-mime/blob/master/CHANGELOG.md">postal-mime's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.3...v2.7.4">2.7.4</a>
(2026-03-17)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add missing originalKey to Header type and Uint8Array to Attachment
content (<a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0">92cc91c</a>)</li>
<li>include originalKey in parsed headers output (<a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347">83521c8</a>)</li>
<li>preserve __esModule and .default in CJS build for bundler interop
(<a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048">1466910</a>)</li>
<li>prevent RFC 2047 encoded-word address fabrication (<a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3">844f920</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.2...v2.7.3">2.7.3</a>
(2026-01-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>correct TypeScript type definitions to match implementation (<a
href="https://github.com/postalsys/postal-mime/commit/b225d7cca422cb9bc3ab5301e94c4c0bef9a69e2">b225d7c</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.2">2.7.2</a>
(2026-01-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add null checks for contentType.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/ad8f4c62e0972fd0244859ee5a5184b2cac26395">ad8f4c6</a>)</li>
<li>improve RFC compliance for MIME parsing (<a
href="https://github.com/postalsys/postal-mime/commit/e004c3acb29d72ed7eaf1b0b66351cf8b82b970d">e004c3a</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.7.0...v2.7.1">2.7.1</a>
(2025-12-22)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Add null checks for contentDisposition.parsed access (<a
href="https://github.com/postalsys/postal-mime/commit/fd54c37093cc64737c6bb17986bc9d052d2d5add">fd54c37</a>)</li>
</ul>
<h2><a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.0">2.7.0</a>
(2025-12-22)</h2>
<h3>Features</h3>
<ul>
<li>add headerLines property exposing raw header lines (<a
href="https://github.com/postalsys/postal-mime/commit/c79a02ab05d9cac44e05e95a433752ff292aa5eb">c79a02a</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postalsys/postal-mime/commit/178f1ef0b1cd0047e1b8e690beabfec541b4daa7"><code>178f1ef</code></a>
chore(master): release 2.7.4 (<a
href="https://redirect.github.com/postalsys/postal-mime/issues/88">#88</a>)</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1f7ba618d42d34b779157dfa33794cbae383a24d"><code>1f7ba61</code></a>
chore: bump devDependencies</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/83521c87f62e5e095ae09913c70798f20e2ab347"><code>83521c8</code></a>
fix: include originalKey in parsed headers output</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/b0d7b11550a2a3c65a52a2adf4f8281058023cab"><code>b0d7b11</code></a>
test: improve test coverage across codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/ebc5ce619649d13ad72f4d12414f3e337a9e248c"><code>ebc5ce6</code></a>
refactor: simplify and clean up codebase</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/1466910e31608b9e5307724ecc6a0a3a70556048"><code>1466910</code></a>
fix: preserve __esModule and .default in CJS build for bundler
interop</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/844f92023d49d819ef13b9ad5c50b7c346eb02d3"><code>844f920</code></a>
fix: prevent RFC 2047 encoded-word address fabrication</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/24dc6c64dfb43d89a8c8837ec941c96ebfa2c1fa"><code>24dc6c6</code></a>
test: update type check test with originalKey property</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/92cc91c1c8477e0462cb0e93ddf8ea6aec6534d0"><code>92cc91c</code></a>
fix: add missing originalKey to Header type and Uint8Array to Attachment
content</li>
<li><a
href="https://github.com/postalsys/postal-mime/commit/aa5baeafa6ffd093ab447c22d20e5da25051faff"><code>aa5baea</code></a>
docs: add link to full documentation site</li>
<li>Additional commits viewable in <a
href="https://github.com/postalsys/postal-mime/compare/v2.6.1...v2.7.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postal-mime&package-manager=npm_and_yarn&previous-version=2.6.1&new-version=2.7.4)](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: Charles Bochet <charles@twenty.com>
2026-04-30 11:38:44 +00:00
3fbb70c13f i18n - docs translations (#20157)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 12:49:31 +02:00
c6b6c824d4 i18n - translations (#20156)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 11:55:45 +02:00
b21bf66b38 i18n - translations (#20155)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-30 11:50:00 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
22838ac6de chore(deps-dev): bump @babel/preset-typescript from 7.24.7 to 7.28.5 (#20151)
Bumps
[@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript)
from 7.24.7 to 7.28.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/preset-typescript</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.5 (2025-10-23)</h2>
<p>Thank you <a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>, <a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a>, and
<a href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>
for your first PRs!</p>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17446">#17446</a>
Allow <code>Runtime Errors for Function Call Assignment Targets</code>
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-validator-identifier</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17501">#17501</a>
fix: update identifier to unicode 17 (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17534">#17534</a>
Allow mixing private destructuring and rest (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
</ul>
</li>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17521">#17521</a>
Improve <code>@babel/parser</code> error typing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17491">#17491</a>
fix: improve ts-only declaration parsing (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-proposal-discard-binding</code>,
<code>babel-plugin-transform-destructuring</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17519">#17519</a>
fix: <code>rest</code> correctly returns plain array (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-helper-member-expression-to-functions</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-optional-chaining</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17503">#17503</a> Fix
<code>JSXIdentifier</code> handling in
<code>isReferencedIdentifier</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17504">#17504</a>
fix: ensure scope.push register in anonymous fn (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17494">#17494</a>
Type checking babel-types scripts (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>🏃‍♀️ Performance</h4>
<ul>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17490">#17490</a>
Faster finding of locations in <code>buildCodeFrameError</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 8</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Byeongho Yoo (<a
href="https://github.com/youthfulhps"><code>@​youthfulhps</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Hyeon Dokko (<a
href="https://github.com/CO0Ki3"><code>@​CO0Ki3</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li><a
href="https://github.com/Olexandr88"><code>@​Olexandr88</code></a></li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
<li>fisker Cheung (<a
href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/preset-typescript</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<blockquote>
<p><strong>Tags:</strong></p>
<ul>
<li>💥 [Breaking Change]</li>
<li>👓 [Spec Compliance]</li>
<li>🚀 [New Feature]</li>
<li>🐛 [Bug Fix]</li>
<li>📝 [Documentation]</li>
<li>🏠 [Internal]</li>
<li>💅 [Polish]</li>
</ul>
</blockquote>
<p><em>Note: Gaps between patch versions are faulty, broken or test
releases.</em></p>
<p>This file contains the changelog starting from v8.0.0-alpha.0.</p>
<ul>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.15.0-v7.28.5.md">CHANGELOG
- v7.15.0 to v7.28.5</a> for v7.15.0 to v7.28.5 changes (the last common
release between the v8 and v7 release lines was v7.28.5).</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7.0.0-v7.14.9.md">CHANGELOG
- v7.0.0 to v7.14.9</a> for v7.0.0 to v7.14.9 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v7-prereleases.md">CHANGELOG
- v7 prereleases</a> for v7.0.0-alpha.1 to v7.0.0-rc.4 changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v4.md">CHANGELOG
- v4</a>, <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v5.md">CHANGELOG
- v5</a>, and <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-v6.md">CHANGELOG
- v6</a> for v4.x-v6.x changes.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/.github/CHANGELOG-6to5.md">CHANGELOG
- 6to5</a> for the pre-4.0.0 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel/blob/main/packages/babel-parser/CHANGELOG.md">Babylon's
CHANGELOG</a> for the Babylon pre-7.0.0-beta.29 version changelog.</li>
<li>See <a
href="https://github.com/babel/babel-eslint/releases"><code>babel-eslint</code>'s
releases</a> for the changelog before <code>@babel/eslint-parser</code>
7.8.0.</li>
<li>See <a
href="https://github.com/babel/eslint-plugin-babel/releases"><code>eslint-plugin-babel</code>'s
releases</a> for the changelog before <code>@babel/eslint-plugin</code>
7.8.0.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2>v8.0.0-rc.4 (2026-04-29)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17954">#17954</a>
fix(parser): ts parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17923">#17923</a>
Support flow extends bound (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17888">#17888</a> TS
parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17865">#17865</a>
Fix(parser): flow parser small fixes (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-generator</code>, <code>babel-parser</code>,
<code>babel-plugin-transform-spread</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17871">#17871</a>
Disallow super call after new (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💥 Breaking Change</h4>
<ul>
<li><code>babel-cli</code>,
<code>babel-helper-transform-fixture-test-runner</code>,
<code>babel-helpers</code>, <code>babel-node</code>,
<code>babel-register</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17938">#17938</a>
Bundle more packages (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17937">#17937</a>
Remove <code>Scope#buildUndefinedNode</code> (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-helper-wrap-function</code>,
<code>babel-plugin-transform-block-scoping</code>,
<code>babel-plugin-transform-regenerator</code>,
<code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17907">#17907</a>
Remove <code>NodePath#toComputedKey</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-external-helpers</code>,
<code>babel-template</code>, <code>babel-traverse</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17830">#17830</a>
Replace remaining whitelist/blacklist with inclusive alternatives (<a
href="https://github.com/stuckvgn"><code>@​stuckvgn</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-property-mutators</code>,
<code>babel-standalone</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17882">#17882</a>
Remove <code>@babel/plugin-transform-property-mutators</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/61647ae2397c82c3c71f077b5ab109106a5cac0f"><code>61647ae</code></a>
v7.28.5</li>
<li><a
href="https://github.com/babel/babel/commit/42cb285b59fc99a8102d69bef6223b75617e9f46"><code>42cb285</code></a>
Improve <code>@babel/core</code> types (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17404">#17404</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/eebd3a06021c13d335b5b0bd79734df3abbea678"><code>eebd3a0</code></a>
v7.27.1</li>
<li><a
href="https://github.com/babel/babel/commit/fdc0fb59e119ee0b38bced63867a344a5b4bc2f3"><code>fdc0fb5</code></a>
[Babel 8] Bump nodejs requirements to <code>^20.19.0 || &gt;=
22.12.0</code> (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17204">#17204</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/5c350eab83dd12268add44cce0eeda6c898211e3"><code>5c350ea</code></a>
v7.27.0</li>
<li><a
href="https://github.com/babel/babel/commit/ca4865a7f43a6a56aec242e23e4a3e318cf0ca92"><code>ca4865a</code></a>
Fix: align behaviour to tsc <code>rewriteRelativeImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17118">#17118</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cd24cc07ef6558b7f6510f9177f6393c91b0549f"><code>cd24cc0</code></a>
chore: Update TS 5.7 (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/17053">#17053</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/63d30381c169780460e01bdb6669c5e01af1dfbe"><code>63d3038</code></a>
v7.26.0</li>
<li><a
href="https://github.com/babel/babel/commit/bfa56c49569f0bfd5579e0e1870ffa92f74fad48"><code>bfa56c4</code></a>
Support <code>import()</code> in <code>rewriteImportExtensions</code>
(<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript/issues/16794">#16794</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/b07957ebb316a1e2fc67454fc7423508bb942e63"><code>b07957e</code></a>
v7.25.9</li>
<li>Additional commits viewable in <a
href="https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-typescript">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 <code>@​babel/preset-typescript</code> since your current
version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/preset-typescript&package-manager=npm_and_yarn&previous-version=7.24.7&new-version=7.28.5)](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-04-30 09:32:13 +00:00
martmullandGitHub bddd23fd9c Fix application icons (#20142)
fixes application chip (icon Name) in all setting tables

## After
<img width="1200" height="896" alt="image"
src="https://github.com/user-attachments/assets/bd377f47-1d52-4142-b904-f2ce90c1db78"
/>
<img width="1200" height="917" alt="image"
src="https://github.com/user-attachments/assets/f49cc742-f11e-47e3-86ed-34beffe493c7"
/>
<img width="1234" height="878" alt="image"
src="https://github.com/user-attachments/assets/2ab459de-5f9d-4d39-9490-eec4ed9ee432"
/>
<img width="1239" height="845" alt="image"
src="https://github.com/user-attachments/assets/3c1bf258-285a-47b9-a60d-05ba1564334d"
/>
<img width="1183" height="907" alt="image"
src="https://github.com/user-attachments/assets/715b2470-2d88-48e3-88ac-d3daf3451717"
/>
<img width="1300" height="912" alt="image"
src="https://github.com/user-attachments/assets/d7c829fa-bf1d-4f19-82de-a8bf29e22bfa"
/>
2026-04-30 09:32:04 +00:00
842e679cc6 fix(billing): gate AI credit-cap at entry points instead of workflow executor (#20096)
## Background

The 2026-04-26 incident saw 716M Sonnet 4.6 tokens consumed in a single
trial workspace. Two causes: failed agent executions weren't billed
(addressed by #20065) and the credit-cap gate had been removed from
`WorkflowExecutorWorkspaceService.executeStep` in #19904, leaving no
enforcement point at all.

## Why not just revert #19904

#19904 was right that gating at the workflow executor is too coarse.
When one user exhausted a workspace's credits via chat, *all* workflows
hard-failed mid-run — including cheap DB/CRUD/branch automations costing
essentially nothing. Reverting would re-introduce that cliff.

## New design: gate at the AI entry points

The chat resolver already gates this way
(`agent-chat.resolver.ts:137-148`). This PR replicates the same pattern
at every other point where the workspace can incur real AI cost:

- `executeAgent` in `agent-async-executor.service.ts`
- the REST handler in `ai-generate-text.controller.ts`
- `generateThreadTitle` in `agent-title-generation.service.ts`

In each, after auth/validation: skip if `IS_BILLING_ENABLED` is false;
otherwise call `BillingService.canBillMeteredProduct(workspaceId,
BillingProductKey.WORKFLOW_NODE_EXECUTION)`; on `false`, throw
`BillingException(BILLING_CREDITS_EXHAUSTED)`. No new method, no new
exception code, no new product key.

This matches industry convention (Lovable/Replit also gate at the
expensive-operation boundary, not at every cheap step).

## Deliberately not gated

- `WorkflowExecutorWorkspaceService.executeStep` — the design choice is
now intentional, so the #19904 TODO is replaced by a one-line
absolute-behavior comment explaining why the gate isn't here. Cheap
workflow steps (DB CRUD, branching, action steps) are not gated, so a
chat-driven cap exhaustion does not block non-AI automations.
- `repair-tool-call.util` — repair is a sub-call inside an already-gated
AI flow. If the parent is gated, repair will naturally not run. Adding a
gate here adds complexity without value.

## Net effect

A workspace that exhausts credits via chat or AI agent stops making AI
calls. Its non-AI workflows continue running normally. A workflow with
both AI and non-AI steps fails at the AI step with
`BILLING_CREDITS_EXHAUSTED`, but downstream non-AI steps that don't
depend on the AI output still run.

## Conflicts

This PR overlaps with three other in-flight PRs in the same files. None
of them touch the gate logic; rebasing on top of any of them is trivial:

- #20065 (agent-async-executor): adds `workspaceId` to `executeAgent`
args and bills in `finally`. The gate at the top of `executeAgent` from
this PR sits naturally above that.
- #20066 (REST controller): adds usage billing to the controller.
- #20067 (title gen): adds usage billing to title generation and
tool-call repair.

Recommend landing #20065/#20066/#20067 first; this PR rebases trivially
on top.

## Tests

Out of scope per the PR series convention. The existing chat-resolver
gate isn't unit-tested either; this PR follows the same precedent.
Follow-up: add integration coverage that exercises a workspace at
`hasReachedCurrentPeriodCap=true` against each of the three new gates
plus the pre-existing chat-resolver gate.

## Future follow-ups

- Per-user soft cap inside a workspace (the Lovable Business-tier
pattern), so one user can't exhaust the workspace's cap.
- Pre-flight cost estimate so the user sees an "approaching cap" warning
before the hard stop.
- Rename `BillingProductKey.WORKFLOW_NODE_EXECUTION` — the name predates
this design choice and is misleading now that it gates AI entry points
rather than workflow nodes.

## Test plan

- [ ] Trigger a workspace into `hasReachedCurrentPeriodCap=true`.
- [ ] Send a chat message — expect failure with
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow whose only AI step is an `ai-agent` action — expect
that step to fail with `BILLING_CREDITS_EXHAUSTED`, downstream non-AI
steps still run.
- [ ] POST to `/rest/ai/generate-text` — expect
`BILLING_CREDITS_EXHAUSTED`.
- [ ] Create a new chat thread (which kicks off `generateThreadTitle`) —
expect `BILLING_CREDITS_EXHAUSTED`.
- [ ] Run a workflow with no AI step (only DB CRUD/branching/actions) —
expect it to run unaffected.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:48:06 +02:00
neo773andGitHub a713f8d87f CalDAV: support Digest auth (#20135)
Adds digest auth support for CalDAV, mostly used by legacy servers

/closes https://github.com/twentyhq/twenty/issues/19922
2026-04-29 16:09:21 +00:00
EtienneandGitHub fd6d5f895d Ai Chat - Caching optim (#20126)
EDIT : 
- solving auto-caching from Anthropic by updating ai-sdk/anthropic +
adding providerOption at stream level
- concerning Bedrock, it needs breakpoint


**1. Breakpoints were only on the system prompt**

The code already placed a cache marker on the system prompt (~10K
tokens). But the conversation history — which can grow to hundreds of
thousands of tokens — had no marker, so Anthropic re-read it at full
price on every turn.

The fix adds a prepareStep hook inside streamText that stamps the last
message with a cache breakpoint before every LLM call. Anthropic then
caches the entire conversation prefix, and subsequent turns read it at
$0.30/M instead of $3/M.

prepareStep is used rather than a one-shot pre-processing step because
an agentic turn makes multiple internal LLM calls as tool results
accumulate — the hook refreshes the breakpoint before each one.

**2. Bedrock was using the wrong field**

The system prompt marker for Bedrock was set as cacheControl: { type:
'ephemeral' } — which is the Anthropic wire format. The Bedrock Converse
API expects cachePoint: { type: 'default' }. The system prompt was
silently not being cached on Bedrock at all.

Both the system prompt and the new prepareStep now go through a shared
getCacheProviderOptions helper that returns the correct field per
provider.

**3. Persisted cached token usage to monitor cache strat. efficiency**
2026-04-29 14:42:12 +00:00
11628d19a3 add recurring calendar events for google cal (#19748)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-29 14:06:57 +00:00
46ba5fd16c i18n - translations (#20138)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-29 15:51:28 +02:00
nitinandGitHub 4649736d49 [Command Menu] Fix record-selection command filtering in edit mode (#20034)
https://github.com/user-attachments/assets/fe1461c7-0d5c-4c6f-8c2e-2cf569e7de90

## What

Fix `RECORD_SELECTION` items leaking into the command menu when nothing
is selected, and unify how the menu renders in normal vs edit mode.

## The bug

`RECORD_SELECTION`-availability items were showing up even when
`numberOfSelectedRecords === 0`. New util
`doesCommandMenuItemMatchSelectionState` gates them, applied
consistently in the runtime provider and the editor.

## The refactor

`PinnedCommandMenuItemButtonsEditMode` was a 140-line near-duplicate of
`PinnedCommandMenuItemButtons` with its own (drifting) filter logic.
Killed it. Edit mode now flows through the same
`CommandMenuContextProvider` with a new `isInPreviewMode` flag — one
filter chain, one rendering path.

## Behavior in edit mode

**Header (pinned buttons in page header):**
- Runs the full filter chain — object metadata, page type, selection
state, page layout, *and the conditional availability expression*
- Buttons render at full styling but are inert via `pointer-events:
none` + `cursor: not-allowed`
- Preview now reflects exactly what users will see on the live page (not
a grayed-out approximation)

**Side panel editor:**
- New `useEditableCommandMenuItems` hook
- Same filters as runtime *minus* the conditional availability
expression and `FALLBACK` items — so it surfaces everything that's
actually configurable for this page context
- Still gates on selection state — if no records selected,
`RECORD_SELECTION` items are hidden from the editor too. Open to
feedback if we'd rather always show them so users can pin them ahead of
time.

## Misc

- `usePinnedCommandMenuItemsInlineLayout` — visible count now waits
until every item is measured before committing. Fixes a flash of wrong
counts on mount/resize
- Renamed `useCommandMenuContextApi` → `useCurrentCommandMenuContextApi`
— name now conveys it reads from the *current* scoped context store
- Copy: "Records selected" → "Record(s) selected"
2026-04-29 13:34:28 +00:00
Abdullah.andGitHub 51384bc085 refactor: optimize website visual runtime (#20120)
Refactors the website visual runtime to make WebGL-heavy sections more
reliable and less expensive.

This adds shared image/model loading caches, safer WebGL context
recovery, staggered visual mounting, and static rendering for decorative
Helped card visuals. It also removes a large bespoke Helped renderer in
favor of the shared halftone model canvas, reduces scroll/layout work in
the Helped section, and cleans up duplicated model-loading code across
several visuals.
2026-04-29 12:44:47 +00:00
neo773andGitHub abfa6200dd ssrf hardening (#19963)
Hardened CalDav with new approach of wrapping axios ssrf http agent to
fetch via `@lifeomic/axios-fetch` because `tsdav` only accept `fetch`
override.

Also Hardened test endpoint
2026-04-29 12:13:19 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>nitin
480e5796ec fix(ai): render record links inside markdown headings in AI chat (#20074)
## Summary

Adds `h1`–`h6` component overrides to `LazyMarkdownRenderer` so that
`[[record:...]]` references placed inside markdown headings in the AI
chat are parsed by `processChildrenForRecordLinks` and rendered as
clickable `RecordLink` chips, matching the behavior already in place for
`p`, `li`, `td`, `th`, and `a`.

Fixes #20072

## Test plan

- [ ] In AI chat, ask a question whose answer places a record reference
inside a markdown heading (e.g. `## Found [[person:uuid:John Doe]]`) and
confirm a clickable `RecordLink` chip renders instead of the raw
`[[...]]` text.
- [ ] Verify heading styling (sizes, weights, margins) is unchanged.

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: nitin <ehconitin@users.noreply.github.com>
2026-04-29 13:50:32 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
d8e2de48e6 Fix stale UI state after stop-impersonation (#20088)
## Summary

A customer reported that after **Stop Impersonating**, the sidebar still
showed the impersonated user's pinned favorites, the AI chat tab toggle,
and the AI chat history — even though the original admin's session was
correctly restored.

## Root cause

The refactor in #19597 replaced the previous `signOut()`-based stop flow
with an in-place token swap, but only cleared Apollo cache + reloaded
the user. Several user-scoped client stores were left untouched:

- **`metadataStoreState`** is localStorage-backed
(`navigationMenuItems`,
  `agentChatThreads`, `views`, `pageLayouts`, etc.) and only refreshed
  by `MinimalMetadataLoadEffect`. That effect is gated by
  `metadataLoadedVersion` + `desiredLoadState`, neither of which flips
  on a same-workspace token swap, so the effect never re-runs.
- **In-memory AI atoms** (`currentAiChatThreadState`,
`agentChatInputState`,
  `hasInitializedAgentChatThreadsState`) keep pointing at the
  impersonated user's selected thread / input.
- **Session localStorage keys** (`agentChatDraftsByThreadIdState`,
`lastVisitedObjectMetadataItemIdState`,
`lastVisitedViewPerObjectMetadataItemState`,
  `playgroundApiKeyState`) carry the impersonated user's drafts and
  navigation state.

`clearSession()` (used by logout) avoids this because it calls
`applyMockedMetadata()` and flips `desiredLoadState` mocked↔real, which
chain-triggers a full metadata reload on next sign-in.

## Fix

Extract a `resetUserScopedClientState` helper inside
`useImpersonationSession` that:

1. Calls `clearSessionLocalStorageKeys()` to drop user-scoped
localStorage
   keys.
2. Resets the in-memory AI session atoms.
3. Marks `metadataStoreState['agentChatThreads']` as `'empty'`.
`useLoadStaleMetadataEntities` does **not** handle this entity key, so
   without an explicit reset to `'empty'` the
   `AgentChatThreadInitializationEffect` (which only fires on `'empty'`)
   would never refetch.
4. Calls `invalidateMetadataStore()` to clear all
`currentCollectionHash`
   values and bump `metadataLoadedVersion`, forcing
   `MinimalMetadataLoadEffect` to re-run and refetch
   `navigationMenuItems`, `views`, `pageLayouts`, etc. against the new
   token.

The helper is applied to both `startImpersonating` and
`stopImpersonating`
— start had the same latent bug; the impersonated user could see the
admin's favorites until the cache happened to refresh.

## Test plan

- [ ] As an admin user, pin some favorites in the sidebar
- [ ] Impersonate a user with different favorites → favorites should
      switch to the impersonated user's
- [ ] Click "Stop Impersonating" → sidebar should immediately show the
      admin's favorites (not the impersonated user's)
- [ ] As an admin **without** AI permission, impersonate a user **with**
AI permission, open AI chat, send a message, then stop impersonating
      → AI chat history should be empty / inaccessible (the AI tab
      visibility itself is fixed in a separate PR)
- [ ] Type a draft in AI chat as the impersonated user → after stop, the
      draft should be gone
- [ ] Verify regular sign-out still works while impersonating
- [ ] Verify the impersonation banner still shows / hides correctly

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-29 13:37:12 +02:00
Paul RastoinandGitHub b49e58dfc6 Fix click house migration (#20127)
`it does not support renaming of multiple tables in single query.`

@etiennejouan manually fix the corrupted clickhouse instance
2026-04-29 10:48:21 +00:00
a6118b7dc3 i18n - translations (#20125)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-29 11:57:14 +02:00
Abdul RahmanandGitHub 19ee9444ed add UpsertViewWidget resolver (#20053) 2026-04-29 09:41:02 +00:00
Paul RastoinandGitHub b92617d46e Copy twenty-shared in twenty-website deploy (#20124) 2026-04-29 08:44:54 +00:00
c476c6c80b chore: sync AI model catalog from models.dev (#20122)
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-04-29 08:45:09 +02:00
Paul RastoinandGitHub d2dda67596 Fix upgrade --start-from-workspace-id (#20116)
# Introduction
Prevent using both `--start-from-workspace-id` and `--workspace`

When any of the two are being passed we prevent passing to the next
instance segment, it would require an upgrade re run even if legit

When `--start-from-workspace-id` is passed we filter from all the
fetched active or suspended workspace ids and apply equivalent filter as
before
2026-04-28 16:46:15 +00:00
Abdullah.andGitHub 6aec449a56 refactor: harden website runtime, routing, and hero visual (#20113)
This centralizes routing/SEO ownership, replaces deprecated middleware
with proxy, adds safer lifecycle/runtime primitives, and introduces
visual error boundaries so broken WebGL/canvas-heavy visuals can fail
gracefully instead of taking down the page. It also hardens animation,
resize, visibility, cleanup, and WebGL fallback paths for a broader
range of browsers and devices.

The hero visual was split from large monolithic files into focused
domain folders for shell, pages, shared primitives, window interactions,
terminal conversation, prompt, editor, and traffic-light behavior.
Legacy unused section code was removed, visual configs were extracted,
state/geometry logic was moved into testable modules, and coverage was
added across routing, SEO, lifecycle, animation, visual runtime,
halftone behavior, and hero interactions.

More changes on the way, but this should make the website a lot more
stable - disabling WebGL on Firefox and loading the website does not
cause crashes on local any longer, will test on dev once this is merged.
2026-04-28 17:15:28 +02:00
7ea1dfdd49 i18n - translations (#20115)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-28 16:48:25 +02:00
3290bf3ab1 fix(rest-api): prevent silent pagination failures and include valid options in enum validation errors (#20092)
# Summary 
(fixes #20044)

This PR implements two fixes for the REST API to enforce stricter
validation and provide better error messages.

Issue 1: Cursor parameter silently ignored
Problem: When users provided common cursor aliases (e.g., cursor, after,
before) instead of the correct parameter names (starting_after,
ending_before), the API silently ignored them and returned page 1 on
every request.
Solution: Added strict validation to detect common cursor aliases and
throw a clear error directing users to use the correct parameter names.
Files modified:
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/rest-input-request-parser.exception.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/starting-after-parser-utils/parse-starting-after-rest-request.util.ts
-
packages/twenty-server/src/engine/api/rest/input-request-parsers/ending-before-parser-utils/parse-ending-before-rest-request.util.ts
- Test files for both parsers
Example error:
Invalid cursor parameter 'cursor'. Use 'starting_after' for pagination.
---
Issue 2: OpportunityStageEnum not validated on REST
Problem: When creating or updating opportunities via REST with an
invalid stage value, the API either silently dropped the value or
returned a generic error without listing valid options.
Solution: Updated the SELECT field validation to include valid options
in the error message.
Files modified:
-
packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-rating-and-select-field-or-throw.util.ts
- Test file
Example error:
Invalid value "BAD_VALUE" for field "stage". Valid values are: NEW,
SCREENING, MEETING, PROPOSAL, CUSTOMER
---
### Testing
- Added 5 new test cases for `parse-starting-after-rest-request.util.ts`
- Added 6 new test cases for `parse-ending-before-rest-request.util.ts`
- Added 1 new test case for
`validate-rating-and-select-field-or-throw.util.ts`
- All 21 tests passing
---
Breaking Changes
None - correct usage is unaffected.

---------

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
2026-04-28 14:31:18 +00:00
EtienneandGitHub fbaea0639a Billing - optimize usageEvent CH table (#20019)
- Update usageEvent clickhouse table, partitioning, indexing and
projection (auto materialized view) to optimize credit usage queries
- Add caching for available credits and billing subscription


To do in next PR: deprecate enforceCapUsage cron. Bonus : real-time on
billingSubscription
2026-04-28 14:06:49 +00:00
2a3b8adcfb i18n - translations (#20114)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-28 16:17:27 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <claude[bot]@users.noreply.github.com>
8f362186ce Redesign application content tab + logic function settings; add Layout detail pages (#20056)
## Summary

Iterative redesign of two related areas in settings, plus a new
`pages/settings/layout/` folder for read-only entity detail pages.

### Application content tab

- **Grouped into three sections** — Data / Layout / Logic — each with
one H2 + multiple `TableSection`-wrapped sub-tables (mirrors the
role-permissions pattern). Replaces six per-category table/row
components with one uniform `<SettingsApplicationContentSubtable>` +
`ApplicationContentRow` shape (net **−~700 lines** across the refactor).
- **All 10 row categories now clickable** for installed apps:
- Objects / Fields / Logic functions / Front components → existing
detail pages
  - Agents → existing `AiAgentDetail`
- Skills → existing `AiSkillDetail` (looked up by `Skill.applicationId +
name`)
- Roles → existing `RoleDetail` (looked up by
`Role.universalIdentifier`)
- Views / Page layouts / Navigation menu items → **new** detail pages
(see below)
- **Lifecycle hooks visible** — `pre-install` / `post-install` logic
functions are surfaced in the Trigger column instead of appearing as
empty/misconfigured.

### Logic function settings (Triggers + Test tabs)

- Triggers tab is now editable (HTTP / Cron / Database event / AI tool)
with a `<SettingsLogicFunctionTriggerSection>` wrapper that owns the
toggle, header, and read-only short-circuit.
- HTTP section gets a Live URL field with copy-to-clipboard.
- Each section shows a **Sample input** preview (the JSON the function
will receive) using the same payload builders the Test tab uses.
- Test tab: **Simulate trigger** buttons that prefill the JSON input
from the configured trigger's schema. Replaces an unclickable `<Select>`
(which auto-disables when there's only one option — the typical case).
- Read-only behavior for installed-app functions: explicit `<Callout>`
notice when there's no trigger; trigger sections render as disabled
controls when there is one.
- Removed the empty Environment Variables section from the Settings tab
(it just told the user to go elsewhere).

### New `pages/settings/layout/` folder

Three new app-scoped detail pages so users can drill into entities the
GraphQL `Application` type doesn't expose by id (keyed by manifest
`universalIdentifier`):

- `ApplicationViewDetail` — type, object, visibility + Fields / Filters
/ Sorts subsections (field UIDs resolved to readable labels via
`useFieldLabelByUid`)
- `ApplicationPageLayoutDetail` — type, object + per-tab subsections
listing widgets
- `ApplicationNavigationMenuItemDetail` — type, destination (resolved),
icon, color, position

Each page reads from the marketplace manifest the parent app page
already loads (no extra queries). Folder set up so a future "Layout"
settings tab can grow here (analogous to the existing `data-model/`
folder under the Data tab).

### Other consistency fixes

- Breadcrumbs on every app-scoped entity detail page now include a
category crumb so users know what they're looking at: `Workspace /
Applications / Timely / Navigation menu items / Time entry`.
- Title fallback for nav menu items uses the resolved destination
(`"Time entry"`) instead of the raw enum (`"OBJECT"`).
- New shared utils: `getNavigationMenuItemDestination`,
`resolveManifestObjectLabel`, `getLogicFunctionTriggerLabel`,
`<MonoText>`.

## Backend changes

Only one minor schema-shape change (additive): added `applicationId` to
the `SkillFields` GraphQL fragment and `universalIdentifier` to the
`RoleFragment` so the new lookups have what they need. Generated
metadata schema patched in-tree to match — regenerate with `nx run
twenty-front:graphql:generate --configuration=metadata` if it drifts.

## Test plan

- [ ] Application content tab on an installed app shows the 3 grouped
sections; rows in each section are clickable
- [ ] Click an Object → existing object detail page
- [ ] Click a Field → existing field-edit page
- [ ] Click an Agent / Skill / Role → existing detail page
- [ ] Click a View / Page layout / Navigation menu item → new read-only
detail page; subsections (Fields/Filters/Sorts for views, per-tab
widgets for page layouts) populate correctly
- [ ] Breadcrumbs on every entity detail page have 5 crumbs ending in
`<Category> / <Entity name>`
- [ ] Logic function Triggers tab: toggle each trigger type on/off, see
the Sample input preview update; for installed apps, sections render as
read-only
- [ ] Test tab: each "Simulate trigger" button prefills the JSON editor
with the matching payload shape
- [ ] Functions list: a function configured as `post-install` shows
"Post-install" in the Trigger column

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
2026-04-28 16:08:15 +02:00
martmullandGitHub 8998009805 Stop reseting isListed and is featured after each sync (#20111)
isListed and isFeatured are manually updated by the admin, it should not
be updated if the application already exists
2026-04-28 13:23:42 +00:00
Abdul RahmanandGitHub a710c105cf Fix orphan views by deferring record table widget view creation to dashboard save (#20006) 2026-04-28 10:06:26 +00:00
Sai Sathwik PandGitHub a5cd64daf5 refactor: standardize JsonStringified casing (#20101)
## Summary
- Rename safeParseRelativeDateFilterJSONStringified to
safeParseRelativeDateFilterJsonStringified
- Update the matching utility file, exports, tests, and workflow usages

Part of #19839.

## Validation
- CI passed
2026-04-28 09:01:26 +00:00
df3e217d64 fix(ai-billing): bill executeAgent in a finally block so failed runs don't leak (#20065)
## Summary

`AgentAsyncExecutorService.executeAgent` consumes Anthropic tokens at
two points (the main `generateText` and the optional structured-output
sub-call). Billing was previously the **caller's** responsibility,
executed only after `executeAgent` returned. If `executeAgent` threw —
e.g. when `structuredResult.output == null` for a schema-mismatched
response, or anything caught by the catch-and-rethrow — we paid
Anthropic but never recorded a `usageEvent`. Likely the dominant source
of the 716M-token-vs-3.27-credits discrepancy seen on the affected
workspace in the 2026-04-26 incident.

## What changed

- Inject `AiBillingService` into `AgentAsyncExecutorService`. Add
`workspaceId` (required), `userWorkspaceId`, and `operationType`
(default `AI_WORKFLOW_TOKEN`) to `executeAgent`'s args.
- Capture `accumulatedUsage`, `cacheCreationTokens`, and
`nativeWebSearchCallCount` into mutable locals as each `generateText`
resolves. A throw between the main and structured-output calls still
bills the first call's tokens; the schema-validation throw still bills
the merged usage.
- Wrap the body in `try { ... } finally { ... }`. The finally calls
`calculateAndBillUsage` and `billNativeWebSearchUsage`, each guarded by
its own `try/catch + logger.error` so a billing exception can't mask the
original execution error or block the second emit.
- `ai-agent.workflow-action.ts`: pass the new args; drop the
now-redundant billing calls and `AiBillingService` injection.
`AiBillingModule` removed from this action's module imports.
- `run-evaluation-input.job.ts`: pass `workspaceId` (already in `data`)
and `userWorkspaceId: null`. **As a side effect, the eval pipeline now
bills correctly** — closing an additional billing leak from the audit
(`RunEvaluationInputJob` previously called `executeAgent` and discarded
`executionResult.usage`).

## Behavior change worth calling out

Previously, failed agent executions were silently free. They will now be
billed for the tokens Anthropic charged us. This is intentional and
correct.

## Test plan
- [ ] Trigger a workflow agent action that succeeds — `usageEvent` count
should match what was previously emitted.
- [ ] Trigger a workflow agent with a JSON response schema and ambiguous
input that produces a non-schema-conforming output
(`structuredResult.output == null`) — verify a `usageEvent` row is now
written for the consumed tokens (was 0 rows previously).
- [ ] Trigger a `runEvaluationInput` GraphQL mutation — verify a
`usageEvent` row is written (was 0 rows previously).

## Notes for review
- Conflicts trivially with the Sentry-context PR on
`run-evaluation-input.job.ts`. Recommend merging Sentry-context first;
this PR's 2-line argument addition rebases inside that PR's
`aiCallContextService.run(...)` callback wrapper.
- A small follow-up after this lands: thread `billingContext` through
the `experimental_repairToolCall` callback in
`agent-async-executor.service.ts:198` (currently marked with a TODO from
the title-gen+repair-tool PR).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:43:53 +02:00
1c06506256 chore: sync AI model catalog from models.dev (#20106)
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-04-28 08:49:33 +02:00
Mohamed Houssein DouiciandGitHub d271ba06f1 chore: use navigation menu item type into the generated navigation menu layout (#20099)
This is a small PR to fix an issue that came up when I created an object
with the default navigation menu item.

<img width="811" height="461" alt="image"
src="https://github.com/user-attachments/assets/70c7f55d-3d41-48ed-8cf9-5f649ea41f49"
/>
2026-04-28 06:23:00 +00:00
7d35979fcf i18n - docs translations (#20100)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-28 00:31:22 +02:00
8d3047c1fc i18n - docs translations (#20097)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 22:44:15 +02:00
Félix MalfaitGitHubClaude Opus 4.7claude[bot] <41898282+claude[bot]@users.noreply.github.com>
e632b7dbb9 fix(ai-billing): bill POST /rest/ai/generate-text usage to ClickHouse (#20066)
## Summary

`POST /rest/ai/generate-text` calls `generateText` and returns `usage`
to the client without emitting a `usageEvent`. Authenticated, gated only
by `PermissionFlagType.AI` — any workspace user with that permission
could call it in a loop without billing. Identified during the
2026-04-26 incident audit.

## What changed

- Inject `AiBillingService` into `AiGenerateTextController`.
- Add `@AuthUserWorkspaceId() userWorkspaceId: string` to source the
user-workspace identifier.
- Wrap the `generateText` call in `try { ... return ... } finally { ...
}` so billing fires even if the controller throws after Anthropic was
paid.
- Bill with `UsageOperationType.AI_WORKFLOW_TOKEN` and
`cacheCreationTokens: result.usage.inputTokenDetails?.cacheWriteTokens
?? 0`.
- Inner `try/catch` around the billing emit so a billing error can't
break the response.
- One-line module change: `AiGenerateTextModule` imports
`AiBillingModule` (NestJS DI requirement).

## Test plan
- [ ] Call `POST /rest/ai/generate-text` with a small prompt; verify a
`usageEvent` row appears in ClickHouse for the workspace with the
correct token count and `operationType = AI_WORKFLOW_TOKEN`.
- [ ] Call with a malformed model id that throws after the API key is
validated — verify no spurious billing call occurs (no Anthropic call
was made).

## Notes for review
- Response shape unchanged.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-27 21:16:19 +02:00
5b3ee3f1a7 fix(ai-billing): bill thread title generation and tool-call repair (#20067)
## Summary

Two `generateText` call sites were unbilled — identified during the
2026-04-26 incident audit:

- **Thread title generation**
(`AgentTitleGenerationService.generateThreadTitle`) — fires once per new
chat thread; low-volume but completeness matters.
- **Tool-call repair** (`repairToolCall` util, used inside
`experimental_repairToolCall` callbacks) — can fire `MAX_STEPS` times
per agent turn if a model gets stuck producing malformed tool calls.

## What changed

- `AgentTitleGenerationService` — inject `AiBillingService`, expand
`generateThreadTitle` signature to accept `workspaceId` and
`userWorkspaceId`, bill in a `finally` block with
`UsageOperationType.AI_CHAT_TOKEN`. Restructured so the no-default-model
short-circuit happens before the `try`, avoiding a fake billing call.
- `repair-tool-call.util.ts` — added an optional `billingContext` arg
containing `aiBillingService`, `modelId`, `workspaceId`,
`userWorkspaceId`, `operationType`. Wraps the `generateText` call in
`try/finally`; bills in finally with the operation type provided by the
caller. Optional so existing callers keep compiling.
- `chat-execution.service.ts` — threads `billingContext`
(`AI_CHAT_TOKEN`) into the repair callback.
- `agent-chat.service.ts` — passes `workspaceId` and
`thread.userWorkspaceId` to `generateThreadTitle`.
- `agent-async-executor.service.ts` — TODO comment marking that the
repair callback should thread billing once `executeAgent` accepts
`workspaceId` (depends on the executeAgent-finally PR).

## Follow-up

After the executeAgent-finally PR lands, `workspaceId` is in scope at
the `experimental_repairToolCall` callback in
`agent-async-executor.service.ts:198`. Thread `billingContext` through
to fire repair-call billing for workflow agents too, and remove the
TODO. Tiny follow-up.

## Test plan
- [ ] Create a new chat thread; verify a `usageEvent` row is written for
the title generation call.
- [ ] Send a chat message that triggers tool-call repair (e.g. force a
malformed tool call); verify a `usageEvent` row for the repair sub-call.

## Notes for review
- `billingContext` arg made **optional** to allow incremental rollout —
the executeAgent-finally PR plus a small follow-up will close the
agent-async-executor side.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:54:40 +02:00
875795cc30 feat(sentry): propagate workspace context to all spans (#20064)
## Summary

After the 2026-04-26 token-usage incident, identifying the responsible
workspace from a Sentry trace required a Postgres scavenger hunt —
Vercel AI SDK auto-instrumentation captures token counts and model name
but no twenty-specific identifiers, and that same gap exists for every
other auto-instrumented span (HTTP outbound, Postgres queries, GraphQL
resolvers, Redis, etc.).

This PR plugs that gap globally, not just for AI:

- A small utility
(`packages/twenty-server/src/engine/core-modules/sentry/utils/sentry-workspace-context.util.ts`)
that writes workspace identifiers onto Sentry's active isolation scope
as a `twenty` context block plus filterable tags and a `Sentry.setUser`
call.
- Two hook points covering all server traffic:
- **`WorkspaceAuthContextMiddleware`** — already runs after token
hydration on the GraphQL, metadata, admin-panel, and REST routes. It now
calls the utility once per authenticated request, before delegating to
`withWorkspaceAuthContext`.
- **`BullMQDriver.work` and `SyncDriver.processJob`** — every queue job
now runs inside `Sentry.withIsolationScope` and applies workspace
context from `job.data.workspaceId` (skipping silently for system jobs
that don't carry one).
- A `beforeSendSpan` hook in `instrument.ts` that reads the scope's
`twenty` context block back and projects it onto every span as
`twenty.workspace.id` and (when available) `twenty.user_workspace.id` —
dotted-namespace naming consistent with OTel/Sentry conventions like
`user.id` and `http.response.status_code`. Spans without a workspace
context (unauthenticated traffic) pass through untouched.

## Why this shape

Sentry's docs position `beforeSendSpan` as a per-span hook. The previous
iteration set context only at AI-specific call sites, which left non-AI
spans (DB queries, outbound HTTP, regular GraphQL queries, workflow
steps not touching AI) entirely unenriched. Hooking the two existing
global boundaries — auth middleware for HTTP/GraphQL/REST, and the queue
driver `work()` callback for background jobs — covers every
authenticated span across the app with no per-handler instrumentation.

## What's not in this PR

AI-specific identifiers (`twenty.agent.id`, `twenty.thread.id`,
`twenty.turn.id`, `twenty.workflow_run.id`) are out of scope here.
They're useful additions but require either propagating the IDs through
the call stack or a more fine-grained scope (per-step, per-turn) than
the request/job boundary, which is best handled in follow-up PRs that
target those specific call sites.

## Test plan

- [ ] Make any authenticated GraphQL request locally and confirm the
resulting span(s) in Sentry carry `twenty.workspace.id` and (for
user-authenticated routes) `twenty.user_workspace.id`.
- [ ] Make any authenticated REST request and confirm the same.
- [ ] Trigger a queue job (chat stream, agent turn evaluation, workflow
run, etc.) and confirm spans produced inside the worker carry
`twenty.workspace.id`.
- [ ] Confirm that DB and outbound HTTP spans produced under the
request/job also carry the workspace tag — these previously had no
twenty-specific identifiers.
- [ ] In the Sentry UI, filter events by the `twenty.workspace.id` tag
and confirm matching events appear.

## Notes for review

- Sentry init lives in `instrument.ts`, loaded before Nest bootstraps,
so `beforeSendSpan` runs outside Nest DI and reads context off the
isolation scope rather than holding a service reference.
- The middleware change is three lines; the BullMQ wrap is a single
`Sentry.withIsolationScope` around the existing job handler body; the
SyncDriver wrap mirrors it for the dev/test path. No new modules or DI
providers.
- The previous iteration's `AiCallContextService` and per-handler
`setContext` / `withContext` calls have been removed in favor of these
two hooks.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:53:50 +02:00
350b835fbc i18n - docs translations (#20095)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 20:48:05 +02:00
ef49dad0bc i18n - docs translations (#20093)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 18:51:56 +02:00
Mohamed Houssein DouiciandGitHub e3a668a054 chore: export the enum view calendar layout to avoid typing errors on apps (#20090)
This is a small PR to fix an issue that came up when I created an app
with a custom object. I think the enum `ViewCalendarLayout` was not
caught as part of the exports.

<img width="1624" height="1114" alt="image"
src="https://github.com/user-attachments/assets/7bca3d0d-0e68-48ef-ac5f-6a3375733499"
/>
2026-04-27 16:35:22 +00:00
74536e6f98 Gate AI chat navigation entries by AI_SETTINGS permission (#20089)
## Summary

The AI chat history tab in the navigation drawer (and the corresponding
"New chat" icon in the mobile bottom bar) were rendered for every
authenticated user, regardless of whether they had `AI_SETTINGS`
permission. The actual chat content load is already gated — see
`AgentChatThreadInitializationEffect` — but the entry points were not,
so users without AI access saw a tab they could open and find empty.

This regressed when the `IS_AI_ENABLED` feature flag was removed in
#19916. The flag check was the only gate; nothing replaced it with a
permission check.

## Fix

Three small changes, all using
`useHasPermissionFlag(PermissionFlagType.AI_SETTINGS)`:

- **`MainNavigationDrawerTabsRow`** — return `null` when the user
  lacks AI access. This row only contains AI controls (the chat
  history tab pill + the "New chat" button), so without AI access
  there is nothing meaningful to render.
- **`MainNavigationDrawer`** — only render
`NavigationDrawerAiChatContent`
  when the user has AI access **and** the active tab is
  `AI_CHAT_HISTORY`. This is defensive: with the tabs row hidden the
  user can no longer switch to AI, but the active-tab atom could still
  carry `AI_CHAT_HISTORY` from a previous state (notably right after
  stopping impersonation of a user who had AI). Without this guard,
  the AI panel would briefly stay rendered.
- **`MobileNavigationBar`** — drop the `newAiChat` item when the user
  lacks AI access.

Surfaced by the same customer report that motivated #20088
(stop-impersonation
state cleanup), but this is a separate, pre-existing bug — admins
without AI permission see the tab regardless of impersonation.

## Test plan

- [ ] As a user **with** `AI_SETTINGS` permission, verify the AI chat
      tab and "New chat" button still appear and work in both the
      desktop sidebar and mobile bottom bar
- [ ] As a user **without** `AI_SETTINGS` permission, verify:
  - the tabs row at the top of the sidebar is gone (only the
    navigation menu shows below)
  - the "New chat" mobile bottom-bar icon is gone
  - the AI chat panel does not appear even after navigating away from
    a state where it was previously active

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:21:14 +02:00
b6ec2acb56 i18n - docs translations (#20087)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 16:59:00 +02:00
nitinandGitHub bb5f294c5a [AI] Collapse NativeToolBinder to a single bind() entry (#20051)
The binder doesnt need an agent or full tool context -- just a model and
options.
Single `bind(model, options)` entry!


Builds on #20022.
2026-04-27 16:35:59 +02:00
13aaea597e i18n - docs translations (#20083)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 14:56:00 +02:00
ec893c2767 i18n - translations (#20081)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-27 14:34:12 +02:00
Paul RastoinandGitHub f8c1ad5b3c Filtered upgrade logs when stopping before starting next instance segment (#20078)
# Introduction
In a nutshell, added a more readable logs that relates that when
performing a workspace fitlered workspace you can only browser the
workspace commands sequence

This PR is not a fix, but a log improvement
As before when cross-upgrading a single workspace you would be facing a
`instance` sync barrier error as not all your workspaces would have been
updated

Now logging and early returning instead of letting the guard hard throw

## Tests
Created a dedicated test for instance prevention on filtered upgrade
Standardized `migrationRecordToKey` usage across all upgrade integration
tests suites
2026-04-27 12:12:04 +00:00
Abdullah.andGitHub a90895e167 [Website] locale-segment routing and shared Lingui factory (#20079)
**1. Shared Lingui factory in `twenty-shared`**

- Extracted `createI18nInstanceFactory` into
`packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts` so
every package gets the same per-render Lingui bootstrap with a
per-locale singleton cache and a `SOURCE_LOCALE` fallback.
- `twenty-emails/src/utils/i18n.utils.ts` now consumes the shared
factory.

**2. `twenty-website-new` Lingui bootstrap + Crowdin wiring**

- `lingui.config.ts`, `src/lib/i18n/*`, `nx run
twenty-website-new:lingui:{extract,compile}`.
- 31 locale PO files generated; minified compiled output kept out of
Prettier and Oxlint.
- `i18n-{push,pull}.yaml` workflows updated to include
`twenty-website-new` in Crowdin sync.

**3. `app/[locale]/...` segment routing with English at the root**

- All marketing routes moved under `src/app/[locale]/`; static
generation preserved (15 routes × 31 locales = 465 prerendered URLs).
- Middleware behavior:
  - `/{en}/...`         → 301 redirect to unprefixed canonical.
  - `/{non-en}/...`     → pass through, set `NEXT_LOCALE` cookie.

### What this PR explicitly does not do (deferred)

- Lingui-wrapping the actual marketing copy. Keys, build pipeline, and
  runtime are wired; copy migration is a separate, reviewer-friendlier
  PR.
2026-04-27 12:11:30 +00:00
Charles BochetandGitHub 9b7f7d059a docs: document rawBody on RoutePayload (#20080)
## Summary

Follow-up to #20061, which added `rawBody?: string` to
`LogicFunctionEvent` / `RoutePayload` so logic functions can verify
HMAC-style webhook signatures (GitHub's `X-Hub-Signature-256`, Stripe,
…) against the exact bytes that were received.

That PR did not update the public SDK docs, so the new field is
effectively invisible to developers consuming `RoutePayload` from
`twenty-sdk`. This PR adds a single row to the `RoutePayload` structure
table in `logic-functions.mdx` so the field is discoverable.

Addresses the review feedback on
https://github.com/twentyhq/twenty/pull/20061#discussion_r3147065014.
2026-04-27 12:06:55 +00:00
95fee18126 fix(server): match IMAP \Noselect attribute case-insensitively (#20043)
## Summary

`ImapGetAllFoldersService.isMailboxSelectable` checked
`mailbox.flags?.has('\\Noselect')`, which is case-sensitive. Per [RFC
3501 §6.3.8](https://www.rfc-editor.org/rfc/rfc3501#section-6.3.8), IMAP
attribute names are case-insensitive — different servers spell the flag
differently:

| Server | Spelling |
|---|---|
| Dovecot | `\Noselect` |
| Stalwart | `\NoSelect` |
| Cyrus | `\Noselect` |
| RFC examples | `\NOSELECT` |

The previous check only caught Dovecot's spelling. On other servers,
virtual namespace placeholders (e.g. Stalwart's `Shared Folders` parent)
passed through `isMailboxSelectable` and got persisted as folders. When
`MessagingMessageListFetchJob` later ran, it issued `SELECT "Shared
Folders"`, the server correctly rejected it with `NO [NONEXISTENT]
Mailbox does not exist.`, and the entire message-list fetch failed for
the channel.

## Reproduction

1. Connect an IMAP account whose server advertises a `\NoSelect` (or
`\NOSELECT`) namespace placeholder. Stalwart Mail v0.15.x exhibits this
with shared mailboxes:
   ```
   * LIST (\NoSelect) "/" "Shared Folders"
   ```
2. The folder discovery job persists it as a syncable folder.
3. `MessagingMessageListFetchJob` runs and fails:
   ```
   IMAP: Error fetching message list: D0 SELECT "Shared Folders"
   responseStatus: NO  serverResponseCode: NONEXISTENT
   ```

## Fix

Iterate the flag set and lowercase-compare against `'\\noselect'`. This
matches every legal spelling without changing behavior for compliant
`\Noselect` clients.

```diff
 private isMailboxSelectable(mailbox: ListResponse): boolean {
-  return !mailbox.flags?.has('\\Noselect');
+  if (!mailbox.flags) return true;
+  for (const flag of mailbox.flags) {
+    if (flag.toLowerCase() === '\\noselect') return false;
+  }
+  return true;
 }
```

## Test plan

- [x] Existing `\Noselect` test cases (`should not issue STATUS against
a \Noselect folder`, parent-reference preservation, Sent-folder
exclusion) still pass — the lowercase comparison subsumes them.
- [x] New parameterized test covers `\Noselect`, `\NoSelect`,
`\NOSELECT`, `\noselect` spellings against a Stalwart-style `Shared
Folders` namespace placeholder, asserting Twenty does **not** issue
`STATUS` on the placeholder and does **not** include it in the
discovered folder set.

---------

Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
2026-04-27 11:58:02 +00:00
neo773andGitHub 5b8804ff06 gmail extract body from deeply nested MIME parts (#19989)
/closes #19879
2026-04-27 11:56:50 +00:00
neo773andGitHub 6545ca274c fix(messaging): refactor SentMessagePersistenceService (#20077)
refactored `SentMessagePersistenceService` to be thin wrapper over
`saveMessagesAndEnqueueContactCreation`
this fixes a bug where the old logic did not call match participants
causing messages to not show up
2026-04-27 11:56:41 +00:00
15c52d3a39 Documentation update (#20059)
Add missing info about how to edit navigation bar, add many-to-many
relation and get Enterprise key for Org license on self-hosted

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-27 11:51:42 +00:00
3db1af9a17 fix(logic-function): forward raw request body for HMAC signature verification (#20061)
## Summary

- Add optional `rawBody?: string` to `LogicFunctionEvent` and forward it
from the route trigger so HMAC-based webhook signatures (GitHub's
`X-Hub-Signature-256`, Stripe, …) can be verified by user logic
functions.
- Update `github-connector`'s `getRawBodyForSignature` to prefer
`event.rawBody` (with the existing string/base64/null fallbacks kept for
older runtimes).

## Why

GitHub computes `X-Hub-Signature-256` over the **raw bytes** of the
request body. The receiver must verify against those exact bytes — key
order, whitespace and unicode escaping all matter, so the parsed JSON
body cannot be re-serialized to them.

Today the route trigger calls `extractBody(request)` which returns the
parsed object only. NestJS already preserves the raw body on
`request.rawBody` (the app is bootstrapped with `rawBody: true` in
`main.ts`), but it was never propagated into `LogicFunctionEvent`.

As a result the github-connector's webhook handler always took the "raw
body unavailable" branch and rejected every delivery (after #19961 /
962c2b3c14). With this change, signature verification can succeed
end-to-end.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:36:02 +00:00
Paul RastoinandGitHub 1e7c1691f4 [CI] Prevent previous version upgrade sequence mutation (#20075)
# Introduction
Prevent any PR to target a previous already released twenty version by
mistake.

Especially useful for existing opened PR introducing commands into an
upgrade that has just been released leading to a
`TWENTY_CURRENT_VERSION` bump

<img width="3150" height="1158" alt="image"
src="https://github.com/user-attachments/assets/b83d211f-a061-4d63-ae7a-354d7851ec08"
/>

## Bypass
If intentional add `ci:allow-previous-version-upgrade-mutation` label to
the PR and re-run the failed job

<img width="3150" height="1158" alt="image"
src="https://github.com/user-attachments/assets/f94ee630-d87b-4477-9e50-bf6773a8a280"
/>

This will require a brand new ci from a commit introduced after the
label has been added
2026-04-27 11:15:49 +00:00
577312f121 chore: sync AI model catalog from models.dev (#20073)
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-04-27 08:46:56 +02:00
fb8b9cb86c Add admin avatars and app logos (#20001)
## Summary
- Add user avatars and workspace logos to the admin general table and
workspace member detail view
- Show app icons in the admin app registrations table and reuse the
shared application display component
- Expose the needed avatar and logo fields through admin GraphQL queries
and backend lookup/statistics services
- Keep workspace fallback behavior consistent when no logo is set and
clean up a few local table styling duplicates

## Testing
- `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json
--noEmit --pretty false`
- Manual UI verification of the admin general, workspace detail, and
apps tables

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-26 17:15:20 +00:00
Charles BochetandGitHub 499067ae14 fix(logic-function): serialize LocalDriver layer builds with cache lock (#20054)
## Summary

Fixes a race condition in `LocalDriver` where concurrent `execute()`
calls for the same application can trash each other's layer build,
causing logic function executions to fail with either:

- `Error: ENOENT: process.cwd failed with error no such file or
directory, the current working directory was likely removed without
changing the working directory, uv_cwd` (the yarn-install child's `cwd`
got wiped mid-run), or
- `ENOENT: no such file or directory, open '…/<pkg>/<file>.js.map'`
raised from yarn's berry link step (files under the deps layer vanish
while yarn is extracting).

Both are thrown from `…/deps/<checksum>/.yarn/releases/yarn-4.9.2.cjs` —
i.e. the yarn process `copyYarnEngineAndBuildDependencies` spawns with
`cwd: buildDirectory`.

## Root cause

`LocalDriver.createLayerIfNotExist` (and `ensureSdkLayer`) both follow a
check-then-act pattern with no mutual exclusion:

```ts
if (await pathExists(depsNodeModulesPath)) return;
await fs.rm(depsLayerPath, { recursive: true, force: true });
await copyDependenciesInMemory(...);
await copyYarnEngineAndBuildDependencies(depsLayerPath); // spawns yarn with cwd=depsLayerPath
```

The deps layer path is shared across all `execute()` calls that match a
given `yarnLockChecksum`. Two concurrent callers (e.g. a webhook
invocation + a cron-triggered logic function firing while the first
run's layer is still being built) both see no `node_modules`, both
`fs.rm` the directory, and one's yarn child ends up with its `cwd` or
its extraction target gone.

## Fix

Wrap the critical sections with `CacheLockService.withLock` — the same
cache-backed lock the Lambda driver already uses for its layer/executor
builds:

- `createLayerIfNotExist` → lock key
`local-driver-deps-layer:${yarnLockChecksum ?? 'default'}`
- `ensureSdkLayer` → lock key
`local-driver-sdk-layer:${workspaceId}:${applicationUniversalIdentifier}`

A fast-path `pathExists` check is kept outside the lock (so warm
executions still skip lock acquisition entirely), and the existence +
staleness check is **repeated inside the lock** so followers no-op after
the leader finishes.

Lock parameters mirror the Lambda driver's layer-build lock (`ttl=120s`,
`retry=500ms`, `maxRetries=240`).

`cacheLockService` is now passed to `LocalDriver` from
`LogicFunctionDriverFactory` (it was already injected there for the
Lambda driver).
2026-04-26 12:44:40 +00:00
nitinandGitHub ca84d28157 [AI] ai usage line chart date gap filling (#20048)
https://github.com/user-attachments/assets/ecd37dfb-daae-41c3-8316-a9d923463eca
2026-04-26 12:41:16 +00:00
nitinandGitHub d1a4902460 [AI] Drop 'serialization' from tool output naming (#20052)
didnt made sense anymore -- we dont really 'serialize'
2026-04-26 14:10:01 +02:00
Paul RastoinandGitHub 89ad87aa64 Make twenty-front build env agnostic (#20055)
## Introduction
In aim to reduce and optimize the number of twenty-front build we do
during our cd process and allow twenty-front build promotion

### Build time

**Nothing is baked.** The `build/` directory is a clean, env-agnostic
artifact. `index.html` contains the empty placeholder:

```html
<script id="twenty-env-config">
  window._env_ = {
    // This will be overwritten
  };
</script>
```

The JS bundles contain no hardcoded server URL.

---

### Deploy mode 1: Frontend served by the backend (Docker / NestJS)

1. Container starts, NestJS boots in `main.ts`
2. `generateFrontConfig()` runs, reads `process.env.SERVER_URL`
3. Rewrites `dist/front/index.html`, replacing the placeholder with:
   ```html
   <script id="twenty-env-config">
     window._env_ = {
       REACT_APP_SERVER_BASE_URL: "https://api.example.com"
     };
   </script>
   ```
4. NestJS serves the static `dist/front/` directory
5. Browser loads `index.html`, `window._env_` is set before the app JS
executes
6. `src/config/index.ts` reads `window._env_.REACT_APP_SERVER_BASE_URL`
and uses it

---

### Deploy mode 2: Frontend served standalone (CDN / nginx / static
server)

1. Take the `build/` artifact as-is
2. Before serving, run at deploy time:
   ```bash
REACT_APP_SERVER_BASE_URL=https://api.example.com sh
./scripts/inject-runtime-env.sh
   ```
3. This does the same `sed` replacement on `build/index.html`
4. Serve the `build/` directory with your static server of choice
5. Same resolution in the browser:
`window._env_.REACT_APP_SERVER_BASE_URL` is picked up by
`src/config/index.ts`

---

### Fallback: no injection at all

If neither mechanism runs (e.g. local dev with `vite dev`),
`window._env_.REACT_APP_SERVER_BASE_URL` is `undefined`, and
`getDefaultUrl()` kicks in:
- **Localhost**: returns `http://localhost:3000`
- **Non-localhost**: returns same-origin (`window.location.origin`)
2026-04-26 07:05:54 +00:00
Charles BochetandGitHub 0c5c9c844e feat(github-connector): exclude self-reviews from review counts (#20050)
## Summary

- Adds an `isSelfReview` boolean to the consolidated `PullRequestReview`
record (`true` when the reviewer is the same contributor as the PR
author).
- Filters `isSelfReview === true` rows out of the top-reviewers
leaderboard (`top-contributors`) and per-contributor review stats
(`contributor-stats`) so contributors are credited only for reviews on
**other people's** PRs.
- Both the live ingestion path (`fetch-prs`) and the recompute job
(`recompute-pull-request-reviews`) now thread `prAuthorId` to
`buildConsolidatedRow`, so re-running either is sufficient to backfill
existing rows — no extra migration needed.

## Test plan

- [x] `yarn test
src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts`
— 20 tests passing, including new coverage for the `isSelfReview` helper
and `buildConsolidatedRow` payload.
- [ ] After merge: re-run `fetch-prs` (or
`recompute-pull-request-reviews`) once on a target workspace to backfill
`isSelfReview` on existing `PullRequestReview` rows, then verify the
top-reviewers leaderboard no longer credits self-reviews.


Made with [Cursor](https://cursor.com)
2026-04-25 23:34:01 +02:00
Charles BochetandGitHub 41571ea377 feat(front-component-renderer): forward offset/movement coordinates on serialised events (#20046)
## Summary

Adds `offsetX`, `offsetY`, `movementX`, `movementY` to
`SerializedEventData` and the host event serialiser so apps can reason
about element-relative pointer positions without trying to read the host
element's bounding rect (which is impossible from a remote-DOM worker).

## Motivation

I was building a front-component with click-to-drop-pin and trackpad
pan/zoom (custom OSM tile renderer). Two real bugs surfaced from the
current event-serialisation surface:

1. **Wheel pan/zoom was broken.** The host already forwards
`deltaX`/`deltaY`, but app authors naturally read them off the
React-style event handler argument as `e.deltaX`/`e.deltaY`. Because
remote-DOM bridges everything via `RemoteEvent extends
CustomEvent<Detail>`, the payload actually arrives at `e.detail.deltaX`.
Reading the wrong place gives `undefined`, and `undefined < 0 ===
false`, so every wheel notch zoomed in the same direction. App code now
uses `e.detail`, but this was a sharp papercut worth flagging in docs /
a helper (separate change).

2. **Element-local click coords are unobtainable from a worker.** With
only `clientX/Y`, an app needs the stage's bounding rect to translate
viewport coordinates to local — which can't be read across the worker
boundary. `offsetX`/`offsetY` close that gap with a one-read solution.
`movementX`/`movementY` round out the set for any future drag-style
interactions if `mousemove` later joins the allow-list.
2026-04-25 10:12:28 +00:00
570038ad65 chore: sync AI model catalog from models.dev (#20045)
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-04-25 08:26:54 +02:00
Charles BochetandGitHub 88add35f0b Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.1.0 (#20041)
## Summary

- Bumps `twenty-sdk` from `2.1.0-canary.1` to `2.1.0`.
- Bumps `twenty-client-sdk` from `2.1.0-canary.1` to `2.1.0`.
- Bumps `create-twenty-app` from `2.1.0-canary.1` to `2.1.0`.
2026-04-24 23:10:40 +02:00
Charles BochetandGitHub d74e3fa3b5 chore(server): bump current version to 2.2.0 (#20040)
## Summary

We are releasing Twenty v2.2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Promote `2.1.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.2.0`
- Reset `TWENTY_NEXT_VERSIONS` to `[]`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.2.0` / `2-2-` slug)
- Update the failing-sequence-runner snapshot to include `2.2.0` in the
covered versions list

The `2-2/` upgrade-version-command module is already in place and wired
into `WorkspaceCommandProviderModule`, so future upgrade commands
targeting `2.2.0` can land directly under `2-2/` (or be generated
against `--version 2.2.0`).
2026-04-24 22:19:16 +02:00
Charles BochetandGitHub aad77b5315 Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.1.0-canary.1 (#20038)
## Summary

- Bumps `twenty-sdk` from `2.1.0` to `2.1.0-canary.1`.
- Bumps `twenty-client-sdk` from `2.0.0` to `2.1.0-canary.1`.
- Bumps `create-twenty-app` from `2.0.0` to `2.1.0-canary.1`.
2026-04-24 19:57:02 +00:00
bba102efe3 chore: remove accidentally committed .claude-pr/ directory (#20036)
## Summary

Removes the `.claude-pr/` directory at the repo root, which contains
byte-identical duplicates of `CLAUDE.md` and `.mcp.json`.

## Why

- Added in #19517 (a PR about file-attachment support for agent chat),
unrelated to its content — looks like an accidental commit of a local
scratch directory.
- Not referenced from any workflow, script, or doc in the repository.
`.github/workflows/claude.yml` uses the root `CLAUDE.md` / `.mcp.json`,
not the copies under `.claude-pr/`.
- Because it's a duplicate of the source of truth, it will silently
drift out of sync over time.

## Test plan

- [x] Confirmed no references to `.claude-pr` anywhere in `*.yml`,
`*.yaml`, `*.json`, `*.md`, `*.sh`, `*.ts`, `*.tsx`
- [x] Confirmed `.github/workflows/claude.yml` does not reference it
- [x] `.claude-pr/CLAUDE.md` and `.claude-pr/.mcp.json` are
byte-identical to the root copies at the time of this PR

If this directory is actually needed by an internal tool I missed, feel
free to close — happy to be corrected.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:53:16 +02:00
Charles BochetandGitHub 80c0e5603d Move isPreInstalled applicationRegistration instance command to 2.1 (#20037)
## Summary

The fast instance command adding `isPreInstalled` to
`core.applicationRegistration` was added after 2.0 was released, so it
must run as part of the 2.1 upgrade rather than 2.0.

- Renamed
`2-0/2-0-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
to
`2-1/2-1-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
- Updated `@RegisteredInstanceCommand` version from `'2.0.0'` to
`'2.1.0'`
- Updated the import path in `instance-commands.constant.ts`

The original timestamp (`1776886452831`) was kept; it is smaller than
the existing 2.1 fast command timestamp, so this command will simply run
first within the 2.1.0 batch.
2026-04-24 17:52:58 +00:00
Abdullah.andGitHub 47710908a8 [Website] Architecture, hardening, and perf pass. (#20020)
This PR is the result of a critical architectural review of
`twenty-website-new` and the
follow-on cleanup work. It does not touch any other package. Scope is
everything except
the enterprise/billing routes (deferred to a separate PR) and CI wiring
(also deferred).

### Why

The package had grown organically: ad-hoc scroll/motion/halftone code
per section,
WebGL renderers instantiated raw, sections without a shared shape
contract, route-scoped
files leaking across layers, public API routes without rate limiting /
timeouts /
schema validation, unbounded module-level caches in visual code,
drag/resize handlers
re-rendering React 60x/sec, no security headers, and a Lottie
scroll-mapping that would
silently drift if the asset got re-exported. We needed contracts and
primitives in
place before further work (i18n, decomposition of the giant visual
files, MDX migration
of customer/legal pages) is safe to do.

### What changed

**Layering and contracts (now enforced, not just documented).**
- New layering rule: `app → sections → lib → design-system / theme /
icons`.
Cross-section reuse goes through `lib/`. Flipped the design-system
import rule
  from warn to error.
- Extracted shared primitives into `lib/`: `scroll`, `motion`,
`halftone`, `customers`,
`partner-application`, `api`, `seo`, `semver`, `community`,
`visual-runtime`.
- Lifted route-scoped data/types out of `app/` into `lib/` +
`sections/`.
- Section shape contract: every section exposes a single compound export
from
`components/index.ts(x)`; non-leaf sections own the outer `<section>`
from
`Root.tsx`; named slots are matched by `displayName` (no
`Children.toArray`
  positional indexing). Enforced by `scripts/check-section-shape.mjs`.
- WebGL boundary: `new THREE.WebGLRenderer(...)` is forbidden outside
`src/lib/visual-runtime/`. Everything goes through
`createSiteWebGlRenderer`,
which enforces the site-wide context cap, the
`NEXT_PUBLIC_DISABLE_HEAVY_VISUALS`
  kill switch, and GPU/power-preference defaults. Enforced by
`scripts/check-boundaries.mjs` with per-line
`boundary-allow-next-line:<rule-id>`
  escape hatches and stale-directive detection.

**Design system grew to cover real cases.**
- Added Modal, Form, and Layout primitives (Stack / Inline / Grid) so
sections stop
reinventing them. Built on `@base-ui/react` for accessibility + focus
management.

**Public API hardening (non-enterprise).**
- New `lib/api/` primitives: `createRateLimiter` (in-memory token
bucket),
`fetchWithTimeout`, `readJsonBody` (Zod-validated). Applied to
newsletter,
community, and partner-application routes. `/api/partner-application`
specifically
  got a per-IP rate limit, body cap, and timeout.

**Performance.**
- `DraggableTerminal` and `DraggableAppWindow`: `pointermove` now
mutates `transform`
(via `translate3d`) and `width`/`height` directly on the DOM ref. React
state
commits only on `pointerup`. Eliminates per-frame re-renders during
interaction.
- `createBoundedFailureCache` (FIFO, 256 entries) replaces unbounded
module-level
failure caches in four visual components. Bounds memory growth from bad
asset URLs.

**Lottie frame-map guard.**
- `dotlottie-react`'s `player.totalFrames` returns a raw float (`op -
ip`), not an
integer. The HomeStepper scroll → frame map is keyed to the authored
timeline,
  so silent drift would desync every step boundary.
- Reads now `Math.floor(player.totalFrames)` consistently.
- `scripts/check-lottie-frames.mjs` extracts `op - ip` from
`public/lottie/stepper/stepper.lottie` at build time and asserts it
against
`HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES`. If anyone re-exports the
Lottie,
the build fails until both that constant and every `STEP_*_END` are
updated together.

**Security headers (`next.config.ts`).**
- HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy:
strict-origin-when-cross-origin`,
`Permissions-Policy` (camera/mic/geolocation/payment off),
`X-Frame-Options: DENY`,
  `Content-Security-Policy: frame-ancestors 'none'`.
- Full CSP intentionally deferred until we enumerate all third-party
origins
  (Cal.com, Stripe, GitHub avatars, twenty-icons.com, etc.).

**Build / config quirks documented in code.**
- `tsconfig.json` is standalone (does NOT extend the monorepo base) —
Next.js +
  React Compiler require options that conflict with the base config.
- `sharp` moved from `devDependencies` to `dependencies` so production
image
  optimization works.

### What's deliberately NOT in this PR

- **Enterprise / billing routes** — open redirect on
`/api/enterprise/checkout`,
indefinite-bearer JWT, non-idempotent Stripe seat updates, unpinned
Stripe
`apiVersion`, missing webhook reconciliation, inconsistent error
envelope.
  Going out as a separate, security-focused PR.
- **CI workflow for `twenty-website-new`** (`lint` / `typecheck` /
`test` / `build`
  targets) — separate follow-up PR.
- **i18n via Lingui** — decision made (we need internationalization and
we already
use Lingui in `twenty-front` / `twenty-emails`); 4-phase migration plan
exists
  but does not land here.
- **Decomposition of giant visual files** (HomeVisual, ThreeCards
visuals) — blocked
  on the i18n landing first; otherwise we'd rebase the world twice.
- **Customer / legal pages → MDX** — same reason.
- **Selective memoization pass** — needs browser profiling, not blind
`useMemo`.
- **Pre-existing lint errors / typecheck noise** (~44 errors, ~41
warnings, plus
generated Next.js types and `@ts-nocheck` files) are unchanged. The
cleanup
  did not introduce new ones.

### Test plan

- [ ] `yarn install`
- [ ] `yarn nx run twenty-website-new:dev` — homepage, customers,
partner,
enterprise activate, blog, why-twenty, plans/pricing, legal pages
render.
- [ ] HomeStepper: scroll through, confirm the Lottie animation lines up
with
every step boundary. Console must NOT log a `totalFrames` mismatch.
- [ ] HomeVisual: drag and resize the terminal + app window; verify
smoothness
(no per-frame React re-renders) and that final position/size persists on
release.
- [ ] Public API endpoints: hit `/api/newsletter`, `/api/community`,
`/api/partner-application` with bad payloads → expect 4xx with Zod
errors,
not 500s. Hammer `/api/partner-application` past the per-IP limit → 429.
- [ ] Response headers on any page include HSTS, nosniff, referrer
policy,
permissions policy, X-Frame-Options, and `frame-ancestors 'none'` CSP.
- [ ] `yarn nx run twenty-website-new:lint` — error/warning count must
not exceed
      the pre-existing baseline.
- [ ] `yarn nx run twenty-website-new:typecheck` — same baseline rule.
- [ ] `node packages/twenty-website-new/scripts/check-boundaries.mjs` —
passes,
      no stale directives.
- [ ] `node packages/twenty-website-new/scripts/check-section-shape.mjs`
— passes.
- [ ] `node packages/twenty-website-new/scripts/check-lottie-frames.mjs`
— passes.
- [ ] `yarn nx run twenty-website-new:build` — green, including the
three checks
      above if wired into the build target.
2026-04-24 16:40:09 +00:00
Charles BochetandGitHub 0bb3660844 chore(twenty-sdk): shrink logic-function bundles via stubbing (#20033)
## Summary

Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.

This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.

## What changes

- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
  - Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.

## Why this is safe

- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.

## Measurements (github-connector)

| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
2026-04-24 17:51:35 +02:00
WeikoandGitHub 2ccc293f99 Gate export/import command menu items by permission flag (#19991)
## Summary
- Hides the `exportRecords`, `exportView`, and `importRecords` command
menu actions from
users whose role does not hold the matching `EXPORT_CSV` / `IMPORT_CSV`
permission flag.
- Exposes the current user's role permission flags to
`conditionalAvailabilityExpression`
by adding `permissionFlags: Record<string, boolean>` to
`CommandMenuContextApi`, mirroring
 how `featureFlags` is already accessible.
- Adds a `2.1.0` workspace upgrade command that rewrites the three
existing rows on every
 active/suspended workspace.

## Before
<img width="1294" height="287" alt="Screenshot 2026-04-22 at 19 37 40"
src="https://github.com/user-attachments/assets/11ca8635-14d7-40a0-9ca0-76329c54e3c6"
/>

## After
<img width="1283" height="285" alt="Screenshot 2026-04-22 at 19 32 25"
src="https://github.com/user-attachments/assets/5e49fa8a-4541-42ee-96da-4c1de7d00aae"
/>
2026-04-24 15:22:44 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix MalfaitFélix Malfait
6c1c0737b0 Clarify registry tools vs native model tool binding (#20022)
## Intent

This is a small foundation cleanup for the tool architecture.

The main decision is: registry tools and native SDK/model tools are
different things.

- Registry tools have descriptors, schemas, catalog entries, and execute
through `ToolExecutorService`
- Native model tools are opaque AI SDK objects, bound directly into the
model `ToolSet`
- Surfaces still own their policy: chat, MCP, and workflow agents decide
what they expose

## What changed

- Removed `NATIVE_MODEL` from `ToolCategory`
- Kept `ToolRegistryService` focused on registry-backed tools only
- Moved native model tool binding through `NativeToolBinderService`
- Reused native binding from chat instead of duplicating
provider-specific web-search logic
- Kept MCP local execution exclusions in a dedicated constant
- Moved surface-specific constants into dedicated constant files

## What comes next

- Move hardcoded chat app preloads, like Exa web search, into
app/manifest metadata
- Decide a clearer policy for local runtime tools like code interpreter
and HTTP request
- Gradually document the three tool shapes: registry tools, native model
tools, and local runtime tools

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-24 17:18:19 +02:00
251f5deab6 [breaking, deploy server first] fix(ai-chat): persist providerExecuted flag on tool parts (#20030)
## Summary
Fixes Sentry errors of the form:

> \`messages.3: \`tool_use\` ids were found without \`tool_result\`
blocks immediately after: srvtoolu_…. Each \`tool_use\` block must have
a corresponding \`tool_result\` block in the next message.\`

### Root cause
When the model invokes a **provider-hosted tool** (e.g. Anthropic's
native \`web_search\` — note the \`srvtoolu_\` ID prefix), the AI SDK
marks the resulting \`UIMessagePart\` with \`providerExecuted: true\`.
\`convertToModelMessages\` uses that flag to emit the
tool_use/tool_result pair *inside the same assistant message* — the
format Anthropic requires for server-side tools.

Our \`AgentMessagePart\` persistence was dropping \`providerExecuted\`
on the way to the DB (and re-hydration didn't know to set it). On the
next turn, \`convertToModelMessages\` treated the rehydrated part as a
client-side tool call, splitting it into \`assistant(tool_use)\` +
\`user(tool_result)\` — which Anthropic then rejects with the error
above.

### Fix
- Add nullable \`providerExecuted BOOLEAN\` column on
\`core.agentMessagePart\` via a fast instance command.
- Surface the field on \`AgentMessagePartDTO\` (GraphQL).
- Preserve it through \`mapUIMessagePartsToDBParts\` (server) and both
\`mapDBPartToUIMessagePart\` mappers (server + frontend).
- Include it in \`GET_CHAT_MESSAGES\` and \`GET_AGENT_TURNS\`
selections.
- Regenerate \`generated-metadata/graphql.ts\`.

### Backwards compatibility
Existing rows have \`NULL providerExecuted\` and round-trip as the
omitted flag — which is exactly the pre-fix behaviour for tool parts
that were never provider-executed. Only *new* assistant messages using
\`web_search\` (or other provider-hosted tools) will write \`true\`, and
those are the only ones that were breaking.

## Test plan
- [x] \`npx tsgo\` typecheck — server + front clean
- [x] \`oxlint\` + \`prettier --check\` on all touched files — clean
- [x] \`npx nx run twenty-server:database:migrate:prod\` runs the new
instance command locally; \`providerExecuted\` column present on
\`core.agentMessagePart\`
- [x] Regenerated \`generated-metadata/graphql.ts\` —
\`providerExecuted\` wired into both queries and \`AgentMessagePart\`
type
- [ ] Manual: start a chat with Anthropic web_search enabled, invoke the
tool in turn 1, reply in turn 2 — should not throw the srvtoolu error

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:58:26 +02:00
WeikoandGitHub 5f604a503a backfill widget position from gridPosition (phase 1 of gridPosition removal) (#20032)
## Context
Phase 1 of removing the legacy `gridPosition` field from
`PageLayoutWidget` in favor of the new `position` discriminated union
(`grid` / `vertical-list` / `canvas`). This PR is purely additive —
`gridPosition` is still required and read everywhere; we just guarantee
that every widget now also has a non-null `position` so a follow-up PR
can drop `gridPosition` cleanly.

## Changes
- **Slow instance command**
`BackfillPageLayoutWidgetPositionSlowInstanceCommand` (2.1.0):
for every `core.pageLayoutWidget` row where `position IS NULL`, copies
`gridPosition`
into `position` with `layoutMode: 'GRID'`. Historically only grid
widgets used
`gridPosition`, so a single SQL update covers every existing row.
- **`PageLayoutDuplicationService`**: when duplicating a widget, also
forwards
`originalWidget.position` (was previously only forwarding
`gridPosition`).
- **Frontend default layouts** (10 `Default*PageLayout.ts` files): added
a `position`
sibling to every widget, matching the parent tab's `layoutMode` —
`VERTICAL_LIST` widgets
get `{ layoutMode, index }`, `CANVAS` widgets get `{ layoutMode }`

<img width="338" height="226" alt="Screenshot 2026-04-24 at 16 23 17"
src="https://github.com/user-attachments/assets/c3319318-f1b8-4271-96b4-196b209a1f5e"
/>
2026-04-24 14:35:56 +00:00
Paul RastoinandGitHub 9771725cc4 Dockerfile twenty-server target (#20028)
# Introduction
Creating a target funnel that allow bypassing front build and injection
in the server

## New targets
- `twenty-server` only ships server
- `twenty` ships both front and back in the same image
- `twenty-server-aws` only ships server and `aws-cli`
- `twenty-aws` ships both front and back in the same image with the
`aws-image`
2026-04-24 14:24:32 +00:00
WeikoandGitHub 2a3c759928 Removing community apps in favor of npm-distributed apps (#20029)
## Context
With the registry pattern around `Twenty Applications` and the upcoming
marketplace, app
distribution can be decoupled from the core repo. Pulling apps from npm
instead of
shipping them in-tree means:

- Contributors don't clone / index / build code for apps they'll never
touch
- Each app can version, release, and iterate independently of the core
cadence
- We stop carrying apps that are effectively unmaintained in the
monorepo

## How
This PR removes the 11 community apps from
`packages/twenty-apps/community/`

## Note
- **Marketplace apps** must be published on npm and pulled from there —
no app code living in this repo just to be distributed
- **Showcase / example apps** stay as living examples of what the SDK
can do under `example/`
- **Officially-maintained marketplace apps** (like the new
`github-connector`?) stay in the repo under `internal/`. Some may be
pre-installed on new workspaces alongside the Standard app
2026-04-24 13:50:08 +00:00
a36c67a500 i18n - translations (#20027)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-24 12:33:40 +02:00
nitinandGitHub 097432d3a2 [Command Menu] Refactor layout customization conditional availability [Warning] (#19974)
closes
https://discord.com/channels/1130383047699738754/1494312529286004837
2026-04-24 10:18:02 +00:00
WeikoandGitHub 3deb467845 Fix layout edition mode dark mode text color - 2 (#20024)
Followup https://github.com/twentyhq/twenty/pull/19992

Added button { --tw-button-color: ${GRAY_SCALE_LIGHT.gray1}; } to
StyledContainer,
mirroring the existing color override. This forces the loader's color
(which reads
var(--tw-button-color) in Loader.tsx) to stay white on the blue bar
regardless of theme —
in dark mode, the button's inline --tw-button-color previously resolved
to a dark
font.color.inverted, making the spinner nearly invisible.

## Before
<img width="137" height="37" alt="Screenshot 2026-04-24 at 11 01 51"
src="https://github.com/user-attachments/assets/a870f1a3-83bd-4094-bc78-f9416fce1e0e"
/>

## After
<img width="139" height="31" alt="Screenshot 2026-04-24 at 11 00 14"
src="https://github.com/user-attachments/assets/1bff4411-aee2-4ad4-bae3-8118fa117368"
/>
2026-04-24 09:44:16 +00:00
WeikoandGitHub 9a9daf77ca Keep fallback record page layouts read-only in edition mode (#20023)
When a record has no associated `pageLayoutId`, the FE falls back to a
hardcoded default page layout (`DEFAULT_*_RECORD_PAGE_LAYOUT`). These
mocks use non-UUID ids for the layout, tabs, and widgets
(e.g. `default-person-page-layout`).

Today nothing distinguishes these fallbacks from real layouts at edit
time. Clicking "Edit layout" flips the global customization mode,
`PageLayoutRecordPageCustomizationSessionRegistrationEffect` registers
the mock id, and on Save `useSaveLayoutCustomization` fires
`UpdatePageLayoutWithTabsAndWidgets` with those mock ids — the BE
rejects them and we end up in a partial-save state (nav / command menu
commit while the page layout fails).

This PR keeps fallback record page layouts in read mode even when
global layout-edition mode is on, and defends the save loop so mock
ids can never be sent to the BE.

## Non editable "base" record page layout (=mock)
<img width="1508" height="616" alt="Screenshot 2026-04-24 at 10 35 02"
src="https://github.com/user-attachments/assets/34b093d1-f4bb-4076-ab8c-ce97ecb945a4"
/>

## Editable record page layout
<img width="1512" height="597" alt="Screenshot 2026-04-24 at 10 35 20"
src="https://github.com/user-attachments/assets/5ff6f3ff-79a5-4e6f-aa1c-01c7e09273b3"
/>
2026-04-24 08:54:37 +00:00
e8f58fd23a chore: sync AI model catalog from models.dev (#20018)
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-04-24 08:34:13 +02:00
d51d982a5f Fix: Database query on opportunity table (#20017)
## Automated fix for [bug
30463](https://sonarly.com/issue/30463?type=bug)

**Severity:** `critical`

### Summary
When createMany is called with upsert:true and records lack pre-assigned
IDs, findExistingRecords() executes a SELECT with no WHERE clause,
scanning the entire table. This caused an 11-second transaction on the
opportunity table (2.9s query + 7.7s JS processing).

### Root Cause
1. WHY was the transaction 11 seconds? Because a SELECT on the
opportunity table took 2.9s and its result processing took 7.7s.
2. WHY did the SELECT take 2.9s? Because it was a full table scan with
NO WHERE clause, fetching every record (including soft-deleted).
3. WHY was there no WHERE clause? Because findExistingRecords() in
common-create-many-query-runner.service.ts calls buildWhereConditions()
which returned an empty array, so no .orWhere() was applied to the query
builder before .getMany() was called.
4. WHY did buildWhereConditions return empty? Because the input records
had no pre-assigned IDs and the opportunity object has no other unique
fields, so there were no conflicting field values to build conditions
from.
5. WHY wasn't the empty-conditions case guarded? The
findExistingRecords() method was written without an early-return check
for empty whereConditions — it always executes the query regardless.

**Introduced by:** etiennejouan on 2025-10-15 in commit
[`4ae2999`](https://github.com/twentyhq/twenty/commit/4ae299973bcea3470252c4c68540d33a4df59cc7)
> Common Api - createOne/Many (#15083)

### Suggested Fix
Added an early return in findExistingRecords() when
buildWhereConditions() returns an empty array. When no WHERE conditions
exist (because input records lack values for any unique/conflicting
field), the method now returns an empty array immediately instead of
executing a SELECT with no WHERE clause that scans the entire table.
This prevents the O(n) full table scan + O(n) JS result processing that
caused the 11-second transaction. The downstream categorizeRecords()
correctly handles empty existingRecords by classifying all input records
as recordsToInsert.

---
*Generated by [Sonarly](https://sonarly.com)*

Co-authored-by: Sonarly Claude Code <claude-code@sonarly.com>
2026-04-24 06:18:57 +00:00
dbc350fafe i18n - translations (#20016)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-23 22:18:46 +02:00
085c0b9b7f feat(admin-panel): add read-only Billing tab and workspace logos (#20012)
## Summary
- Adds a **Billing** tab on the admin-panel workspace detail page that
surfaces Stripe customer + active subscription details (status, plan,
interval, current period, trial, cancellation, line items, credit
balance). Tab is gated on `IS_BILLING_ENABLED` both in the backend
service and in the frontend tab list — completely hidden on instances
where billing is disabled.
- Renders a **workspace avatar next to the name** in the admin Top
Workspaces list by plumbing the workspace `logo` field through the admin
DTO, statistics SQL query, and generated admin GraphQL types.
- **Read-only** by design: no Stripe API calls, no mutations — data
comes from the existing \`BillingCustomerEntity\` /
\`BillingSubscriptionEntity\` / \`BillingPriceEntity\` tables via
\`BillingSubscriptionService.getCurrentBillingSubscription\`.

### What the tab shows
- **Customer** container — Stripe customer ID (with link to the Stripe
dashboard, monospaced), credit balance (formatted, from
\`creditBalanceMicro\`).
- **Subscription** container — status tag (color-coded), plan tag,
billing interval, current period range, trial range (if trialing),
\`cancelAtPeriodEnd\` / \`cancelAt\` / \`canceledAt\` (only when set),
Stripe subscription ID (external link).
- **Line items** — one card per subscription item with product name,
product key tag, seats (if quantity), credits per period (for metered),
unit price (formatted with currency).

### Design choices
- Styling matches the user-facing billing page
(\`SubscriptionInfoContainer\` + \`Tag\` + \`H2Title\` + \`Section\`) —
no new UI primitives.
- Currency is rendered inline with amounts via \`Intl.NumberFormat\`
(e.g. \`\$19.00\`) instead of as a separate row.
- Uses the generated admin GraphQL types
(\`WorkspaceBillingAdminPanelQuery\`, \`SubscriptionStatus\`,
\`SubscriptionInterval\`) — no hand-typed response shapes.

## Test plan
- [x] \`npx nx typecheck twenty-server\` — passes
- [x] \`npx nx typecheck twenty-front\` — passes
- [x] oxlint + prettier on all touched files — clean
- [x] \`graphql:generate --configuration=admin\` — regenerated; new
\`workspaceBillingAdminPanel\` query + \`logo\` field on
\`AdminPanelTopWorkspace\` appear in \`generated-admin/graphql.ts\`
- [x] Backend GraphQL schema introspection shows
\`workspaceBillingAdminPanel\` query on \`/admin-panel\`
- [x] Direct GraphQL call with seeded \`BillingCustomer\` +
\`BillingSubscription\` + \`BillingPrice\` rows returns the expected
shape (\`status: "Trialing"\`, plan \`PRO\`, items with
quantity/unitAmount/includedCredits, trial period dates)
- [x] With \`IS_BILLING_ENABLED=false\` (default) the Billing tab is
hidden — verified in the admin panel UI
- [x] Top Workspaces list renders workspace avatars next to names —
verified in the admin panel UI
- [ ] Smoke test the Billing tab render in a real instance that has
\`IS_BILLING_ENABLED=true\` + live Stripe data (skipped locally due to
dev-env auth friction after toggling billing/multi-workspace; recommend
a reviewer check)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:12:05 +02:00
9ce9e2bc12 i18n - translations (#20015)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-23 22:05:44 +02:00
4f938aa097 feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.

## Summary

- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).

## What's in this PR

**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.

**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.

**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.

**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.

**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.

**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.

**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.

## Stats

- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean

## Behavior deltas

- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.

## Risks

- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.

## Test plan

- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.

## What's NOT in this PR (PR 2 scope)

- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:59:00 +02:00
ec98130def fix(admin-panel): inline skeleton loaders for table sections (#20014)
## Summary
\`SettingsSkeletonLoader\` wraps its content in \`PageHeader\` +
\`PageBody\`, which is right for **full-page replacement** (user detail,
config variable detail, etc.) but renders as a large empty stub with one
tiny floating bar when placed **inline inside a section that's already
scaffolded**. That's what showed up in Recent Users, Top Workspaces, and
the Chats tab in the admin panel — a jarring white page flash where a
few row placeholders should be.

This PR adds \`SettingsAdminSectionSkeletonLoader\` — a small,
configurable-row-count skeleton that uses the project's standard
\`SkeletonTheme\` pattern (\`theme.background.tertiary\` /
\`theme.background.transparent.lighter\` + \`borderRadius: 4\`, matching
\`PageContentSkeletonLoader\` and \`SettingsAdminTabSkeletonLoader\`)
and renders row-height bars that match \`TableRow\` spacing. Swaps it
into the three inline call sites.

Full-page usages of \`SettingsSkeletonLoader\` are unchanged.

### Changed
- **New**:
\`packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminSectionSkeletonLoader.tsx\`
- **Swapped** (3 inline loading states): \`SettingsAdminGeneral.tsx\`
(Recent Users + Top Workspaces), \`SettingsAdminWorkspaceDetail.tsx\`
(Chats tab)

## Test plan
- [x] typecheck (\`npx nx typecheck twenty-front --skip-nx-cache\`) —
clean
- [x] oxlint — 0 warnings
- [x] prettier — clean
- [ ] Visual: navigate to admin panel, observe Recent Users / Top
Workspaces / Chats tab loading — should see a tight stack of row-height
placeholder bars instead of a big empty page stub

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:58:33 +02:00
Paul RastoinandGitHub b1838b3090 Retrieve ai catalog at bootstrap (#20005)
# Introduction
Migrating from build injection to a runtime bootstrap injection of the
ai catalog that are dynamic to the env we're deploying to
2026-04-23 17:00:35 +00:00
Thomas TrompetteandGitHub fa3d0cd4a6 Force uuids in AI workflow tools (#20010)
- Add .uuid() Zod validation to all AI workflow tool schemas
(workflowVersionId, workflowId, stepId, edge target) so the AI model is
constrained to use valid UUIDs instead of human-readable strings like
step-find-stale-leads
- Fixes Sentry noise from Invalid UUID errors triggered when workflows
created with non-UUID step IDs are later edited via GraphQL mutations
that enforce UUID scalars

Will fix
https://twenty-v7.sentry.io/issues/7240446584/events/05c7782655a34f4a8f5fc889164026ab/?project=4507072499810304&referrer=previous-event
2026-04-23 16:27:53 +00:00
652930e0ac i18n - docs translations (#20009)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-23 17:07:23 +02:00
Thomas TrompetteandGitHub d887fdc532 Stop throwing for event stream does not exists (#20008)
Fixes
https://twenty-v7.sentry.io/issues/7351816489/?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

- When an SSE event stream already exists in Redis during a
reconnection, the server now checks if the caller is the rightful owner
(via isAuthorized) and destroys the stale stream before creating a fresh
one, instead of throwing an EVENT_STREAM_ALREADY_EXISTS error
- Unauthorized callers still receive the error, preserving the security
guard against hijacking
2026-04-23 13:22:09 +00:00
992a7ca12f i18n - docs translations (#20007)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-23 14:45:01 +02:00
Charles BochetandGitHub a445f4a6fa feat(sdk): add definePageLayoutTab for extending existing page layouts (#20004)
## Summary

Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.

This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.

```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';

export default definePageLayoutTab({
  universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
  pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
  title: 'Hello World',
  position: 1000,
  icon: 'IconWorld',
  layoutMode: PageLayoutTabLayoutMode.CANVAS,
  widgets: [/* ... */],
});
```

## Changes

- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
  - new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
  - dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
2026-04-23 12:00:10 +00:00
neo773andGitHub 20f7ba82d7 optimize workspace export command (#20000)
- Use `COPY` instead of `INSERT` for workspace tables, 40x faster
imports
- Multi value statements, 2x smaller file size
- Bump Batch size to 10K , remove COUNT query
- Add `formatPgCopyField` utility with unit tests
2026-04-23 09:42:49 +00:00
WeikoandGitHub aa4aea0f9b Fix layout edition mode dark mode text color (#19992)
Summary

Fixed the dark-mode regression in the layout customization banner at
packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx:

- The banner's background is themeCssVariables.color.blue
(theme-independent), so its text color must be theme-independent too.
- Replaced color: ${themeCssVariables.font.color.inverted} (which
resolves to near-black in dark mode) with color:
${GRAY_SCALE_LIGHT.gray1} (always white), matching the convention used
 in AnimatedButton.tsx for text on colored backgrounds.

## Before
<img width="1508" height="185" alt="Screenshot 2026-04-22 at 19 50 03"
src="https://github.com/user-attachments/assets/b0a95fc2-05d5-4207-9d72-64a83daf94ae"
/>

## After
<img width="1511" height="190" alt="Screenshot 2026-04-22 at 19 49 39"
src="https://github.com/user-attachments/assets/9ce33353-412f-4db2-9322-a6f293f6f1b4"
/>
2026-04-23 08:36:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77f5b9d7c8 chore(deps): bump @ai-sdk/google from 3.0.31 to 3.0.64 (#19998)
Bumps [@ai-sdk/google](https://github.com/vercel/ai) from 3.0.31 to
3.0.64.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/e2f82795704d7badbd90f9f4a798d8bf2d40b2df"><code>e2f8279</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14527">#14527</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/71c52e0c053571dd6893be6fa56d275ec2d9e216"><code>71c52e0</code></a>
Backport: chore(provider/google): update available models (<a
href="https://redirect.github.com/vercel/ai/issues/14524">#14524</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e662efda9bb35838d5c2a03a6320197d1192db4c"><code>e662efd</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14510">#14510</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/83434a97d6856c34d440e22038ac083cd5134017"><code>83434a9</code></a>
Backport: feat (provider/gateway): add provider routing sort options (<a
href="https://redirect.github.com/vercel/ai/issues/14508">#14508</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/3aa3a68cfec59cae4dccb1ad1446d30fbe93f17c"><code>3aa3a68</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14507">#14507</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/a27a631939dedc310b13fd48de144760cc2a3145"><code>a27a631</code></a>
Backport: feat (provider/gateway): make model list resilient to unknown
model...</li>
<li><a
href="https://github.com/vercel/ai/commit/a2d619a76be80c4fdd60ce047d21fdf95879e08f"><code>a2d619a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14502">#14502</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/b937f3e7fa0c943c6ce8a379b57dd5bb7abf07b4"><code>b937f3e</code></a>
Backport: fix(xai): support encrypted reasoning round-trip for ZDR (<a
href="https://redirect.github.com/vercel/ai/issues/14497">#14497</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/b0e5ab3a75052fbc0469066b19196ec419e49c20"><code>b0e5ab3</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14494">#14494</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/8c4abafaaa28f82f031a498ff335c9f394a33d48"><code>8c4abaf</code></a>
Backport: chore(provider/gateway): update gateway model settings files
v6 (<a
href="https://redirect.github.com/vercel/ai/issues/1">#1</a>...</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/google@3.0.31...@ai-sdk/google@3.0.64">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/google&package-manager=npm_and_yarn&previous-version=3.0.31&new-version=3.0.64)](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-04-23 07:37:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
80e8f6d516 chore(deps): bump @blocknote/server-util from 0.47.1 to 0.47.3 (#19997)
Bumps
[@blocknote/server-util](https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util)
from 0.47.1 to 0.47.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/releases"><code>@​blocknote/server-util</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v0.47.3</h2>
<h2>0.47.3 (2026-03-25)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>core:</strong> preserve whitespace edge cases but collapse
html formatting newlines (BLO-1065) (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2551">#2551</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2230">#2230</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Yousef</li>
</ul>
<h2>v0.47.2</h2>
<h2>0.47.2 (2026-03-20)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>use <code>&lt;details&gt;</code> &amp; <code>&lt;summary&gt;</code>
for toggle block HTML export (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2524">#2524</a>)</li>
<li>remove <code>@hocuspocus/provider</code> peer dependency by inlining
tiptap comment types BLO-1064 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2564">#2564</a>)</li>
<li><strong>core:</strong> slash menu fails in custom blocks after space
BLO-1036 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2553">#2553</a>)</li>
<li><strong>i18n:</strong> fix typo in russian translation (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2560">#2560</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Drone</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/blob/main/CHANGELOG.md"><code>@​blocknote/server-util</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>0.47.3 (2026-03-25)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>core:</strong> preserve whitespace edge cases but collapse
html formatting newlines (BLO-1065) (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2551">#2551</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2230">#2230</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Yousef</li>
</ul>
<h2>0.47.2 (2026-03-20)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>use <!-- raw HTML omitted -->/<!-- raw HTML omitted --> for toggle
block HTML export (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2524">#2524</a>)</li>
<li>remove <code>@​hocuspocus/provider</code> peer dependency by
inlining tiptap comment types BLO-1064 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2564">#2564</a>)</li>
<li><strong>core:</strong> slash menu fails in custom blocks after space
BLO-1036 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2553">#2553</a>)</li>
<li><strong>i18n:</strong> fix typo in russian translation (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2560">#2560</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Claude Opus 4.6</li>
<li>Drone</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/cd92dc21be49397b658fef4e308e01ce8f5c04ad"><code>cd92dc2</code></a>
chore(release): publish 0.47.3</li>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/b63b4096daa575f821980ef897fd90f4c76d9e42"><code>b63b409</code></a>
chore(release): publish 0.47.2</li>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/d76fd68e016da698f3896f9d349a935a26a52d5f"><code>d76fd68</code></a>
test: get snapshots working again (<a
href="https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util/issues/2554">#2554</a>)</li>
<li>See full diff in <a
href="https://github.com/TypeCellOS/BlockNote/commits/v0.47.3/packages/server-util">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@blocknote/server-util&package-manager=npm_and_yarn&previous-version=0.47.1&new-version=0.47.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: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-23 07:29:45 +00:00
39831338f7 i18n - translations (#19988)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 18:46:31 +02:00
WeikoandGitHub 876214bc1d scaffold record page layout + fields view when adding an object (#19977)
## Summary

Extends `yarn twenty add` → **Object** so it scaffolds a complete record
page out
of the box:

- A **record-page-fields view** (`<name>-record-page-fields.ts`,
FIELDS_WIDGET)
pre-populated with the `name` field plus the auto-generated default
fields (`createdAt`,
`updatedAt`, `createdBy`, `updatedBy`) — the default-field entries are
emitted as
`generateDefaultFieldUniversalIdentifier({ objectUniversalIdentifier,
fieldName: '...' })`
calls rather than pre-computed UUIDs, so the generated file
double-serves as
documentation for the public util.
- A **record page layout** (`<name>-record-page-layout.ts`) with a Home
tab whose Fields
widget points at the new view (via `viewUniversalIdentifier`), plus a
Timeline tab.
- The companion prompt now covers all three artefacts (was view + nav
menu item).

Fix: Server-side, renames `viewId` → `viewUniversalIdentifier` on the
universal-flat FIELDS
widget configuration so it is consistent with other universal-flat
references. The DB-side
DTO keeps `viewId` (now typed as `SerializedRelation`), and the
conversion utils map
between the two.

<img width="337" height="349" alt="Screenshot 2026-04-22 at 15 40 22"
src="https://github.com/user-attachments/assets/59e36540-1761-46b0-808d-648c68604268"
/>
2026-04-22 16:30:45 +00:00
Raphaël BosiandGitHub 0d996a5629 Resend app improvements (#19986)
## Summary
Major overhaul of the `twenty-for-twenty` Resend app to make sync more
reliable, observable, and feature-complete.
### SDK upgrade
- Bumps `twenty-sdk` to `2.0.0` and `twenty-client-sdk` to
`1.23.0-canary.1`
- Pins React back to `^18.2.0` to match the SDK
### Sync engine rewrite
- Splits the single `sync-resend-data` job into 4 staggered cron-driven
logic functions: **Emails**, **Contacts**, **Broadcasts (+ segments +
dependencies)**, **Templates** — each running every 5 minutes on a
different minute offset with per-slot timeouts
- Adds a new `ResendSyncCursor` object + `with-sync-cursor`
orchestration so each step persists its progress, last run timestamp,
and last run status
- Introduces an `INITIAL_SYNC_MODE` app variable +
`resend-initial-sync-mode-monitor` that flips to intermediate sync once
every cursor is empty (intermediate sync only refetches the last 7 days
of emails)
- Stops auto-creating People from Resend contacts; instead backfills
`personId` on Resend contacts/emails by matching existing People by
email
- Renames `on-*-deleted` handlers to `on-*-destroyed` and removes from
Resend on destroy (not soft delete)
- Adds rate-limit retry, paginated `for-each-page`, typed-client, and
existing-IDs lookup helpers

### New objects & fields
- New `ResendTopic` object with relation to `ResendBroadcast` (+
navigation menu item, view, page layout)
- New `ResendSyncCursor` object (step / cursor / last run at / last run
status)
- Adds `html` and `text` fields on `ResendBroadcast`; removes raw
`htmlBody`/`textBody`/`tags` from `ResendEmail`

### New UI
- **Sync Status standalone page** (`ResendSyncStatus` front component +
nav item) showing live cursor / last run state per step
- **Person Resend Email Stats** front component: deliverability rate +
per-status breakdown with progress bars
- **Email Broadcast HTML viewer** front component renders an individual
email against its parent broadcast's HTML; new dedicated **Broadcast
HTML viewer**
- Adds Resend Broadcast record page layout (Home / Preview / Timeline /
Tasks / Notes / Files tabs)
### Tests
- ~25 new unit / integration test files covering sync utilities, cursor
lifecycle, webhook handler, email-stats computation, sync-status page
resolution, and rate-limit retry
- Replaces legacy `fetch-all-paginated` tests with `for-each-page` tests
2026-04-22 18:17:27 +02:00
Charles BochetandGitHub 3ebeb3a3e8 feat(community): add github-connector example app (#19961)
## Summary

Adds a new community app at
`packages/twenty-apps/community/github-connector` that demonstrates a
complete, production-style GitHub integration built on the Twenty SDK.

It is extracted (and decoupled) from the internal `twenty-eng` workspace
so external developers can use it as a reference for their own
connectors.

What it ships:

- **Six synced objects**: `pullRequest`, `pullRequestReview`,
`pullRequestReviewEvent`, `issue`, `projectItem`, `engineer`
- **Logic functions** for periodic backfills (PRs, reviews, issues,
project items, contributors) and a single signed-webhook route trigger
(`POST /github/webhook`) that performs idempotent upserts for
`pull_request`, `pull_request_review`, `issues`, and `projects_v2_item`
events
- **Views, navigation menu items and a GitHub folder** so the data is
discoverable in the UI out of the box
- **Configurable repos / project numbers** via `GITHUB_REPOS` and
`GITHUB_PROJECT_NUMBERS` application variables — no hardcoded org

## Authentication

Two interchangeable modes (PAT preferred for quick setup, GitHub App
recommended for production):

1. **Personal Access Token** — set `GITHUB_TOKEN`. Used as-is for both
REST and GraphQL.
2. **GitHub App** — set `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`,
`GITHUB_APP_INSTALLATION_ID`. Issues a signed JWT, exchanges it for a
short-lived installation token, and caches the token until expiry.

Webhook signature verification (`X-Hub-Signature-256`) is enforced when
`GITHUB_WEBHOOK_SECRET` is set.

## Notes

- Built on `twenty-sdk@2.0.0` / `twenty-client-sdk@2.0.0`
- Decoupled from internal modules (`quality/bug`, `discord`, `release`,
`code-build`, `project-management`) — `mustBeQa` is inlined and a local
`github` nav folder replaces shared ones
- `npx twenty typecheck`, `yarn lint`, and `npx twenty build` all run
cleanly
- Includes a comprehensive README with setup, env vars, webhook
configuration, and the auth resolution flow
2026-04-22 18:17:08 +02:00
f30ef2432f i18n - translations (#19987)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 17:55:26 +02:00
Paul RastoinandGitHub 921a0f01c8 Forbid permissions update cross app role retarget (#19982)
closes https://github.com/twentyhq/twenty/issues/19807
2026-04-22 15:39:26 +00:00
WeikoandGitHub 0696290af4 fix(page-layout): hide deactivated fields from FIELDS widget and layout editor (#19984)
## Context

- The FIELDS widget resolved every viewField against
objectMetadataItem.fields, which
includes deactivated field metadata — so fields deactivated after being
added to a view
kept rendering on records.
- Same leak existed in the layout editor: deactivated fields appeared as
toggleable hidden
viewFields, and newly-deactivated object fields were auto-proposed via
the "missing
fields" flow.

## Fix
- pre-filter objectMetadataItem.fields to field.isActive at each entry
point
(useFieldsWidgetGroups, useFieldsWidgetEditorGroupsData,
useFieldsWidgetHiddenFields) and
inside buildDefaultFieldsWidgetGroups for the no-view fallback.
2026-04-22 15:28:29 +00:00
EtienneGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Etienne
32e0425a65 Docs - Update getting started (#19976)
- Add product tour video
- Add new graphic design assets

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
2026-04-22 14:52:52 +00:00
f34ba6ac12 i18n - docs translations (#19983)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 16:50:26 +02:00
Paul RastoinandGitHub f0a625c3f8 Cleanup application and app registration test util (#19981)
## Introduction
Centralizing integ test app cleanup
Role will be deleted by the app uninstall if exists
2026-04-22 14:39:58 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
b010599000 fix(server): preserve kanban/calendar fields in view manifest sync (#19946)
## Summary

The `fromViewManifestToUniversalFlatView` converter hardcoded five view
fields to `null` instead of reading them from the manifest:
- `mainGroupByFieldMetadataUniversalIdentifier`
- `kanbanAggregateOperation`
- `kanbanAggregateOperationFieldMetadataUniversalIdentifier`
- `calendarLayout`
- `calendarFieldMetadataUniversalIdentifier`

As a result **any** Kanban view in an app manifest is rejected by
`validateFlatViewCreation` with `"Kanban view must have a main group by
field"`, and any Calendar view would trip the `view.entity.ts` check
constraint requiring `calendarLayout` + `calendarFieldMetadataId` to be
non-null. Discovered while trying to install
[`twenty-crm-meeting-baas`](https://github.com/Meeting-BaaS/twenty-crm-meeting-baas)
which ships a Kanban view.

## Changes

- **Server converter**: read all five fields from the manifest (with `??
null` fallback).
- **`ViewManifest` type** (`twenty-shared`): add the five fields so SDK
users can set them type-safely.
- **Move `ViewCalendarLayout`** from `twenty-server` to `twenty-shared`
so the manifest type can reference it. Seven import sites updated; the
front-end imports via generated GraphQL types and is unaffected.
- **Unit tests**: extend
`from-view-manifest-to-universal-flat-view.util.spec.ts` with
preservation + null-default cases for both Kanban and Calendar (5 tests
total).
- **Regression coverage**: add a Kanban view
(`post-cards-by-status.view.ts`) to the `rich-app` fixture grouped by
the existing `status` SELECT field. The existing
`applications-install-delete-reinstall` e2e test now exercises the
Kanban path end-to-end — a future regression here would fail CI.

Note: `expected-manifest.ts` and the `views.length` assertion in
`manifest.tests.ts` were updated to reflect the new fixture view.

## Test plan

- [x] `nx test twenty-server --
from-view-manifest-to-universal-flat-view` → 5/5 pass
- [x] `nx typecheck twenty-shared` / `twenty-sdk` / `twenty-server` → no
new errors (one pre-existing unrelated error in
`admin-panel.module-factory.ts`)
- [x] `nx lint twenty-shared` / `twenty-sdk` → clean
- [x] Manual install of the Meeting BaaS app on a dev workspace succeeds
with the Kanban view after this fix
- [ ] CI: SDK e2e `applications-install-delete-reinstall` passes against
the new fixture view
- [ ] CI: integration test `calendar-field-deactivation-deletes-views`
still passes after the enum move

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-22 16:31:00 +02:00
Félix MalfaitandGitHub c2cf3eac50 feat(sdk): confirm authentication method on remote add (#19947)
## Summary

`yarn twenty remote add` only prints `✓ Default remote set to X.` after
authenticating. When using the OAuth path, the browser flow happens
silently — there's no line that says _"you authenticated"_ — so users
(including me this morning while installing a Twenty app) are left
wondering whether auth actually completed and which method was used.

This PR adds explicit confirmation of the auth step:

**New remote via OAuth**
```
✓ Remote "myremote" added (https://app.twenty.com) via OAuth.
✓ Default remote set to "myremote".
```

**New remote via API key**
```
✓ Remote "myremote" added (https://app.twenty.com) via API key.
✓ Default remote set to "myremote".
```

**Re-authenticating an existing remote**
```
✓ Re-authenticated "myremote" via OAuth.
✓ Default remote set to "myremote".
```

## Implementation

- `authenticate()` now returns the method actually used (`'OAuth' | 'API
key'`) instead of `void`. This correctly surfaces OAuth → API-key
fallback: if OAuth fails and the user drops into the API-key prompt, the
success line reflects that.
- New-remote and re-auth paths print distinct messages so the user can
tell which path they took.
- No new API calls — method name comes from which branch of
`authenticate()` succeeded.

## Test plan

- [x] `nx typecheck twenty-sdk` — clean
- [x] `nx lint twenty-sdk` — clean
- [ ] Manual smoke test: `yarn twenty remote add --as test --api-url
...` via OAuth, API key, and re-auth

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-22 13:18:25 +00:00
789f8aba5d i18n - translations (#19975)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 15:23:41 +02:00
68d509e98d Update settings application illustrations and app metadata previews (#19964)
## Summary
- Refresh the settings application visuals with new light/dark PNG
covers for the data model card
- Replace the custom and standard application carousel assets with the
new provided illustrations
- Align app chips, type tags, and application detail previews with the
updated icon and description treatment
- Keep the data model cover container and overlay button behavior intact
while swapping the underlying imagery

## Testing
- Not run (not requested)
- Existing frontend typecheck and formatting checks were exercised
during implementation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-22 13:07:41 +00:00
0c929e7903 refactor(tool-provider): rename web_search to exa_web_search, drop XOR toggle (#19969)
## Summary

- Today `WEB_SEARCH_PREFER_NATIVE` forces a **mutual exclusion**: either
the custom Exa tool preloads as `web_search` or the SDK-native
`web_search` binds. Same name, different backends.
- This PR lets them **coexist**. Custom Exa becomes `exa_web_search`;
native keeps `web_search`. The model picks based on tool descriptions.
- `WEB_SEARCH_PREFER_NATIVE` and `shouldUseNativeSearch()` are deleted.
Exa enablement follows `WEB_SEARCH_DRIVER` (existing). Native enablement
follows the agent's `modelConfiguration.webSearch.enabled` (existing).

## Key changes

**Config / service**
- Deleted `WEB_SEARCH_PREFER_NATIVE` (config-variables.ts)
- Deleted `WebSearchService.shouldUseNativeSearch()`
- `WebSearchService.isEnabled()` unchanged — still gates Exa
availability

**Custom tool rename**
- `ActionToolProvider.toolMap`: `'web_search'` → `'exa_web_search'`
- Descriptor name matches
- `WebSearchTool.description` rewritten to position Exa as
structured/entity-aware, complementary to native

**Native tool binder**
- `NativeToolBinder.bind()` drops the `shouldUseNativeSearch` gate.
Per-agent `modelConfiguration.webSearch.enabled` (inside
`getNativeModelTools`) stays authoritative.

**Chat**
- Preload list now always includes `exa_web_search` —
`ActionToolProvider` silently skips the descriptor when Exa is disabled,
so `getToolsByName` degrades gracefully
- Native tools always attempted; returns empty ToolSet when the model
doesn't support them
- `directTools = { ...preloadedTools, ...nativeSearchTools }` — both
present when both enabled
- `billNativeWebSearchUsage` called unconditionally (the function
already short-circuits on count ≤ 0)

**Workflow agent**
- Same unconditional billing pattern
- `WebSearchService` dependency removed

**System prompt**
- Dropped the special-cased `web_search` branch. Preloaded tools list
uniformly now.

**Frontend**
- `exa_web_search` reuses the same "Searching the web for X" display as
native
- Test coverage added

## Billing isolation (verified)

- `countNativeWebSearchCallsFromSteps` counts `toolName ===
'web_search'` only. After the rename, only native calls match. Exa calls
(`exa_web_search`) are billed separately via
`WebSearchService.emitUsageEvent` inside `search()`.
- No double-billing path.

## Behavior deltas (intended)

| Scenario | Before | After |
|---|---|---|
| Anthropic model + Exa enabled + PREFER_NATIVE=true | native only |
**both** |
| Anthropic + Exa enabled + PREFER_NATIVE=false | Exa only (as
`web_search`) | **both** |
| Non-native model + Exa enabled | Exa as `web_search` | Exa as
`exa_web_search` |
| Any model + Exa disabled + native supported | native only | native
only |
| Workflow agent with `webSearch.enabled=true` + Anthropic + Exa enabled
| native only | **both** |

## Known regression (accepted)

Customers who set `WEB_SEARCH_PREFER_NATIVE=false` to force Exa-only
will now **also** see native `web_search` if the model supports it.
There's no chat-level kill switch after this PR. Per discussion, this is
accepted — future model-level capability gating (in the model JSON) will
be the right place for that control.

## Stats

- 10 files, +63 / −73 (net deletion)
- Typecheck clean (server: 7 pre-existing unrelated, front: 13
pre-existing unrelated — zero new either side)
- Prettier clean

## Test plan

- [ ] `npx nx typecheck twenty-server` and `npx nx typecheck
twenty-front` pass
- [ ] With Anthropic + Exa enabled: chat shows both `web_search` and
`exa_web_search` in preloaded list; model can call either
- [ ] With Anthropic + Exa disabled: chat shows only native `web_search`
- [ ] With non-native model + Exa enabled: chat shows only
`exa_web_search`
- [ ] Workflow agent with `modelConfiguration.webSearch.enabled=true` +
Exa enabled: both available
- [ ] Billing: native calls billed via `billNativeWebSearchUsage`; Exa
calls billed via `WebSearchService.emitUsageEvent`; no double-billing
- [ ] Frontend: `exa_web_search` renders "Searching the web for X" the
same as `web_search`

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:57:44 +02:00
f018f17133 i18n - docs translations (#19970)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 14:44:57 +02:00
Yash RajandGitHub a486ead39d fix: restore Try Twenty button text visibility on docs navbar (#19968)
# [Docs]: Fix blank Try Twenty button text in dark mode

## 🐛 Problem
The "Try Twenty" CTA button in the docs navbar appears blank/invisible
when users enable dark mode, making it impossible to click through to
the sign-up page.

**Issue:** #19965

##  Root Cause
CSS specificity conflict in `packages/twenty-docs/custom.css`:
- The dark mode CSS rule targets only the `<a>` element
- Mintlify renders button text in a nested `<span>` with class
`text-white` (white color)
- The `text-white` Tailwind utility directly applies `color: rgb(255 255
255)` to the span
- This overrides the inherited dark color from the parent `<a>` element
- **Result:** White text on white background = invisible button

##  Solution
Update the CSS selector in `packages/twenty-docs/custom.css` to target
both the `<a>` element AND the nested `<span>`:

**Before:**
```css
:is(.dark, [data-theme="dark"]) #topbar-cta-button a {
  background-color: #ffffff !important;
  color: #141414 !important;
}
2026-04-22 13:59:42 +02:00
44309a6fd9 refactor(tool-provider): rename NativeModelToolProvider to NativeToolBinderService (#19966)
**Stacked on top of #19962.**

## Summary

- `NativeModelToolProvider` lived under `providers/` and had the
`*-tool.provider.ts` suffix, but it never implemented `ToolProvider`,
wasn't in `TOOL_PROVIDERS`, had no descriptors, and wasn't executed by
`ToolExecutorService`. The shape misled readers.
- It's actually a **parallel concept**: a binder that produces
SDK-native tool objects (Anthropic `webSearch`, OpenAI `webSearch`,
etc.) which the AI SDK passes straight to the model. Opaque, not
serializable, never in the catalog, never dispatched by the executor.
- This PR renames + moves it to reflect that.

## Renames

| Before | After |
|---|---|
| `NativeModelToolProvider` (class) | `NativeToolBinderService` |
| `NativeToolProvider` (interface) | `NativeToolBinder` |
| `generateTools(context)` (method) | `bind(context)` |
| `providers/native-model-tool.provider.ts` |
`native/native-tool-binder.service.ts` |
| `interfaces/native-tool-provider.interface.ts` |
`native/native-tool-binder.interface.ts` |

## What doesn't change

- `ToolCategory.NATIVE_MODEL` enum stays (still used by
`getToolsByCategories`).
- `isAvailable()` signature unchanged.
- `WebSearchService.shouldUseNativeSearch()` toggle untouched — that's
product-level and belongs to a separate PR that handles the Exa
coexistence story.
- No behavior change. Pure rename + move.

## Why this matters for the broader architecture

This rename makes the native/binder concept **visible in the type system
and directory structure**. That's what later enables coexisting native +
custom tools (e.g., `web_search` native alongside `exa_web_search`
custom) without the current naming collision, because native tools are
no longer masquerading as a registry provider.

## Stats

- 5 files, +30 / −28.
- Blast radius: 4 files modified, 1 file renamed (git tracks as rename).
- Typecheck clean (7 pre-existing unrelated errors, zero new).
- Prettier clean.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes
- [ ] AI chat: native `web_search` still works end-to-end when enabled
- [ ] Workflow AI agent: `ToolCategory.NATIVE_MODEL` still works (goes
through `bind()` now)
- [ ] MCP: unaffected (doesn't use native tools)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:43:30 +02:00
2a5d5b36db refactor(tool-provider): kill execute_tool's dual dispatch (#19962)
**Stacked on top of #19960.**

## Summary

- `execute_tool` used to check `directTools[toolName]` first, falling
back to the registry. Same tool name, different wrapping: preloaded went
through `wrapToolsWithOutputSerialization`, fallback didn't. Silent
divergence — a model calling a CRUD tool via
\`learn_tools\`/\`execute_tool\` got raw output, while calling it as a
preloaded direct tool got compacted output.
- Now: `execute_tool` always routes through
`toolRegistry.resolveAndExecute`. One path, no fast-path.
- Output serialization (`compactToolOutput`) moves into the registry,
gated by a new `serializeOutput` flag on `hydrateToolSet` /
`resolveAndExecute` / `getToolsByName` / `getToolsByCategories` /
`ToolRetrievalOptions`. Chat passes `true`, MCP and workflow pass
`false`.

## Key changes

**Registry (`tool-registry.service.ts`)**
- `hydrateToolSet` options gain `serializeOutput?: boolean`; when true
the execute closure wraps dispatch result with `compactToolOutput`.
- `resolveAndExecute` signature: replaces unused \`_options:
ToolExecutionOptions\` with `{ serializeOutput?: boolean }`.
- `getToolsByName` and `getToolsByCategories` thread `serializeOutput`
through to `hydrateToolSet`.

**Meta-tool (`execute-tool.tool.ts`)**
- API changes from positional `(toolRegistry, context, directTools?,
excludeTools?)` to `(toolRegistry, context, options?: { excludeTools?,
serializeOutput? })`.
- `directTools` fallback removed. All invocations go to the registry.

**Chat (`chat-execution.service.ts`)**
- Passes `serializeOutput: true` to `getToolsByName` — preloaded tools
get compacted output from the hydrator, no external wrap needed.
- Drops the external `wrapToolsWithOutputSerialization(preloadedTools)`
call.
- `createExecuteToolTool` call now passes `{ serializeOutput: true }`.
Direct-tool and `execute_tool` paths produce identical output shape.

**MCP (`mcp-protocol.service.ts`)**
- `createExecuteToolTool` call updated to new options shape with `{
excludeTools: MCP_EXCLUDED_TOOLS }`. No `serializeOutput` flag → raw
output as today.

**Deletes**
- `output-serialization/wrap-tools-with-output-serialization.util.ts` —
sole caller removed.

## Behavior changes

- **Chat, `execute_tool` fallback path**: now produces compacted output
(matches direct path). Net effect: fewer tokens for CRUD results reached
via discovery. Intended improvement.
- **Chat, `execute_tool({toolName: 'web_search'})` edge**: today
silently hits the native tool via `directTools`; now returns \"tool not
found, use get_tool_catalog\". Self-correcting, rare — native tools are
always directly available to the model.
- **MCP**: no change. No `serializeOutput` flag → identical raw output.
- **Workflow agent**: no change. Doesn't use `execute_tool`.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes (verified: 7 pre-existing
unrelated errors, zero new)
- [ ] \`npx jest
packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts\`
passes in CI
- [ ] AI chat: call a preloaded tool (e.g. \`search_help_center\`)
directly → compacted output
- [ ] AI chat: call a non-preloaded CRUD tool via
\`learn_tools\`/\`execute_tool\` → compacted output (this is the
behavior change)
- [ ] AI chat: native \`web_search\` still works when model calls it
directly
- [ ] MCP: \`tools/call\` on a registry tool → raw output (nulls
preserved)
- [ ] Workflow AI agent: tool dispatch unchanged

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:49:35 +02:00
66a68d8e1c i18n - docs translations (#19967)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 12:47:15 +02:00
b77c44fd20 refactor(tool-provider): dedupe descriptor/generator paths (#19960)
## Summary

- Every tool provider used to implement `generateDescriptors()` **and**
register a category generator at `onModuleInit()` that re-ran the same
factories at execute time. `ToolExecutorService` carried two registries
(`staticToolHandlers`, `categoryGenerators`) to route between them.
- Providers now own execution of their own tools via a new
`executeStaticTool()` method. `ToolExecutorService` drops both maps and
delegates by `descriptor.category`. Each factory-backed provider has a
single `buildToolSet()` used by both descriptor generation and
execution.
- Extracts `resolveObjectIcon` shared util (was duplicated verbatim in
workflow + dashboard providers), and deletes the orphaned
`ToolGeneratorModule` whose consumers were removed in the earlier AI
chat simplification refactor.

No behavior change. Same factories run, same permission checks, same
tools execute. Net diff: 18 files, +311 / −480.

## Key changes

- `ToolProvider` interface gains `executeStaticTool(name, args,
context)`.
- `ToolExecutorService` loses its `staticToolHandlers` and
`categoryGenerators` maps, injects `TOOL_PROVIDERS`, and does
`providers.find(p => p.category ===
descriptor.category).executeStaticTool(...)` for `kind: 'static'`
descriptors.
- `ActionToolProvider` drops the register-handler loop in its
constructor; `executeStaticTool` looks up in the existing `toolMap`.
- `View`, `Metadata`, `Workflow`, `Dashboard`, `ViewField` providers
each have a single `buildToolSet(context)` private method used by both
`generateDescriptors` and `executeStaticTool`. No more `onModuleInit`,
no `ToolExecutorService` dependency.
- `DatabaseToolProvider` and `LogicFunctionToolProvider` implement
`executeStaticTool` with an invariant-violation throw — they only emit
`database_crud` / `logic_function` kinds, so the static-tool path is
unreachable for them.
- Deletes `tool-generator/` (dead code — zero consumers).

## Dependency graph before/after

**Before:** provider → `ToolExecutorService` (for `register*` calls)
**After:** `ToolExecutorService` → `TOOL_PROVIDERS` → providers.
Cleaner, no cycle.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes (verified: same 7
pre-existing unrelated errors)
- [ ] `npx nx lint twenty-server` passes
- [ ] AI chat: trigger a tool call that hits `execute_tool` fallback
(e.g. a view/metadata tool not in the preloaded set) — verify it still
executes
- [ ] AI chat: trigger a preloaded action tool (e.g.
`search_help_center`) — verify it still executes
- [ ] MCP: `tools/list` and `tools/call` for both preloaded and
catalog-discovered tools
- [ ] Workflow AI agent: run a workflow with AI agent step that calls
DATABASE_CRUD tools
- [ ] Verify the `web_search` / `code_interpreter` tools (if enabled)
still dispatch correctly

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:32:03 +02:00
Félix MalfaitandGitHub 3d164640c8 Remove Product Hunt banner section (#19959)
Removed Product Hunt banner from README.
2026-04-22 10:58:32 +02:00
e184732af4 i18n - docs translations (#19958)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 10:55:06 +02:00
710e3e2dfe i18n - docs translations (#19957)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 08:59:13 +02:00
40b1cdaba6 i18n - docs translations (#19956)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 07:15:43 +02:00
800d0f28ff i18n - docs translations (#19955)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 05:24:07 +02:00
11fd3bd6ee i18n - docs translations (#19954)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 02:59:51 +02:00
b77c65946f i18n - docs translations (#19952)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-22 00:27:38 +02:00
2a6fed5f5f i18n - docs translations (#19948)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 22:33:35 +02:00
92c8965dd3 i18n - docs translations (#19944)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 20:43:22 +02:00
Abdul RahmanandGitHub f19b3bfd38 Fix left/right arrow keys not working in dropdown search inputs (#19759)
Closes #12847 

Implements a two-mode focus management pattern for `SelectableList`
components with search inputs, resolving the conflict between text input
cursor movement and grid/list navigation.

### How it works

**Input mode** (search input focused):
- Left/right arrow keys move the text cursor normally
(`enableOnFormTags: false` on ArrowLeft/ArrowRight hotkeys)
- Up/down arrow keys blur the input and transfer focus to the grid,
entering grid mode

**Grid mode** (search input blurred):
- All arrow keys navigate the selectable list grid
- Pressing up arrow from the top row clears the grid selection and
refocuses the search input, returning to input mode
- Typing any printable character refocuses the search input (wildcard
hotkey with `enableOnFormTags: false`)

### Demo 


https://github.com/user-attachments/assets/825ad603-a5f8-4863-8269-3ecf35965847


https://github.com/user-attachments/assets/9d07346d-18a0-40fa-8874-21040c11f03d
2026-04-21 18:25:44 +00:00
0aaffe3212 i18n - docs translations (#19943)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 18:44:52 +02:00
Paul RastoinandGitHub 66857ca77b Remove cross version upgrade placeholder (#19940)
Moving to somewhere else to leverage docker cache
2026-04-21 16:16:47 +00:00
WeikoandGitHub 76582dc04a Export generateDefaultFieldUniversalIdentifier from SDK (#19937)
## Context
- Moved generateDefaultFieldUniversalIdentifier from the internal
cli/utilities/build/manifest/utils/ folder to sdk/define/objects/ and
re-exported it from
the public twenty-sdk/define entry point, so app authors can compute the
deterministic UID
of a default field (id, name, createdAt, …) from their own code.
- Narrowed the signature from { objectConfig: ObjectConfig; fieldName }
to {
objectUniversalIdentifier: string; fieldName: string } — the only thing
the helper needs,
and what app code has on hand.
- Updated the two internal callers and the spec to the new import path
and signature.

```typescript
import {
  defineView,
  generateDefaultFieldUniversalIdentifier,
} from "twenty-sdk/define";

export default defineView({
  universalIdentifier: "70f10d44-144a-4da8-8c6f-3ec2422138c0",
  name: "all-thing",
  objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
  icon: "IconList",
  position: 0,
  fields: [
    {
      universalIdentifier: "75a90bc4-d901-4df4-85e0-af29db5e0104",
      fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier(
        {
          objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
          fieldName: "createdAt",
        },
      ),
      position: 0,
      isVisible: true,
      size: 200,
    },
    {
      universalIdentifier: "75a90bc5-d901-4df4-85e0-af29db5e0105",
      fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier(
        {
          objectUniversalIdentifier: "c782b61c-70fd-4c88-9cd6-4e61ab8d7591",
          fieldName: "createdBy",
        },
      ),
      position: 0,
      isVisible: true,
      size: 200,
    },
  ],
});
```
2026-04-21 16:07:09 +00:00
neo773andGitHub a2099d22b5 Optimize website images (#19933)
AVIF has broad support in 2026 we should leverage it for smaller image
size


<img width="1496" height="847" alt="Screenshot 2026-04-21 at 5 49 58 PM"
src="https://github.com/user-attachments/assets/8366a2c7-f72b-4cff-891e-78a372d9a84b"
/>

```
     /halftone/materials/glass/environment.jpg                                    41KB       31KB      23%
     /illustrations/generated/handshake.png                                       61KB       49KB      19%
     /illustrations/generated/home-background-bridge.png                          19KB       13KB      27%
     /illustrations/generated/mic.png                                             47KB       32KB      32%
     /illustrations/generated/milestone.jpg                                      101KB       89KB      12%
     /illustrations/generated/partner-meeting.webp                                78KB       37KB      52%
     /images/case-studies/header-pattern.png                                     250KB      165KB      33%
     /images/home/hero/background.webp                                           101KB       73KB      26%
     /images/home/hero/foreground.webp                                            60KB       37KB      38%
     /images/home/hero/sales-dashboard/distribution.webp                          11KB        7KB      29%
     /images/home/hero/sales-dashboard/revenue.webp                               15KB        9KB      37%
     /images/home/hero/sales-dashboard/visits.webp                                14KB       11KB      21%
     /images/home/hero/twenty-demo-logo.webp                                       0KB        0KB     -99%
     /images/home/logo-bar/bayer.webp                                             52KB       37KB      29%
     /images/home/logo-bar/french-republic.webp                                    6KB        6KB       5%
     /images/home/logo-bar/nic.webp                                                4KB        4KB      -3%
     /images/home/logo-bar/pwc.webp                                                3KB        2KB      10%
     /images/home/logo-bar/shiawase-home.webp                                      7KB        8KB      -2%
     /images/home/logo-bar/windmill-logo.png                                       2KB        3KB     -17%
     /images/home/logo-bar/windmill-original.webp                                  2KB        2KB       8%
     /images/home/problem/monolith-problem.webp                                   23KB       19KB      13%
     /images/home/stepper/background-shape.webp                                    1KB        0KB      34%
     /images/home/stepper/background.webp                                         78KB       36KB      53%
     /images/home/stepper/download-worker.webp                                    21KB       19KB       6%
     /images/home/stepper/gears.jpg                                               24KB       22KB       6%
     /images/home/three-cards-feature/familiar-interface-gradient.webp             7KB        8KB     -11%
     /images/home/three-cards-feature/familiar-interface.webp                     62KB       45KB      27%
     /images/home/three-cards-feature/fast-path-background-noise.webp             49KB       16KB      66%
     /images/home/three-cards-feature/fast-path-gradient.webp                      7KB        8KB      -7%
     /images/home/three-cards-feature/fast-path.webp                              52KB       41KB      21%
     /images/home/three-cards-feature/live-data-gradient.webp                      3KB        2KB      35%
     /images/home/three-cards-feature/live-data.webp                              46KB       36KB      20%
     /images/partner/hero/hero.webp                                              123KB       97KB      20%
     /images/partner/hero/partners-hero.webp                                      42KB       32KB      23%
     /images/partner/testimonials/amrendra-singh.webp                              6KB        5KB       7%
     /images/partner/testimonials/benjamin-reynolds.webp                         111KB       80KB      27%
     /images/partner/testimonials/bertrams.jpeg                                   12KB       10KB       9%
     /images/partner/testimonials/joseph-chiang.jpg                               13KB       11KB      13%
     /images/partner/testimonials/mike-babiy.png                                  24KB       19KB      20%
     /images/partner/testimonials/olivier-reinaud.jpg                             11KB        9KB      20%
     /images/pricing/engagement-band/overlay.webp                                 72KB       56KB      22%
     /images/pricing/plans/organization-icon.png                                   4KB        3KB      10%
     /images/pricing/plans/pro-icon.png                                            2KB        3KB      -7%
     /images/pricing/salesforce/help-icon.webp                                     0KB        0KB     -45%
     /images/product/demo/background.webp                                         84KB       70KB      16%
     /images/product/demo/kanban.webp                                             63KB       57KB       9%
     /images/product/feature/contacts.webp                                        44KB       32KB      28%
     /images/product/feature/dashboards.webp                                      24KB       21KB      14%
     /images/product/feature/data.webp                                            23KB       20KB      13%
     /images/product/feature/emails.webp                                          21KB       18KB      12%
     /images/product/feature/files.webp                                           14KB       12KB      14%
     /images/product/feature/mask.webp                                           148KB       82KB      44%
     /images/product/feature/pipeline.webp                                        30KB       26KB      11%
     /images/product/feature/tasks.webp                                           37KB       33KB       9%
     /images/product/stepper/background-shape.webp                                 1KB        1KB      25%
     /images/product/stepper/background.webp                                      61KB       52KB      13%
     /images/product/stepper/step-one.webp                                         7KB        7KB       0%
     /images/product/stepper/step-three.webp                                      17KB       14KB      13%
     /images/product/stepper/step-two.webp                                         7KB        7KB       3%
     /images/product/tabs/background-shape.webp                                    7KB        1KB      83%
     /images/product/tabs/background.webp                                        180KB      171KB       5%
     /images/product/tabs/deals.webp                                              93KB       89KB       4%
     /images/product/tabs/history.webp                                            66KB       59KB      10%
     /images/product/tabs/tasks.webp                                              90KB       85KB       5%
     /images/product/tabs/workflow.webp                                           60KB       55KB       7%
     /images/releases/0.10/0.10-currency.webp                                     40KB       28KB      29%
     /images/releases/0.10/0.10-datetime.webp                                     34KB       18KB      45%
     /images/releases/0.10/0.10-json.webp                                         35KB       25KB      26%
     /images/releases/0.10/0.10-multi-select.webp                                 46KB       36KB      22%
     /images/releases/0.10/0.10-remote.webp                                       45KB       27KB      39%
     /images/releases/0.11/0.11-calendar.webp                                     69KB       56KB      18%
     /images/releases/0.11/0.11-speed.webp                                        30KB       21KB      29%
     /images/releases/0.12/0.12-database-diagram.webp                             35KB       25KB      29%
     /images/releases/0.12/0.12-link-field.webp                                   50KB       35KB      30%
     /images/releases/0.12/0.12-loader.webp                                       57KB       35KB      38%
     /images/releases/0.12/0.12-notifications.webp                                78KB       64KB      17%
     /images/releases/0.2.3_relations.webp                                        55KB       39KB      28%
     /images/releases/0.2.3_webhooks.webp                                         30KB       25KB      16%
     /images/releases/0.20/0.20-blocklist.webp                                    35KB       27KB      22%
     /images/releases/0.20/0.20-onboarding.webp                                   68KB       53KB      21%
     /images/releases/0.20/0.20-timeline.webp                                     81KB       55KB      32%
     /images/releases/0.21/0.21-advanced-email-settings.webp                      42KB       30KB      27%
     /images/releases/0.21/0.21-many-many.webp                                    60KB       46KB      22%
     /images/releases/0.22/0.22-kanban-improvements.webp                          50KB       34KB      31%
     /images/releases/0.22/0.22-mass-deletion.webp                                32KB       20KB      37%
     /images/releases/0.22/0.22-navbar.webp                                       35KB       23KB      34%
     /images/releases/0.23/0.23-created-by.webp                                   78KB       60KB      23%
     /images/releases/0.23/0.23-filter-webhooks.webp                              54KB       39KB      28%
     /images/releases/0.23/0.23-notes-tasks.webp                                  83KB       62KB      25%
     /images/releases/0.24/0.24-soft-delete.webp                                  70KB       47KB      31%
     /images/releases/0.3.0_rating.webp                                           50KB       46KB       8%
     /images/releases/0.3.1_contributors.webp                                     40KB       32KB      20%
     /images/releases/0.3.2_new_layout.webp                                       52KB       37KB      28%
     /images/releases/0.3.3_emails.webp                                           48KB       38KB      21%
     /images/releases/0.3.3_kanban.webp                                           44KB       35KB      19%
     /images/releases/0.3.3_sign_up.webp                                          24KB       17KB      25%
     /images/releases/0.30/0.30-array-field.webp                                  90KB       68KB      24%
     /images/releases/0.30/0.30-emails.webp                                       44KB       30KB      31%
     /images/releases/0.30/0.30-new-settings.webp                                 31KB       19KB      37%
     /images/releases/0.31/0.31-advanced-settings.webp                            44KB       30KB      31%
     /images/releases/0.31/0.31-search.webp                                       18KB       12KB      31%
     /images/releases/0.32/0.32-improved-cmdk.webp                                50KB       33KB      33%
     /images/releases/0.32/0.32-webhooks.webp                                     41KB       30KB      26%
     /images/releases/0.33/0.33-multiselect-filter.webp                           53KB       36KB      31%
     /images/releases/0.33/0.33-percentage-number.webp                            33KB       20KB      38%
     /images/releases/0.34/0.34-subdomains.webp                                   57KB       41KB      28%
     /images/releases/0.35/0.35-Favorites.webp                                    61KB       41KB      32%
     /images/releases/0.4/0.4-address-field-type.webp                             23KB       15KB      32%
     /images/releases/0.4/0.4-expand-relation-card.webp                           28KB       18KB      34%
     /images/releases/0.4/0.4-multi-workspace.webp                                48KB       35KB      25%
     /images/releases/0.40/0.40-aggregates.webp                                   72KB       57KB      21%
     /images/releases/0.40/0.40-group-by.webp                                     47KB       32KB      31%
     /images/releases/0.41/0.41-labs.webp                                         55KB       40KB      27%
     /images/releases/0.42/0.42-document-viewer.webp                              86KB       72KB      15%
     /images/releases/0.42/0.42-microsoft.webp                                    79KB       64KB      18%
     /images/releases/0.42/0.42-translation.webp                                  66KB       53KB      19%
     /images/releases/0.43.0/email-privacy.webp                                   47KB       33KB      27%
     /images/releases/0.43.0/search-upgrade.webp                                  44KB       30KB      31%
     /images/releases/0.44/0.44-admin-panel.webp                                  57KB       42KB      25%
     /images/releases/0.44/0.44-side-panel.webp                                   67KB       54KB      20%
     /images/releases/0.50/0.50-advanced-filters.webp                             55KB       36KB      33%
     /images/releases/0.50/0.50-permissions.webp                                  51KB       37KB      25%
     /images/releases/0.51.0/0.51-options-menu.webp                               59KB       40KB      31%
     /images/releases/0.52.0/0.52-custom-date-format.webp                         37KB       23KB      36%
     /images/releases/0.52.0/0.52-filtered-views-records.webp                     46KB       31KB      31%
     /images/releases/1.00/1.00-import-update.webp                                63KB       39KB      37%
     /images/releases/1.00/1.00-performance-improvement.webp                      52KB       35KB      31%
     /images/releases/1.00/1.00-permissions.webp                                  72KB       52KB      28%
     /images/releases/1.00/1.00-subfield-filtering.webp                           73KB       49KB      33%
     /images/releases/1.00/1.00-workflow.webp                                     52KB       36KB      29%
     /images/releases/1.1/1.1-multi-manual-trigger.webp                           51KB       37KB      26%
     /images/releases/1.10/1.10.0-calendar.webp                                   54KB       38KB      28%
     /images/releases/1.10/1.10.0-dashboards.webp                                 33KB       20KB      37%
     /images/releases/1.11/1.11.0-morph-relations.webp                            53KB       40KB      24%
     /images/releases/1.11/1.11.0-unlisted-views.webp                             50KB       34KB      32%
     /images/releases/1.12/1.12.0-folder-sync.webp                                38KB       25KB      32%
     /images/releases/1.12/1.12.0-side-panel.webp                                 45KB       34KB      22%
     /images/releases/1.13/1.13.0-stop-workflow-button.png                        39KB       26KB      34%
     /images/releases/1.14/1.14.0-resize-navbar-and-side-panel.png                58KB       40KB      30%
     /images/releases/1.15/1.15.0-updated-by-official.webp                        36KB       23KB      35%
     /images/releases/1.15/1.15.0-updated-by.png                                  36KB       27KB      24%
     /images/releases/1.15/1.15.0-updated-by.webp                                 36KB       23KB      35%
     /images/releases/1.16/1.16.0-files-in-records.webp                           43KB       29KB      32%
     /images/releases/1.16/1.16.0-flexible-relations.webp                         42KB       30KB      28%
     /images/releases/1.17/1.17.0-ai-chat.webp                                    83KB       67KB      18%
     /images/releases/1.18/1.18.0-live-updates.webp                               56KB       37KB      33%
     /images/releases/1.18/1.18.0-sidebar-items.webp                              36KB       27KB      26%
     /images/releases/1.19/1.19.0-invite-roles.webp                               40KB       26KB      34%
     /images/releases/1.2/1.2-any-fields.webp                                     47KB       30KB      35%
     /images/releases/1.2/1.2-import-relations.webp                               42KB       31KB      26%
     /images/releases/1.20/1.20.0-easier-field-editing.webp                       37KB       26KB      27%
     /images/releases/1.20/1.20.0-field-widgets.webp                              38KB       29KB      24%
     /images/releases/1.21/1.21.0-email-replies.webp                              50KB       38KB      25%
     /images/releases/1.21/1.21.0-maintenance-mode.webp                           55KB       39KB      28%
     /images/releases/1.22/1.22.0-rich-text-layouts.webp                          54KB       41KB      24%
     /images/releases/1.23/1.23.0-easier-layouts.webp                             46KB       30KB      34%
     /images/releases/1.3/1.3-IMAP.webp                                           83KB       59KB      29%
     /images/releases/1.3/1.3-merge.webp                                          64KB       44KB      30%
     /images/releases/1.4/1.4-field-permissions.webp                              50KB       37KB      24%
     /images/releases/1.4/1.4-two-factor-auth.webp                                67KB       50KB      25%
     /images/releases/1.4/1.4-workflow-filters.webp                               38KB       26KB      31%
     /images/releases/1.5/1.5-workflow-branches.webp                              31KB       21KB      31%
     /images/releases/1.6/1.6-workflows-improvements.webp                         45KB       31KB      29%
     /images/releases/1.7/1.7-impersonating.webp                                  79KB       60KB      23%
     /images/releases/1.7/1.7-upsert.webp                                         51KB       31KB      38%
     /images/releases/1.8/1.8-bulk-select.webp                                    49KB       34KB      29%
     /images/releases/1.8/1.8-search-limit.webp                                   34KB       23KB      31%
     /images/releases/1.8/1.8-workflow-iterator.webp                              38KB       23KB      39%
     /images/releases/labs/translation.webp                                       65KB       46KB      28%
     /images/shared/companies/logos/a16z.png                                       0KB        0KB     -48%
     /images/shared/companies/logos/accel.png                                      0KB        0KB    -129%
     /images/shared/companies/logos/airbnb.png                                     0KB        0KB     -52%
     /images/shared/companies/logos/airtable.png                                   0KB        1KB     -42%
     /images/shared/companies/logos/anthropic.png                                  0KB        0KB     -45%
     /images/shared/companies/logos/apple-1977.png                                 0KB        1KB     -54%
     /images/shared/companies/logos/apple.png                                      0KB        0KB     -89%
     /images/shared/companies/logos/calendar.png                                   0KB        0KB     -48%
     /images/shared/companies/logos/claude.png                                    40KB       31KB      21%
     /images/shared/companies/logos/cursor.png                                     0KB        0KB     -58%
     /images/shared/companies/logos/docusign.png                                   0KB        0KB     -48%
     /images/shared/companies/logos/figma.png                                      0KB        0KB     -46%
     /images/shared/companies/logos/founders-fund.png                              0KB        0KB     -61%
     /images/shared/companies/logos/github.png                                     0KB        0KB     -37%
     /images/shared/companies/logos/gmail.png                                      0KB        1KB     -60%
     /images/shared/companies/logos/google.png                                     0KB        1KB     -36%
     /images/shared/companies/logos/hubspot.png                                    0KB        0KB      -2%
     /images/shared/companies/logos/kleiner-perkins.png                            0KB        0KB    -105%
     /images/shared/companies/logos/lemlist.png                                    0KB        0KB     -64%
     /images/shared/companies/logos/linkedin.png                                   0KB        0KB     -66%
     /images/shared/companies/logos/mailchimp.png                                  0KB        0KB     -39%
     /images/shared/companies/logos/meet.png                                       0KB        0KB     -51%
     /images/shared/companies/logos/metabase.png                                   1KB        1KB     -32%
     /images/shared/companies/logos/microsoft.png                                  0KB        0KB       4%
     /images/shared/companies/logos/notion.png                                     0KB        1KB     -19%
     /images/shared/companies/logos/okta.png                                       0KB        0KB     -35%
     /images/shared/companies/logos/openai.png                                     0KB        1KB     -13%
     /images/shared/companies/logos/outlook.png                                    0KB        0KB     -26%
     /images/shared/companies/logos/outreach.png                                   0KB        0KB     -53%
     /images/shared/companies/logos/postgresql.png                                 1KB        1KB      -2%
     /images/shared/companies/logos/qonto.png                                      0KB        0KB     -61%
     /images/shared/companies/logos/salesforce.png                                 0KB        0KB     -39%
     /images/shared/companies/logos/segment.png                                    0KB        1KB     -49%
     /images/shared/companies/logos/sequoia.png                                    0KB        0KB     -59%
     /images/shared/companies/logos/slack.png                                      0KB        0KB     -40%
     /images/shared/companies/logos/stripe.png                                     0KB        0KB     -51%
     /images/shared/companies/logos/tally.png                                      0KB        0KB     -53%
     /images/shared/companies/logos/twenty.png                                     0KB        0KB     -56%
     /images/shared/companies/logos/whatsapp.png                                   0KB        0KB     -64%
     /images/shared/companies/logos/zapier.png                                     0KB        0KB     -27%
     /images/shared/engagement-band/halftone-on-gray.png                           7KB       17KB    -121%
     /images/shared/engagement-band/halftone-on-white.png                          2KB        2KB      -2%
     /images/shared/light-noise.webp                                               0KB        0KB    -225%
     /images/shared/menu/developers-preview.png                                   65KB       33KB      49%
     /images/shared/menu/partners-preview.png                                     52KB       24KB      53%
     /images/shared/menu/user-guide-preview.png                                   58KB       50KB      14%
     /images/shared/people/avatars/alexandre-prot.jpg                              0KB        0KB     -32%
     /images/shared/people/avatars/anonymous-anna.jpg                              0KB        0KB     -81%
     /images/shared/people/avatars/anonymous-fabrice.jpg                           0KB        0KB     -28%
     /images/shared/people/avatars/anonymous-felix.jpg                             0KB        0KB     -40%
     /images/shared/people/avatars/anonymous-indira.jpg                            0KB        0KB     -44%
     /images/shared/people/avatars/anonymous-laura.jpg                             0KB        0KB     -52%
     /images/shared/people/avatars/anonymous-mike.jpg                              0KB        0KB     -78%
     /images/shared/people/avatars/anonymous-thomas.jpg                            0KB        0KB     -44%
     /images/shared/people/avatars/ben-chestnut.jpg                                0KB        0KB     -74%
     /images/shared/people/avatars/brian-chesky.jpg                                0KB        0KB     -52%
     /images/shared/people/avatars/chris-wanstrath.jpg                             0KB        0KB     -46%
     /images/shared/people/avatars/craig-federighi.jpg                             0KB        0KB     -63%
     /images/shared/people/avatars/dario-amodei.jpg                               15KB       14KB       4%
     /images/shared/people/avatars/dylan-field.jpg                                 0KB        0KB     -62%
     /images/shared/people/avatars/eddy-cue.jpg                                    0KB        0KB     -54%
     /images/shared/people/avatars/ivan-zhao.jpg                                   0KB        0KB     -69%
     /images/shared/people/avatars/jeff-williams.jpg                               0KB        0KB     -59%
     /images/shared/people/avatars/joe-gebbia.jpg                                  0KB        0KB     -52%
     /images/shared/people/avatars/katherine-adams.jpg                             0KB        0KB     -64%
     /images/shared/people/avatars/patrick-collison.jpg                            0KB        0KB     -20%
     /images/shared/people/avatars/peter-reinhardt.jpg                             0KB        0KB     -87%
     /images/shared/people/avatars/peter-thiel.jpg                                 0KB        0KB     -57%
     /images/shared/people/avatars/phil-schiller.jpg                               0KB        0KB     -52%
     /images/shared/people/avatars/ping-li.jpg                                    13KB       12KB      10%
     /images/shared/people/avatars/ray-damm.jpg                                    0KB        0KB     -41%
     /images/shared/people/avatars/reid-hoffman.jpg                                0KB        0KB     -60%
     /images/shared/people/avatars/roelof-botha.jpg                                0KB        0KB     -41%
     /images/shared/people/avatars/ryan-roslansky.jpg                              0KB        0KB     -46%
     /images/shared/people/avatars/steve-anavi.jpg                                 0KB        0KB     -32%
     /images/shared/people/avatars/stewart-butterfield.jpg                         0KB        0KB     -36%
     /images/shared/people/avatars/sundar-pichai.jpg                               0KB        0KB     -41%
     /images/shared/people/avatars/thomas-dohmke.jpg                              15KB       14KB       4%
     /images/shared/people/avatars/tim-cook.jpg                                    0KB        0KB     -68%
     /images/why-twenty/hero/background.webp                                      10KB        9KB      11%

     === TOTALS ===
     Files: 249
     Total WebP: 8.2 MB
     Total AVIF: 6.1 MB
     Total saved: 2.1 MB (26%)
```
2026-04-21 18:15:38 +02:00
neo773andGitHub a7b10b281c Optimize 3d models (#19935)
Remove unused textures and UV data from 3D models
Also simplified meshes where possible and re-compressed
Verified that no visual regression was seen but needs to be double
checked in case I missed something

Total model size: 5.1MB -> 1.5MB
2026-04-21 18:13:03 +02:00
Abdul RahmanandGitHub 3eabdf302e Allow closing navbar folder while viewing an active child item (#19936)
### Before


https://github.com/user-attachments/assets/f8c5c075-b086-4a4c-9514-c7cbbf9b7c56




### After


https://github.com/user-attachments/assets/37427428-046c-4815-a57e-bc5c4fcc7f68
2026-04-21 14:49:31 +00:00
8bb98c309a i18n - docs translations (#19939)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 16:52:23 +02:00
EtienneandGitHub 8f34a02fea Import - Fix (#19938)
Fixes
https://discord.com/channels/1130383047699738754/1496121832619774042/1496121832619774042

Before
<img width="1089" height="734" alt="Screenshot 2026-04-21 at 16 23 27"
src="https://github.com/user-attachments/assets/6fe17bc1-b1ec-4b5d-a922-d472ff4c4f2f"
/>
After
<img width="1089" height="731" alt="Screenshot 2026-04-21 at 16 22 56"
src="https://github.com/user-attachments/assets/ab7e8fe4-8188-4222-a237-992b73a1594e"
/>
2026-04-21 14:39:34 +00:00
Paul RastoinandGitHub 61fdb613e6 Reset default app packages command (#19931)
# Introduction
If a self host creates its twenty instance using storage type local, and
then edit through the admin panel the storage type, the apps default
deps file won't be swapped to the new storage location

This command allow to manually rebuild them

## What could be done in addition
- We could display a modal in the admin panel when the user is editing
env variable that might have a side effect
- We might wanna rebuild the deps by default if we detect such a change
through the UI though we can't really if it's through the `.env` so I'm
not sure we wanna prio such logic
2026-04-21 13:32:59 +00:00
Abdullah.andGitHub 7947b1b843 Fix self-hosting pricing page design. (#19930)
Resolve:
https://discord.com/channels/1130383047699738754/1496108238909739079
2026-04-21 13:03:17 +00:00
Abdullah.andGitHub 2b399fc94e [Website] Fix flickering of faq illustration. (#19920)
Before:


https://github.com/user-attachments/assets/da189675-5de2-47be-b491-0d52eb11331e

After:


https://github.com/user-attachments/assets/7e382523-3539-4978-873c-e4ea79e264a7
2026-04-21 15:05:58 +02:00
44ba7725ae i18n - docs translations (#19934)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 14:46:37 +02:00
Paul RastoinandGitHub 65e01400c0 Cross version ci placeholder (#19932)
Created this empty workflow so it appears on main and is pickable from a
different branch to start testing the whole flow
2026-04-21 14:19:28 +02:00
neo773andGitHub 62ea14a072 fix email workflow (#19929)
fixes JSON parse crash with proper resolution of variables and tiptap
rich text classification
2026-04-21 13:47:19 +02:00
8cdd2a3319 i18n - docs translations (#19928)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 12:49:35 +02:00
EtienneandGitHub 1db242d399 Website - small fixes (#19918)
- Fix release note link in footer
- Fix Talk to partner CTA in pricing page
2026-04-21 09:50:04 +00:00
Paul RastoinandGitHub a583ee405b Cross version and upgrade status docs (#19926) 2026-04-21 09:40:23 +00:00
cd73088be6 i18n - docs translations (#19925)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 10:57:27 +02:00
15938c1fca Add 2.0.0 release changelog (#19923)
## Summary
- Add 2.0.0 release notes and illustrations to `twenty-website-new`
- Remove old release mdx files from the legacy `twenty-website`
- Fix the resources-menu "Releases" preview to pull the latest release
dynamically, using the first (hero) image of the latest mdx

## Test plan
- [ ] Open any page on `twenty-website-new`, hover "Resources" → the
Releases preview shows "See what shipped in 2.0.0" with the Build-an-app
hero illustration
- [ ] Visit `/releases` and confirm 2.0.0 renders with all five sections
and images
- [ ] Confirm legacy `twenty-website` no longer ships the deleted
release pages

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:45:00 +00:00
neo773andGitHub 071980d511 Revert "fix compute folders to update util (#19749)" (#19921)
This reverts commit 64470baa1e
2026-04-21 08:02:24 +00:00
Abdul RahmanandGitHub e3d7d0199d Fix side panel hotkeys breaking when opening records from table (#19849)
## Summary

- Fix side panel hotkeys (Ctrl+K, Escape, etc.) breaking when opening
records from the record index table
- Ensure `side-panel-focus` is always restored in the focus stack when
navigating within an already-open side panel
- Remove stale `globalHotkeysConfig` on `record-index` focus item that
persisted after the side panel closed

## Problem

When clicking records in the table to open them in the side panel,
`useLeaveTableFocus` called `resetFocusStackToRecordIndex` which wiped
the entire focus stack, including the `side-panel-focus` entry. Since
`openSidePanel` early-returned when the panel was already open,
`side-panel-focus` was never restored. Additionally,
`resetFocusStackToRecordIndex` set `enableGlobalHotkeysWithModifiers:
false` on the remaining `record-index` item when the side panel was
open, and this stale config persisted after the panel closed,
permanently blocking all hotkeys.

## Fix

- **`useNavigateSidePanel.ts`**: Move `pushFocusItemToFocusStack` before
the `isSidePanelOpened` early-return so the side panel's focus entry is
always present in the stack
- **`useResetFocusStackToRecordIndex.ts`**: Always set
`enableGlobalHotkeysWithModifiers: true` on the `record-index` item.
Hotkey scoping when the side panel is open is handled by
`side-panel-focus` sitting on top of the focus stack



https://github.com/user-attachments/assets/ad25befb-338d-4166-9580-18d4e92d6f9b
2026-04-21 07:44:00 +00:00
30b8663a74 chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary
- AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from
`FeatureFlagKey`, the public flag catalog, and the dev seeder.
- Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent,
agent chat, chat subscription, role-to-agent assignment, workflow AI
step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard`
wiring in the AI and workflow modules.
- Removes frontend gating from settings nav, role
permissions/assignment/applicability, command menu hotkeys, side panel,
mobile/drawer nav, and the agent chat provider so AI UI is always on.
Tests and generated GraphQL/SDK schemas updated accordingly.

## Test plan
- [x] `npx nx typecheck twenty-shared`
- [x] `npx nx typecheck twenty-server`
- [x] `npx nx typecheck twenty-front`
- [x] `npx nx lint:diff-with-main twenty-server`
- [x] `npx nx lint:diff-with-main twenty-front`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
feature-flag`
- [x] `npx jest --config=packages/twenty-server/jest.config.mjs
workspace-entity-manager`
- [ ] Manual smoke test: AI features still accessible without any flag
row in `featureFlag`

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:49:46 +02:00
69868a0ab6 docs: remove alpha warning from apps pages except skills & agents (#19919)
## Summary
- Remove the \"Apps are currently in alpha\" warning from 8 pages under
`developers/extend/apps/` (getting-started, architecture/building,
data-model, layout, logic-functions, front-components, cli-and-testing,
publishing).
- Keep the warning on the Skills & Agents page only, and reword it to
scope it to that feature: \"Skills and agents are currently in alpha.
The feature works but is still evolving.\"

## Test plan
- [ ] Preview docs build and confirm the warning banner no longer
appears on the 8 pages above.
- [ ] Confirm the warning still renders on the Skills & Agents page with
the updated wording.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:49:37 +02:00
7583 changed files with 389452 additions and 234391 deletions
-22
View File
@@ -1,22 +0,0 @@
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "bash",
"args": ["-c", "source packages/twenty-server/.env && npx -y @modelcontextprotocol/server-postgres \"$PG_DATABASE_URL\""],
"env": {}
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["@playwright/mcp@latest", "--no-sandbox", "--headless"],
"env": {}
},
"context7": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
}
}
}
-223
View File
@@ -1,223 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Twenty is an open-source CRM built with modern technologies in a monorepo structure. The codebase is organized as an Nx workspace with multiple packages.
## Key Commands
### Development
```bash
# Start development environment (frontend + backend + worker)
yarn start
# Individual package development
npx nx start twenty-front # Start frontend dev server
npx nx start twenty-server # Start backend server
npx nx run twenty-server:worker # Start background worker
```
### Testing
```bash
# Preferred: run a single test file (fast)
npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
# Run all tests for a package
npx nx test twenty-front # Frontend unit tests
npx nx test twenty-server # Backend unit tests
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
# To run an indivual test or a pattern of tests, use the following command:
cd packages/{workspace} && npx jest "pattern or filename"
# Storybook
npx nx storybook:build twenty-front
npx nx storybook:test twenty-front
# When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
```
### Code Quality
```bash
# Linting (diff with main - fastest, always prefer this)
npx nx lint:diff-with-main twenty-front
npx nx lint:diff-with-main twenty-server
npx nx lint:diff-with-main twenty-front --configuration=fix # Auto-fix
# Linting (full project - slower, use only when needed)
npx nx lint twenty-front
npx nx lint twenty-server
# Type checking
npx nx typecheck twenty-front
npx nx typecheck twenty-server
# Format code
npx nx fmt twenty-front
npx nx fmt twenty-server
```
### Build
```bash
# Build packages (twenty-shared must be built first)
npx nx build twenty-shared
npx nx build twenty-front
npx nx build twenty-server
```
### Database Operations
```bash
# Database management
npx nx database:reset twenty-server # Reset database
npx nx run twenty-server:database:init:prod # Initialize database
npx nx run twenty-server:database:migrate:prod # Run instance commands (fast only)
# Generate an instance command (fast or slow)
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```
### Database Inspection (Postgres MCP)
A read-only Postgres MCP server is configured in `.mcp.json`. Use it to:
- Inspect workspace data, metadata, and object definitions while developing
- Verify migration results (columns, types, constraints) after running migrations
- Explore the multi-tenant schema structure (core, metadata, workspace-specific schemas)
- Debug issues by querying raw data to confirm whether a bug is frontend, backend, or data-level
- Inspect metadata tables to debug GraphQL schema generation issues
This server is read-only — for write operations (reset, migrations, sync), use the CLI commands above.
### GraphQL
```bash
# Generate GraphQL types (run after schema changes)
npx nx run twenty-front:graphql:generate
npx nx run twenty-front:graphql:generate --configuration=metadata
```
## Architecture Overview
### Tech Stack
- **Frontend**: React 18, TypeScript, Jotai (state management), Linaria (styling), Vite
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
- **Monorepo**: Nx workspace managed with Yarn 4
### Package Structure
```
packages/
├── twenty-front/ # React frontend application
├── twenty-server/ # NestJS backend API
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
### Key Development Principles
- **Functional components only** (no class components)
- **Named exports only** (no default exports)
- **Types over interfaces** (except when extending third-party interfaces)
- **String literals over enums** (except for GraphQL enums)
- **No 'any' type allowed** — strict TypeScript enforced
- **Event handlers preferred over useEffect** for state updates
- **Props down, events up** — unidirectional data flow
- **Composition over inheritance**
- **No abbreviations** in variable names (`user` not `u`, `fieldMetadata` not `fm`)
### Naming Conventions
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE
- **Types/Classes**: PascalCase (suffix component props with `Props`, e.g. `ButtonProps`)
- **Files/directories**: kebab-case with descriptive suffixes (`.component.tsx`, `.service.ts`, `.entity.ts`, `.dto.ts`, `.module.ts`)
- **TypeScript generics**: descriptive names (`TData` not `T`)
### File Structure
- Components under 300 lines, services under 500 lines
- Components in their own directories with tests and stories
- Use `index.ts` barrel exports for clean imports
- Import order: external libraries first, then internal (`@/`), then relative
### Comments
- Use short-form comments (`//`), not JSDoc blocks
- Explain WHY (business logic), not WHAT
- Do not comment obvious code
- Multi-line comments use multiple `//` lines, not `/** */`
### State Management
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
- GraphQL cache managed by Apollo Client
- Use functional state updates: `setState(prev => prev + 1)`
### Backend Architecture
- **NestJS modules** for feature organization
- **TypeORM** for database ORM with PostgreSQL
- **GraphQL** API with code-first approach
- **Redis** for caching and session management
- **BullMQ** for background job processing
### Database & Upgrade Commands
- **PostgreSQL** as primary database
- **Redis** for caching and sessions
- **ClickHouse** for analytics (when enabled)
- When changing entity files, generate an **instance command** (`database:migrate:generate --name <name> --type <fast|slow>`)
- **Fast** instance commands handle schema changes; **slow** ones add a `runDataMigration` step for data backfills
- **Workspace commands** iterate over all active/suspended workspaces for per-workspace upgrades
- Commands use `@RegisteredInstanceCommand` and `@RegisteredWorkspaceCommand` decorators for automatic discovery
- Include both `up` and `down` logic in instance commands
- Never delete or rewrite committed instance command `up`/`down` logic
- See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for full documentation
### Utility Helpers
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
### Before Making Changes
1. Always run linting (`lint:diff-with-main`) and type checking after code changes
2. Test changes with relevant test suites (prefer single-file test runs)
3. Ensure instance commands are generated for entity changes (`database:migrate:generate`)
4. Check that GraphQL schema changes are backward compatible
5. Run `graphql:generate` after any GraphQL schema changes
### Code Style Notes
- Use **Linaria** for styling with zero-runtime CSS-in-JS (styled-components pattern)
- Follow **Nx** workspace conventions for imports
- Use **Lingui** for internationalization
- Apply security first, then formatting (sanitize before format)
### Testing Strategy
- **Test behavior, not implementation** — focus on user perspective
- **Test pyramid**: 70% unit, 20% integration, 10% E2E
- Query by user-visible elements (text, roles, labels) over test IDs
- Use `@testing-library/user-event` for realistic interactions
- Descriptive test names: "should [behavior] when [condition]"
- Clear mocks between tests with `jest.clearAllMocks()`
## Dev Environment Setup
All dev environments (Claude Code web, Cursor, local) use one script:
```bash
bash packages/twenty-utils/setup-dev-env.sh
```
This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times.
- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`)
- `--down` — stop services
- `--reset` — wipe data and restart fresh
- **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc.
**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script.
## Important Files
- `nx.json` - Nx workspace configuration with task definitions
- `tsconfig.base.json` - Base TypeScript configuration
- `package.json` - Root package with workspace definitions
- `.cursor/rules/` - Detailed development guidelines and best practices
+14 -13
View File
@@ -125,15 +125,16 @@ mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
**Naming Convention:** `{VERSION}-descriptive-name.png`
**Naming Convention:** `{VERSION}-descriptive-name.webp`
Examples:
- `1.9.0-feature-name.png`
- `1.9.0-another-feature.png`
- `1.9.0-feature-name.webp`
- `1.9.0-another-feature.webp`
```bash
# Move and rename files
# Move and rename source files, then convert to webp if needed
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)
@@ -158,19 +159,19 @@ Date: {YYYY-MM-DD}
Short description explaining what the feature does and why it's useful. Keep it user-focused and concise (1-2 sentences).
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp)
# Feature 2 Name
Another short description of the second feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp)
# Feature 3 Name
Description of the third feature.
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.png)
![](/images/releases/{MINOR_VERSION}/{VERSION}-feature-3.webp)
```
**Style Guidelines:**
@@ -221,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/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.png
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.png
- 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.
```
@@ -288,12 +289,12 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
- **Location**: `packages/twenty-website/public/images/releases/`
### Image Files
- **Format**: `{VERSION}-descriptive-name.png`
- **Format**: `{VERSION}-descriptive-name.webp`
- **Convention**: Kebab-case descriptive names
- **Examples**:
- `1.8.0-workflow-iterator.png`
- `1.8.0-bulk-select.png`
- `1.9.0-new-feature.png`
- `1.8.0-workflow-iterator.webp`
- `1.8.0-bulk-select.webp`
- `1.9.0-new-feature.webp`
## Quick Reference Template
+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
```
+38
View File
@@ -0,0 +1,38 @@
# ============================================================
# CI/CD & GitHub infrastructure
# ============================================================
/.github/ @Weiko @charlesBochet @prastoin
/.github/CODEOWNERS @Weiko @charlesBochet @prastoin
/.github/workflows/ @Weiko @charlesBochet @prastoin
/.github/actions/ @Weiko @charlesBochet @prastoin
/.github/dependabot.yml @Weiko @charlesBochet @prastoin
# ============================================================
# Package management & dependency lockfiles
# ============================================================
/.yarnrc.yml @Weiko @charlesBochet @prastoin
/.npmrc @Weiko @charlesBochet @prastoin
**/package.json @Weiko @charlesBochet @prastoin
**/yarn.lock @Weiko @charlesBochet @prastoin
# ============================================================
# twenty-apps: exempt from strict ownership (last match wins)
# ============================================================
/packages/twenty-apps/
# ============================================================
# Build & tooling configs
# ============================================================
/nx.json @Weiko @charlesBochet @prastoin
# ============================================================
# Docker (container-level compromise)
# ============================================================
**/Dockerfile* @Weiko @charlesBochet @prastoin
**/docker-compose*.yml @Weiko @charlesBochet @prastoin
**/docker-compose*.yaml @Weiko @charlesBochet @prastoin
# ============================================================
# Git hooks (execute on checkout/commit/push)
# ============================================================
/.husky/** @Weiko @charlesBochet @prastoin
+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 }}
+18 -4
View File
@@ -7,6 +7,17 @@ inputs:
runs:
using: 'composite'
steps:
- name: Free disk space for install
if: runner.os == 'Linux'
shell: bash
run: |
# Default GitHub images ship large SDKs this repo does not use; removing
# them avoids ENOSPC when restoring or linking a full Yarn node_modules.
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
df -h
- name: Cache primary key builder
id: globals
shell: bash
@@ -18,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 }}-
@@ -33,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 }}
+12 -13
View File
@@ -4,20 +4,19 @@
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
"preserve_hierarchy": true
"base_path": ".."
files: [
{
#
# Source files filter - PO files for Lingui
#
"source": "**/en.po",
preserve_hierarchy: true
base_path: ..
files:
#
# Source files filter - PO files for Lingui
#
- source: packages/twenty-front/src/locales/en.po
#
# Translation files path
#
"translation": "%original_path%/%locale%.po",
}
]
translation: '%original_path%/%locale%.po'
- source: packages/twenty-server/src/engine/core-modules/i18n/locales/en.po
translation: '%original_path%/%locale%.po'
- source: packages/twenty-emails/src/locales/en.po
translation: '%original_path%/%locale%.po'
+23
View File
@@ -0,0 +1,23 @@
#
# Crowdin CLI configuration for Website translations (twenty-website)
# Project ID: 4
# See https://crowdin.github.io/crowdin-cli/configuration for more information
#
project_id: 4
preserve_hierarchy: true
base_url: 'https://twenty.api.crowdin.com'
base_path: ..
languages_mapping:
locale:
fr: fr-FR
files:
#
# Source file - PO file for Lingui
#
- source: packages/twenty-website/src/locales/en.po
#
# Translation files path
#
translation: '%original_path%/%locale%.po'
+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
@@ -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: |
@@ -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,7 +76,7 @@ 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
+10 -22
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,7 +92,7 @@ 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
@@ -121,7 +117,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 +132,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 +160,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 +195,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 +206,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
+83 -5
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
@@ -78,6 +78,83 @@ jobs:
tag: scope:backend
tasks: lint,typecheck
server-previous-version-upgrade-mutation-guard:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Fetch custom Github Actions and base branch history
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@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45.0.9
with:
files: |
packages/twenty-server/src/database/commands/upgrade-version-command/**
- name: Check upgrade version commands are in current version only
if: >
steps.changed-files.outputs.any_changed == 'true' &&
!contains(github.event.pull_request.labels.*.name, 'ci:allow-previous-version-upgrade-mutation')
run: |
VERSION_CONSTANT_FILE="packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-current-version.constant.ts"
CURRENT_VERSION=$(sed -n "s/.*TWENTY_CURRENT_VERSION = '\([0-9.]*\)'.*/\1/p" "$VERSION_CONSTANT_FILE")
if [ -z "$CURRENT_VERSION" ]; then
echo "::error::Could not extract TWENTY_CURRENT_VERSION from $VERSION_CONSTANT_FILE"
exit 1
fi
CURRENT_DIR=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+)\..*/\1-\2/')
echo "Current version: $CURRENT_VERSION (directory: $CURRENT_DIR)"
ADDED_OFFENDERS=""
MODIFIED_OFFENDERS=""
check_files() {
local category="$1"
shift
for file in "$@"; do
VERSION_DIR=$(echo "$file" | sed -n 's|.*upgrade-version-command/\([0-9]*-[0-9]*\)/.*|\1|p')
if [ -n "$VERSION_DIR" ] && [ "$VERSION_DIR" != "$CURRENT_DIR" ]; then
if [ "$category" = "added" ]; then
ADDED_OFFENDERS="$ADDED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
else
MODIFIED_OFFENDERS="$MODIFIED_OFFENDERS\n - $file (version directory: $VERSION_DIR)"
fi
fi
done
}
check_files "added" ${{ steps.changed-files.outputs.added_files }}
check_files "modified" ${{ steps.changed-files.outputs.modified_files }}
if [ -n "$ADDED_OFFENDERS" ] || [ -n "$MODIFIED_OFFENDERS" ]; then
echo "This PR touches upgrade command files outside the current version directory ($CURRENT_DIR / $CURRENT_VERSION)."
if [ -n "$ADDED_OFFENDERS" ]; then
echo ""
echo "New files added to non-current version directories:"
echo -e "$ADDED_OFFENDERS"
fi
if [ -n "$MODIFIED_OFFENDERS" ]; then
echo ""
echo "Existing files modified in non-current version directories:"
echo -e "$MODIFIED_OFFENDERS"
fi
echo ""
echo "If this is intentional, add the label 'ci:allow-previous-version-upgrade-mutation' to this PR and re-run CI."
echo "Otherwise, please move your changes to the current version directory ($CURRENT_DIR)."
echo "::error::Upgrade commands were added or modified in non-current version directories."
exit 1
fi
server-validation:
needs: server-build
timeout-minutes: 30
@@ -101,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
@@ -201,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
@@ -264,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
@@ -311,6 +388,7 @@ jobs:
changed-files-check,
server-build,
server-lint-typecheck,
server-previous-version-upgrade-mutation-guard,
server-validation,
server-test,
server-integration-test,
+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
+20 -37
View File
@@ -1,12 +1,12 @@
name: CI Website
permissions:
contents: read
on:
pull_request:
merge_group:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -18,53 +18,36 @@ jobs:
with:
files: |
package.json
yarn.lock
packages/twenty-website/**
website-build:
packages/twenty-shared/**
website-task:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
timeout-minutes: 30
runs-on: ubuntu-latest
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
env:
NODE_OPTIONS: '--max-old-space-size=6144'
strategy:
matrix:
task: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Server / Create DB
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
- name: Website / Run migrations
run: npx nx database:migrate twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
- name: Website / Build Website
run: npx nx build twenty-website
env:
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
KEYSTATIC_GITHUB_CLIENT_ID: xxx
KEYSTATIC_GITHUB_CLIENT_SECRET: xxx
KEYSTATIC_SECRET: xxx
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG: xxx
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
tag: scope:website
tasks: ${{ matrix.task }}
ci-website-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, website-build]
needs: [changed-files-check, website-task]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+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 -9
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,13 +69,11 @@ 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
download_translations: true
source: '**/en.po'
translation: '%original_path%/%locale%.po'
export_only_approved: false
localization_branch_name: i18n
base_url: 'https://twenty.api.crowdin.com'
@@ -141,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"
+22 -12
View File
@@ -36,17 +36,27 @@ jobs:
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 }}"}'
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/"$REPOSITORY"/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_full_name]=$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) }}}'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-env-url \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
-f "client_payload[repo]=$REPOSITORY"
+57 -10
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- 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 }}
@@ -56,10 +56,54 @@ jobs:
- name: Create Tunnel
id: expose-tunnel
uses: codetalkio/expose-tunnel@v1.5.0
with:
service: bore.pub
port: 3000
env:
CLOUDFLARED_VERSION: '2026.3.0'
run: |
set -euo pipefail
# Install cloudflared (pinned for reproducibility)
sudo curl -fsSL -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
sudo chmod +x /usr/local/bin/cloudflared
cloudflared --version
# Start an account-less "quick tunnel" pointing at the server container.
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
log_file="$RUNNER_TEMP/cloudflared.log"
: > "$log_file"
cloudflared tunnel \
--url http://localhost:3000 \
--no-autoupdate \
--logfile "$log_file" \
--loglevel info \
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
pid=$!
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
echo "cloudflared PID: $pid"
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
url=''
for _ in $(seq 1 60); do
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
[ -n "$url" ] && break
if ! kill -0 "$pid" 2>/dev/null; then
echo "cloudflared exited before producing a URL"
cat "$log_file" || true
exit 1
fi
sleep 2
done
if [ -z "$url" ]; then
echo "Timed out waiting for tunnel URL"
cat "$log_file" || true
exit 1
fi
echo "Tunnel URL: $url"
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
- name: Start services with correct SERVER_URL
env:
@@ -99,9 +143,9 @@ jobs:
- 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..."
echo "Seeding light dev workspace (Apple only)..."
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
@@ -122,7 +166,7 @@ jobs:
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: tunnel-url
path: tunnel-url.txt
@@ -134,6 +178,9 @@ jobs:
- name: Cleanup
if: always()
run: |
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
fi
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"
+134
View File
@@ -0,0 +1,134 @@
# Pull down website translations from Crowdin every two hours or when triggered manually.
# When force_pull input is true, translations will be pulled regardless of compilation status.
name: 'Pull website translations from Crowdin'
permissions:
contents: write
pull-requests: write
on:
schedule:
- cron: '0 */2 * * *' # Every two hours.
workflow_dispatch:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
workflow_call:
inputs:
force_pull:
description: 'Force pull translations regardless of compilation status'
required: false
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
pull_website_translations:
name: Pull website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: ${{ github.head_ref || github.ref_name }}
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build twenty-shared
run: npx nx build twenty-shared
# Strict mode fails if there are missing website translations.
- name: Compile website translations
id: compile_translations_strict
run: npx nx run twenty-website:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@twenty.com'
git add .
git stash
- name: Pull website translations from Crowdin
if: inputs.force_pull || steps.compile_translations_strict.outcome == 'failure'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: false
upload_translations: false
download_translations: true
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'
auto_approve_imported: false
import_eq_suggestions: false
download_sources: false
push_sources: false
skip_untranslated_strings: false
skip_untranslated_files: false
push_translations: false
create_pull_request: false
skip_ref_checkout: true
dryrun_action: false
config: '.github/crowdin-website.yml'
env:
GITHUB_TOKEN: ${{ github.token }}
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
# As the files are extracted from a Docker container, they belong to root:root.
# We need to fix this before the next steps.
- name: Fix file permissions
run: sudo chown -R runner:docker .
- name: Compile website translations
id: compile_translations
run: |
npx nx run twenty-website:lingui:compile
git status
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
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes
if: steps.compile_translations.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Create pull request
if: steps.compile_translations.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.compile_translations.outputs.changes_detected == 'true'
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches -f event_type=i18n-pr-ready
+110
View File
@@ -0,0 +1,110 @@
name: 'Push website translations to Crowdin'
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
workflow_call:
push:
branches: ['main']
paths:
- 'packages/twenty-website/**'
- '.github/crowdin-website.yml'
- '.github/workflows/website-i18n-push.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
extract_website_translations:
name: Extract and upload website translations
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ github.token }}
ref: main
- name: Setup website i18n branch
run: |
git fetch origin i18n-website || true
git checkout -B i18n-website origin/i18n-website || git checkout -b i18n-website
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: npx nx build twenty-shared
- name: Extract website translations
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/src/locales
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: extract website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Compile website translations
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/src/locales/generated
if ! git diff --staged --quiet --exit-code; then
git commit -m "chore: compile website translations"
echo "changes_detected=true" >> $GITHUB_OUTPUT
else
echo "changes_detected=false" >> $GITHUB_OUTPUT
fi
- name: Push changes and create remote branch if needed
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: git push origin HEAD:i18n-website
- name: Upload missing website translations
if: steps.check_extract_changes.outputs.changes_detected == 'true'
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
upload_sources: true
upload_translations: true
download_translations: false
localization_branch_name: i18n-website
base_url: 'https://twenty.api.crowdin.com'
config: '.github/crowdin-website.yml'
env:
# Website translations project
CROWDIN_PROJECT_ID: '4'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Create a pull request
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
run: |
if git diff --name-only origin/main..HEAD | grep -q .; then
gh pr create -B main -H i18n-website --title 'i18n - website translations' --body 'Created by Github action' || true
else
echo "No file differences between branches, skipping PR creation"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger i18n automerge
if: steps.check_extract_changes.outputs.changes_detected == 'true' || steps.check_compile_changes.outputs.changes_detected == 'true'
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"
+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
+2 -1
View File
@@ -110,7 +110,8 @@ packages/
├── twenty-ui/ # Shared UI components library
├── twenty-shared/ # Common types and utilities
├── twenty-emails/ # Email templates with React Email
├── twenty-website/ # Next.js documentation website
├── twenty-website/ # Next.js marketing website
├── twenty-docs/ # Documentation website
├── twenty-zapier/ # Zapier integration
└── twenty-e2e-testing/ # Playwright E2E tests
```
+28 -34
View File
@@ -1,9 +1,3 @@
<p align="center">
<a href="https://www.producthunt.com/products/twenty-crm?launch=twenty-2-0">
<img src="./packages/twenty-website/public/images/readme/product-hunt-banner.png" alt="We're live on Product Hunt — Support us" />
</a>
</p>
<p align="center">
<a href="https://www.twenty.com">
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
@@ -12,14 +6,14 @@
<h2 align="center" >The #1 Open-Source CRM</h2>
<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.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/public/images/readme/github-cover-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.png" />
<img src="./packages/twenty-website/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>
@@ -30,7 +24,7 @@
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/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 />
@@ -69,7 +63,7 @@ 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.
@@ -91,17 +85,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.png" />
<img src="./packages/twenty-website/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/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/public/images/readme/v2-version-control-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.png" />
<img src="./packages/twenty-website/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/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
</td>
@@ -109,17 +103,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.png" />
<img src="./packages/twenty-website/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/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/public/images/readme/v2-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.png" />
<img src="./packages/twenty-website/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/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
</td>
@@ -127,17 +121,17 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<tr>
<td width="50%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.png" />
<img src="./packages/twenty-website/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/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/public/images/readme/v2-crm-tools-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.png" />
<img src="./packages/twenty-website/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/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
</td>
@@ -158,13 +152,13 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/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/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/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/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).
+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"]
+7 -157
View File
@@ -1,172 +1,21 @@
{
"private": true,
"dependencies": {
"@apollo/client": "^4.0.0",
"@floating-ui/react": "^0.24.3",
"@linaria/core": "^6.2.0",
"@linaria/react": "^6.2.1",
"@radix-ui/colors": "^3.0.0",
"@sniptt/guards": "^0.2.0",
"@tabler/icons-react": "^3.31.0",
"@wyw-in-js/babel-preset": "^1.0.6",
"@wyw-in-js/vite": "^0.7.0",
"archiver": "^7.0.1",
"danger-plugin-todos": "^1.3.1",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"deep-equal": "^2.2.2",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"googleapis": "105",
"hex-rgb": "^5.0.0",
"immer": "^10.1.1",
"jotai": "^2.17.1",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.escaperegexp": "^4.1.2",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"microdiff": "^1.3.2",
"next-with-linaria": "^1.3.0",
"planer": "^1.2.0",
"pluralize": "^8.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.30.3",
"react-tooltip": "^5.13.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"semver": "^7.5.4",
"slash": "^5.1.0",
"temporal-polyfill": "^0.3.0",
"ts-key-enum": "^2.0.12",
"tslib": "^2.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.2",
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@chromatic-com/storybook": "^4.1.3",
"@graphql-codegen/cli": "^3.3.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@nx/jest": "22.5.4",
"@nx/js": "22.5.4",
"@nx/react": "22.5.4",
"@nx/storybook": "22.5.4",
"@nx/vite": "22.5.4",
"@nx/web": "22.5.4",
"@oxlint/plugins": "^1.51.0",
"@sentry/types": "^8",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.3.3",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.3.3",
"@storybook/test-runner": "^0.24.2",
"@swc-node/register": "^1.11.1",
"@swc/cli": "^0.7.10",
"@swc/core": "^1.15.11",
"@swc/helpers": "~0.5.19",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/addressparser": "^1.0.3",
"@types/bcrypt": "^5.0.0",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
"@types/fs-extra": "^11.0.4",
"@types/graphql-fields": "^1.3.6",
"@types/inquirer": "^9.0.9",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.7",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.isobject": "^3.0.7",
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.7",
"@types/lodash.upperfirst": "^4.3.7",
"@types/ms": "^0.7.31",
"@types/node": "^24.0.0",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^2.1.0",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
"danger": "^13.0.4",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.25.10",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.5.4",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.20",
"storybook": "^10.3.3",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"ts-node": "10.9.1",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"oxfmt": "0.50.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1",
"vite": "^7.0.0",
"vitest": "^4.0.18"
"verdaccio": "^6.3.1"
},
"engines": {
"node": "^24.5.0",
@@ -185,7 +34,9 @@
"@lingui/core": "5.1.2",
"@types/qs": "6.9.16",
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch"
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
"@opentelemetry/api": "1.9.1",
"chokidar": "^3.6.0"
},
"version": "0.2.1",
"nx": {},
@@ -204,18 +55,17 @@
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-website-new",
"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-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/2f25922f4cd5bd61e1427c57c4f8ea224e1d552c/packages/twenty-website/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
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.0.0",
"version": "2.7.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",
@@ -40,12 +39,16 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@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",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.2",
"vite": "^7.0.0",
+36 -17
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,30 +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)',
)
.option(
'--description <description>',
'Application description (skips prompt)',
)
.option(
'--skip-local-instance',
'Skip the local Twenty instance setup prompt',
'--authentication-method <method>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
)
.helpOption('-h, --help', 'Display this help message.')
.action(
async (
directory?: string,
options?: {
example?: string;
name?: string;
displayName?: string;
description?: string;
skipLocalInstance?: boolean;
url?: string;
apiUrl?: string;
authenticationMethod?: AuthenticationMethod;
},
) => {
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
@@ -52,13 +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,
serverUrl,
authenticationMethod: options?.authenticationMethod,
});
},
);
@@ -15,5 +15,23 @@
}
],
"typescript/no-explicit-any": "off"
}
},
"overrides": [
{
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["twenty-shared", "twenty-shared/*"],
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
}
]
}
]
}
}
]
}
@@ -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 }}
@@ -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,93 +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,
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;
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();
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:'),
@@ -97,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 };
@@ -175,96 +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(): Promise<boolean> {
const existingServerUrl = await detectLocalServer();
if (existingServerUrl) {
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',
});
@@ -273,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';
@@ -76,11 +77,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 () => {
@@ -168,6 +174,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,
}: {
@@ -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) {
@@ -1,6 +0,0 @@
TWENTY_API_KEY=<SET_YOUR_TWENTY_API>
DAYS_AGO=<SET_YOUR_DAYS_AGO>
DISCORD_WEBHOOK_URL=<SET_YOUR_DISCORD_WEBHOOK_URL>
FB_GRAPH_TOKEN=<SET_YOUR_FB_GRAPH_TOKEN>
WHATSAPP_RECIPIENT_PHONE_NUMBER=<SET_YOUR_WHATSAPP_RECIPIENT_PHONE_NUMBER>
SLACK_HOOK_URL=<SET_YOUR_SLACK_HOOK_URL>
@@ -1,2 +0,0 @@
.yarn/install-state.gz
.env
@@ -1,108 +0,0 @@
# Twenty CRM Activity Reporter
A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!
## Features
- 🧑‍💻 **People & Company Tracking**: Summarizes newly created people and companies
- 🎯 **Opportunity Monitoring**: Reports on new opportunities created, broken down by stage
-**Task Analytics**:
- Tracks task creation
- Calculates on-time completion rates
- Identifies team members with the most overdue tasks (the "slackers")
- 🔔 **Multi-Platform Notifications**: Send reports to Slack, Discord, and/or WhatsApp
-**Configurable Time Range**: Look back any number of days
## Prerequisites
- Node.js (v14 or higher recommended)
- TypeScript
- A [Twenty CRM](https://twenty.com) account with API access
- Optional: Slack webhook, Discord webhook, and/or WhatsApp Business API access
## Installing dependencies
```bash
# Install dependencies
yarn install
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `TWENTY_API_KEY` | ✅ Yes | Your Twenty CRM API key |
| `DAYS_AGO` | ✅ Yes | Number of days to look back for the report |
| `SLACK_HOOK_URL` | ❌ No | Slack incoming webhook URL |
| `DISCORD_WEBHOOK_URL` | ❌ No | Discord webhook URL |
| `FB_GRAPH_TOKEN` | ❌ No | Facebook Graph API token for WhatsApp |
| `WHATSAPP_RECIPIENT_PHONE_NUMBER` | ❌ No | WhatsApp recipient phone number (with country code) |
## Project Structure
```
.
├── index.ts # Main entry point
├── people-creation-summariser.ts # Summarizes people/company creation
├── opportunity-creation-summariser.ts # Summarizes opportunity creation
├── task-creation-summariser.ts # Summarizes task creation & completion
├── senders.ts # Handles sending to Slack/Discord/WhatsApp
├── utils.ts # API request utility
└── README.md
```
## How It Works
1. **Data Collection**: The bot queries the Twenty CRM API for activities within the specified time range
2. **Analysis**:
- Counts new people and companies
- Categorizes opportunities by stage
- Calculates task completion rates and identifies overdue tasks
3. **Reporting**: Formats the data into friendly messages
4. **Distribution**: Sends reports to configured platforms (Slack, Discord, WhatsApp)
## Report Format
Each report includes:
```
Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last X days:
🧑‍💻 People & Companies
- X People and Y Companies were added
🎯 Opportunities
- X Opportunities were added: Y in NEW, Z in PROPOSAL
📋 Tasks
- X Tasks were created
- Y% Tasks were completed on time
- [Name] slacked the most with Z Tasks overdue
```
## API Integration
This bot uses the [Twenty CRM REST API](https://api.twenty.com/rest/). The following endpoints are used:
- `GET /people` - Fetch people data
- `GET /opportunities` - Fetch opportunity data
- `GET /tasks` - Fetch task data
- `GET /workspaceMembers/{id}` - Fetch workspace member details
## Notes
- The "slacker" detection is lighthearted and identifies team members with the most overdue tasks
- At least one messaging platform must be configured for the bot to send reports
- The bot uses ISO date format (YYYY-MM-DD) for date filtering
- Task completion percentage only considers incomplete tasks (excludes already completed tasks from the calculation)
## Troubleshooting
**Issue**: No messages being sent
- **Solution**: Ensure at least one messaging platform is configured with valid credentials
**Issue**: API authentication errors
- **Solution**: Verify your `TWENTY_API_KEY` is correct and has necessary permissions
**Issue**: WhatsApp messages not sending
- **Solution**: Ensure both `FB_GRAPH_TOKEN` and `WHATSAPP_RECIPIENT_PHONE_NUMBER` are set correctly
## Contributing
Built with ❤️ and 🥖 by Azmat, Ali and Mike from 9dots
@@ -1,45 +0,0 @@
import { type ApplicationConfig } from 'twenty-sdk/application';
const config: ApplicationConfig = {
universalIdentifier: 'b53627f5-ca60-478c-bc43-c7ab4904e34a',
displayName: 'Activity Summary',
description:
'A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: '304b7d5d-e2bb-4444-9b04-6b3ae8b73730',
description: 'Twenty API Key',
isSecret: true,
},
DAYS_AGO: {
universalIdentifier: '040a3097-9cee-4f74-b957-c2f9bf636c3f',
description:
'How far back into the past we want to summarise defaults to the past 7 days',
value: '7',
isSecret: false,
},
SLACK_HOOK_URL: {
universalIdentifier: 'fd16e370-934c-4267-83b4-7d88259bf7e1',
description: 'Slack hook URL for sending message to channel',
isSecret: true,
},
DISCORD_WEBHOOK_URL: {
universalIdentifier: 'f3741075-d525-4988-ba42-55d519c6fd76',
description:
'Discord webhook URL for sending message to channel of a server',
isSecret: true,
},
FB_GRAPH_TOKEN: {
universalIdentifier: 'fb907f49-74ac-4aa5-ba45-cfc9250ecc44',
description: 'For Facebook auth',
isSecret: true,
},
WHATSAPP_RECIPIENT_PHONE_NUMBER: {
universalIdentifier: 'c856ee5d-44bf-42f4-9a39-2553a94af518',
description: 'Phone number for receiving WhatsApp message',
isSecret: true,
},
},
};
export default config;
@@ -1,17 +0,0 @@
{
"name": "activity-summary",
"version": "0.0.1",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"packageManager": "yarn@4.9.2",
"dependencies": {
"twenty-sdk": "0.0.3"
},
"devDependencies": {
"@types/node": "^24.7.2"
}
}
@@ -1,80 +0,0 @@
import { summariseOpportunityCreation } from './opportunity-creation-summariser';
import { summarisePeopleCreation } from './people-creation-summariser';
import { sendToDiscord, sendToSlack, sendToWhatsApp } from './senders';
import { summariseTaskCreation } from './task-creation-summariser';
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
export const main = async (): Promise<object> => {
let date: string | Date = new Date();
date.setDate(new Date().getDate() - Number(process.env.DAYS_AGO));
date = date.toISOString().substring(0, 10);
const peopleCreationSummary = await summarisePeopleCreation(date);
const opportunityCreationSummary = await summariseOpportunityCreation(date);
const taskCreationSummary = await summariseTaskCreation(date);
let body = {
daysAgo: Number(process.env.DAYS_AGO),
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
discord: {},
whatsapp: {},
slack: {},
};
if (process.env.SLACK_HOOK_URL) {
const slackBody = await sendToSlack({
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
});
body = {
...body,
slack: slackBody,
};
}
if (process.env.DISCORD_WEBHOOK_URL) {
const discordBody = await sendToDiscord({
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
});
body = {
...body,
discord: discordBody,
};
}
if (
process.env.FB_GRAPH_TOKEN &&
process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER
) {
const whatsappBody = await sendToWhatsApp({
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
});
body = {
...body,
whatsapp: whatsappBody,
};
}
return body;
};
export const config: ServerlessFunctionConfig = {
universalIdentifier: 'c5b0e3f7-cbbd-4bd6-b01c-150d52cf2ce9',
name: 'summarise-and-send',
triggers: [
{
universalIdentifier: '36e1c4c7-8664-4d6d-a88f-ac56f1bd0651',
type: 'cron',
pattern: '0 9 * * *',
},
],
};
@@ -1,28 +0,0 @@
import { request } from "./utils"
type Opportunity = {
stage: 'NEW' | 'PROPOSAL'
}
export const summariseOpportunityCreation = async (date: string) => {
const { opportunities }: { opportunities: Opportunity[] } = await request(
`opportunities?filter=createdAt[gte]:${date}`,
)
if (opportunities.length === 0) {
return `- No Opportunities were added`
}
const stageSummary = Object.entries(
opportunities.reduce((hash: Record<string,number>, opportunity) => {
if (!hash[opportunity.stage]) {
hash[opportunity.stage] = 0
}
hash[opportunity.stage] += 1
return hash
}, {})
).map(([stage, value]) => `${value} in ${stage}`).join(', ')
return `- ${opportunities.length} Opportunities were added: ${stageSummary}`
}
@@ -1,38 +0,0 @@
import { request } from "./utils"
type Person = {
companyId: string
company: Company
}
type Company = {
id: string
accountOwnerId: string
}
export const summarisePeopleCreation = async (date: string) => {
const { people }: { people: Person[] } = await request(
`people?depth=1&filter=createdAt[gte]:${date}`,
)
if (people.length === 0) {
return '- No People were added'
}
let createdForCompanies: Record<string, Company> = {}
let numberOfAccountOwnerlessCompanies = 0
for (const person of people) {
const isCompanyTracked = createdForCompanies[person.companyId]
if (person.companyId && !isCompanyTracked) {
createdForCompanies[person.company.id] = person.company
if (!person?.company?.accountOwnerId) {
numberOfAccountOwnerlessCompanies += 1
}
}
}
return `- ${people.length} People were added for ${Object.keys(createdForCompanies).length} Companies
- Out of those, ${numberOfAccountOwnerlessCompanies} Companies don't have account owners yet`
}
@@ -1,172 +0,0 @@
export const sendToSlack = async (params: {
peopleCreationSummary: string;
opportunityCreationSummary: string;
taskCreationSummary: string;
}) => {
const {
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
} = params;
const slackMessage = {
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days`,
emoji: true,
},
},
{
type: 'header',
text: {
type: 'plain_text',
text: '🧑‍💻 People & Companies',
emoji: true,
},
},
{
type: 'section',
text: {
type: 'plain_text',
text: peopleCreationSummary,
emoji: true,
},
},
{
type: 'header',
text: {
type: 'plain_text',
text: '🎯 Opportunities',
emoji: true,
},
},
{
type: 'section',
text: {
type: 'plain_text',
text: opportunityCreationSummary,
emoji: true,
},
},
{
type: 'header',
text: {
type: 'plain_text',
text: '📋 Tasks',
emoji: true,
},
},
{
type: 'section',
text: {
type: 'plain_text',
text: taskCreationSummary,
emoji: true,
},
},
],
};
const response = await fetch(process.env.SLACK_HOOK_URL ?? '', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slackMessage),
});
return {
formattedMessage: slackMessage,
webhookStatus: response.status,
};
};
export const sendToDiscord = async (params: {
peopleCreationSummary: string;
opportunityCreationSummary: string;
taskCreationSummary: string;
}) => {
const {
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
} = params;
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
**🧑‍💻 People & Companies**
${peopleCreationSummary}
**🎯 Opportunities**
${opportunityCreationSummary}
**📋 Tasks**
${taskCreationSummary}`;
const body = {
username: 'Twenty Bot',
content: formattedMessage,
};
const response = await fetch(process.env.DISCORD_WEBHOOK_URL ?? '', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return {
formattedMessage,
webhookStatus: response.status,
};
};
export const sendToWhatsApp = async (params: {
peopleCreationSummary: string;
opportunityCreationSummary: string;
taskCreationSummary: string;
}): Promise<object> => {
const {
peopleCreationSummary,
opportunityCreationSummary,
taskCreationSummary,
} = params;
const formattedMessage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
*🧑‍💻 People & Companies*
${peopleCreationSummary}
*🎯 Opportunities*
${opportunityCreationSummary}
*📋 Tasks*
${taskCreationSummary}`;
const response = await fetch(
'https://graph.facebook.com/v22.0/828771160324576/messages',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.FB_GRAPH_TOKEN}`,
},
body: JSON.stringify({
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER,
type: 'text',
text: {
preview_url: true,
body: formattedMessage,
},
}),
},
);
const responseBody = await response.json();
return {
formattedMessage,
webhookStatus: response.status,
webhookResponse: responseBody,
};
};
@@ -1,89 +0,0 @@
import { request } from "./utils"
type Task = {
status: string
dueAt: string
createdBy: {
workspaceMemberId: string
}
}
export const summariseTaskCreation = async (date: string) => {
const { tasks }: { tasks: Task[] } = await request(
`tasks?filter=createdAt[gte]:${date}`,
)
if (tasks.length === 0) {
return '- No Tasks were added'
}
const presentDateISOString = new Date().toISOString()
const workspaceMemberIdsDueDateCounter: Record<string, number> = {}
let workspaceMembers = []
tasks.forEach((task) => {
if (task.status === 'DONE') {
return
}
if (presentDateISOString >= task.dueAt) {
if (!workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId]) {
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] = 0
}
workspaceMemberIdsDueDateCounter[task.createdBy.workspaceMemberId] += 1
}
})
for (const { userId, count } of findMaxIncompleteKeys(workspaceMemberIdsDueDateCounter)) {
const data = await request(`workspaceMembers/${userId}`)
if (data.workspaceMember) {
workspaceMembers.push({
...data.workspaceMember,
slackCount: count
})
}
}
let slackerMessage = workspaceMembers.length > 0 ? workspaceMembers.reduce((text, member, index) => {
if (index === 0 && workspaceMembers.length === 1) {
return `${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`
}
if (index !== workspaceMembers.length - 1) {
return text.concat(`, ${member.name.firstName}`)
}
if (index === workspaceMembers.length - 1) {
return text.concat(`, and ${member.name.firstName} slacked the most with ${member.slackCount} Tasks overdue`)
}
}, '') : 'No one was caught slacking!'
const tasksCompletedOnTime = tasks.filter(
task => task.status === 'DONE' && task.dueAt >= presentDateISOString
)
const taskCompletionPercentage = tasksCompletedOnTime.length > 0
? (tasksCompletedOnTime.length / tasks.length) * 100
: NaN
const taskCompletionMessage = isNaN(taskCompletionPercentage)
? 'No completed Tasks yet'
: `${taskCompletionPercentage.toFixed(2)}% of Tasks were completed on time`
return `- ${tasks.length} Tasks were created
- ${taskCompletionMessage}
- ${slackerMessage}`
}
/**
* @description This is generated by AI
*/
const findMaxIncompleteKeys = (workspaceMemberIdsDueDateCounter: Record<string,number>) => {
// Get the maximum incomplete count
const max = Math.max(...Object.values(workspaceMemberIdsDueDateCounter));
// Filter keys that match the maximum value
return Object.entries(workspaceMemberIdsDueDateCounter)
.filter(([_, count]) => count === max)
.map(([userId, count]) => ({ userId, count }))
};
@@ -1,13 +0,0 @@
export const request = async (route: string, authToken?: string) => {
const response = await fetch(
`https://api.twenty.com/rest/${route}`,
{
headers: {
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`
},
}
)
const json = await response.json()
return json.data
}
@@ -1,26 +0,0 @@
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"outDir": "./dist",
"rootDir": ".",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"strict": true,
"target": "es2018",
"module": "esnext",
"lib": ["es2020", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
@@ -1,38 +0,0 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"@types/node@npm:^24.7.2":
version: 24.9.2
resolution: "@types/node@npm:24.9.2"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/7905d43f65cee72ef475fe76316e10bbf6ac5d08a7f0f6c38f2b6285d7ca3009e8fcafc8f8a1d2bf3f55889c9c278dbb203a9081fd0cf2d6d62161703924c6fa
languageName: node
linkType: hard
"activity-summary@workspace:.":
version: 0.0.0-use.local
resolution: "activity-summary@workspace:."
dependencies:
"@types/node": "npm:^24.7.2"
twenty-sdk: "npm:0.0.3"
languageName: unknown
linkType: soft
"twenty-sdk@npm:0.0.3":
version: 0.0.3
resolution: "twenty-sdk@npm:0.0.3"
checksum: 10c0/0a3c85c27edb22fb50f7eb0da4f9770e85729fce05e9e0118ad0cdfc36e42425c93340a6cd1c276daf30aeeaa612db0cd905831c0a8287a31bff3da5be9b0562
languageName: node
linkType: hard
"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard
@@ -1,19 +0,0 @@
# Set environment values for your application here.
# Use the format: KEY=value
#
# These variables are automatically loaded when running your serverless functions.
# You can access them directly in your code using:
# const myValue = process.env.KEY;
#
# To make these variables available to your application,
# add them to package.json "env" key. This "env" key defines all
# environment variables that will be provided to your serverless
# functions at runtime.
#
# Example:
# API_TOKEN=your-api-token
# TIMEOUT_MS=3000
TWENTY_API_URL=
TWENTY_API_KEY=
OPENAI_API_KEY=
@@ -1,2 +0,0 @@
.yarn/install-state.gz
.env
@@ -1,159 +0,0 @@
# AI Meeting Transcript
Automatically process meeting transcripts to extract insights, action items, and follow-ups using AI.
## Features
- **Automatic Transcript Processing**: Receives meeting transcripts via webhook from Granola or similar transcription tools
- **AI-Powered Analysis**: Uses OpenAI to extract:
- Meeting summary
- Key discussion points
- Action items with assignees and due dates
- Commitments made by participants
- **Rich Note Creation**: Creates formatted notes in Twenty with summary and key points
- **Task Generation**: Automatically creates tasks for action items and commitments, linked to the meeting note
## Requirements
- `twenty-cli` - Install globally: `npm install -g twenty-cli`
- `apiKey` - Go to `https://twenty.com/settings/api-webhooks` to generate one
- `OpenAI API Key` - Get your API key from [OpenAI](https://platform.openai.com/api-keys)
## Installation
1. Copy the environment file:
```bash
cp .env.example .env
```
2. Edit `.env` and replace the placeholders:
- `<SET_YOUR_TWENTY_API>` with your Twenty API key
- `<SET_YOUR_OPENAI_API_KEY>` with your OpenAI API key
3. Install dependencies:
```bash
yarn install
```
4. Sync the app to your Twenty workspace:
```bash
twenty auth login
twenty app sync
```
## Configuration
After syncing, configure the environment variables in your Twenty workspace:
1. Go to Settings → Apps → AI Meeting Transcript
2. Set the following environment variables:
- `TWENTY_API_KEY` - Your Twenty API key
- `TWENTY_API_URL` - Your Twenty instance URL (e.g., `https://api.twenty.com` or `http://localhost:3000` for local development)
- `OPENAI_API_KEY` - Your OpenAI API key
**Important**: `TWENTY_API_URL` is required and must be set to your Twenty instance URL. For local development, use `http://localhost:3000`. For production, use your actual Twenty instance URL.
## Usage
### Webhook Endpoint
The app exposes a public serverless route trigger.
```
POST /s/webhook/transcript
```
Examples:
- Local: `POST http://localhost:3000/s/webhook/transcript`
- Hosted: `POST https://your-twenty-instance.com/s/webhook/transcript`
### Webhook Payload Format
Send a POST request with the following JSON structure:
```json
{
"transcript": "Full meeting transcript text here...",
"meetingTitle": "Q4 Planning Meeting",
"meetingDate": "2024-01-15",
"participants": ["John Doe", "Jane Smith"],
"metadata": {
"duration": "45 minutes",
"location": "Conference Room A"
}
}
```
**Required Fields:**
- `transcript` (string): The full meeting transcript text
**Optional Fields:**
- `meetingTitle` (string): Title of the meeting
- `meetingDate` (string): Date of the meeting (ISO format or readable date)
- `participants` (string[]): List of meeting participants
- `metadata` (object): Additional metadata about the meeting
### Example Webhook Call
```bash
curl -X POST https://your-twenty-instance.com/s/webhook/transcript \
-H "Content-Type: application/json" \
-d '{
"transcript": "John: Let'\''s start the meeting. Today we need to discuss Q4 goals. Jane: I agree. We should focus on customer retention. John: Great point. Can you prepare a report by Friday? Jane: Yes, I will have it ready.",
"meetingTitle": "Q4 Planning Meeting",
"meetingDate": "2024-01-15"
}'
```
### What Happens
1. **Transcript Analysis**: The transcript is sent to OpenAI for analysis
2. **Note Creation**: A formatted note is created in Twenty with:
- Meeting summary
- Key discussion points
- Reference to the transcript source
3. **Task Creation**: Tasks are automatically created for:
- Each action item identified
- Each commitment made by participants
- Tasks include a reference to the meeting note ID in their description
## Development
Run dev mode to see application updates on your workspace instantly:
```bash
twenty app dev
```
## Integration with Granola
To integrate with Granola or similar transcription tools:
1. Set up a webhook in your transcription service
2. Configure it to POST to: `https://your-twenty-instance.com/s/webhook/transcript`
3. Map the transcription service's payload format to the expected format above
### Granola Webhook Setup
If using Granola, configure the webhook to send:
- `transcript` field with the transcript text
- Optionally include meeting metadata fields
## API Response
The webhook returns a JSON response:
```json
{
"success": true,
"noteId": "uuid-of-created-note",
"taskIds": ["uuid-of-task-1", "uuid-of-task-2"],
"summary": {
"noteCreated": true,
"tasksCreated": 2,
"actionItemsProcessed": 1,
"commitmentsProcessed": 1
}
}
```
@@ -1,30 +0,0 @@
import { type ApplicationConfig } from 'twenty-sdk/application';
const config: ApplicationConfig = {
universalIdentifier: '028754f1-3235-43b9-9427-fa6a62dbd473',
displayName: 'AI Meeting Transcript',
description:
'Automatically process meeting transcripts to extract insights, action items, and follow-ups',
applicationVariables: {
TWENTY_API_KEY: {
universalIdentifier: '1359d05c-4947-4673-809f-abd55bede365',
isSecret: true,
value: '',
description: 'Twenty API key',
},
TWENTY_API_URL: {
universalIdentifier: 'dbe83355-b574-445c-92c0-5c2b94a61ddb',
isSecret: true,
value: '',
description: 'Twenty API URL',
},
OPENAI_API_KEY: {
universalIdentifier: '9559470d-15eb-4bc2-9cbc-3bc5c869d1fd',
isSecret: true,
value: '',
description: 'OpenAI API key for transcript analysis',
},
},
};
export default config;

Some files were not shown because too many files have changed in this diff Show More