When syncing an app with relation multiple times, it would fail because
the relation fields could not be identified due to universalIdentifier
being overwritten at field cretion
- Introduces a SerializedEventData type that captures only serializable
properties from DOM/React events
- Updates `wrapEventHandler` in the host component registry to serialize
native events via serializeEvent() before passing them across the worker
boundary via postMessage, avoiding circular references and non-cloneable
DOM nodes
- Updates generated remote element event signatures to use
RemoteEvent<SerializedEventData> instead of bare RemoteEvent, giving
front-component authors typed access to event details
## Summary
- Fixes login failing with `Error: URL is required` when captcha (Google
reCAPTCHA or Turnstile) is enabled
- The secure axios adapter (`getSecureAxiosAdapter`) checked
`config.url` before `baseURL` resolution. In Axios, `baseURL + url`
combination happens inside the default HTTP adapter, not before custom
adapters are called. When captcha drivers configure a `baseURL` and call
`.post('', data)`, the empty string url was incorrectly treated as
missing.
- The adapter now resolves `baseURL` + `url` itself before validation,
matching the default Axios HTTP adapter behavior
## Test plan
- [x] Existing unit tests (23 tests) all pass
- [ ] Verify login works with captcha enabled (`CAPTCHA_DRIVER` set to
`google-recaptcha` or `turnstile`)
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Context
Ability to serve frontend component
See:
```typescript
curl -i 'http://localhost:3000/rest/front-components/35063b3f-bc4c-4358-8966-7762677802a3' \
--header 'Authorization: Bearer eyJhb...'
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/javascript
Date: Mon, 09 Feb 2026 10:28:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
// react-globals:react/jsx-runtime
var jsx = globalThis.jsx;
var jsxs = globalThis.jsxs;
var Fragment = globalThis.React.Fragment;
// src/front-components/test.tsx
var RemoteComponents = globalThis.RemoteComponents;
var Component = () => {
return /* @__PURE__ */ jsxs(RemoteComponents.HtmlDiv, { style: { padding: "20px", fontFamily: "sans-serif" }, children: [
/* @__PURE__ */ jsx(RemoteComponents.HtmlH1, { children: "My new component!" }),
/* @__PURE__ */ jsx(RemoteComponents.HtmlP, { children: "This is your front component: test" })
] });
};
var test_default = globalThis.jsx(Component, {});
export {
test_default as default
};
//# sourceMappingURL=test.mjs.map
```
readFile_v2 returns a Node.js Stream object (Readable). Here we are
using stream pipeline which connects the readable stream (file) to the
writable stream (HTTP response) which efficiently streams the file
content directly to the HTTP response without loading the entire file
into memory. (in chunks, handling backpressure and closing the
connection when the file is fully sent)
## Summary
- Wrap `AddFileEntityUniqueConstraint` migration in a SAVEPOINT
try-catch so it doesn't break when duplicate `(workspaceId,
applicationId, path)` rows exist in `core.file`
- Extend `DeleteFileRecordsCommand` upgrade command to add the unique
constraint after cleaning up file records
## PR Description
This PR:
- Introduces a `FrontComponentHostCommunicationApi`, which allows us to
pass functions to be executed from the worker
- Exposes a navigate function from the host to the front component
remote workers, enabling SDK components to trigger in-app navigation
- Gets rid of `useSyncExternalStore` and makes the execution context
reactive without it
- Refactors and improves the `esbuild` plugin system
## Video
https://github.com/user-attachments/assets/7b26a1c2-f85f-4898-a71d-f60c70e61711
⚠️ **AI-generated PR — not ready for review** ⚠️
cc @FelixMalfait
---
## Changes
### System prompt improvements
- Explicit skill-before-tools workflow to prevent the model from calling
tools without loading the matching skill first
- Data efficiency guidance (default small limits, use filters)
- Pluralized `load_skill` → `load_skills` for consistency with
`load_tools`
### Token usage reduction
- Output serialization layer: strips null/undefined/empty values from
tool results
- Lowered default `find_*` limit from 100 → 10, max from 1000 → 100
### System object tool generation
- System objects (calendar events, messages, etc.) now generate AI tools
- Only workflow-related and favorite-related objects are excluded
### Context window display fix
- **Bug**: UI compared cumulative tokens (sum of all turns) against
single-request context window → showed 100% after a few turns
- **Fix**: Track `conversationSize` (last step's `inputTokens`) which
represents the actual conversation history size sent to the model
- New `conversationSize` column on thread entity with migration
### Workspace AI instructions
- Support for custom workspace-level AI instructions
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## Summary
- Normalize all file paths with `path.resolve` instead of `join` to
properly handle `..` segments in file path inputs
- Add `assertPathIsWithinStorage` guard on all write, delete, move,
copy, and existence-check operations
- Introduce `ACCESS_DENIED` exception code with i18n-ready user-friendly
message
- Read path already had realpath-based validation; updated its error
code to `ACCESS_DENIED` for consistency
## Test plan
- [x] Typecheck passes
- [x] Lint passes
- [x] Manual: verify file upload/download still works with valid paths
- [x] Manual: verify `../` in file paths is rejected with ACCESS_DENIED
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
Fix#17711
### Reproduce steps
1. import this xlsx file
[test-import.xlsx](https://github.com/user-attachments/files/25156642/test-import.xlsx)
2. import company worksheet at first and then import people worksheet
3. you will see an error <img width="324" height="101"
alt="Snipaste_2026-02-07_19-22-04"
src="https://github.com/user-attachments/assets/bf2703da-a57a-4795-805b-6ddcc689c621"
/>
And I found neither the frontend nor the backend applies lowercase
normalization to the query value. Although the standard UI input always
displays company links in lowercase, user might mixed the case in their
xlsx/csv bulk import. So I did a simple check in frontend.
### Additional findings:
1. Email has the same case sensitivity issue when opportunities import.
You can test same as in test-import.xlsx file (I created a opportunities
worksheet. It will have same issue: <img width="330" height="101"
alt="email error"
src="https://github.com/user-attachments/assets/db36b19b-2abe-4c61-a661-f8d1eb357a5e"
/>
2. API also has: I also tested via the GraphQL API Playground and
confirmed that createOnePerson with a mixed-case URL fails to find an
existing company. <img width="1419" height="513"
alt="Snipaste_2026-02-07_23-33-56"
src="https://github.com/user-attachments/assets/c189f1fd-9793-42d5-a1a9-616cffd2592e"
/>
### Approach:
1. Only change it in frontend, and I will add email check later; (my
current commit)
2. Or backend: Normalize values in computeUniqueConstraintCondition in
twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts
(which handles connect.where queries, and this will cover xlsx import
and api import: createone, createmany, updatemany, updateone. And much
better for future extension.
(Personally, I'd prefer the backend approach. But I'd love to hear your
thoughts on which approach you'd prefer, and whether my analysis is on
the right track.
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
## Summary
- Removes unreachable dead code in `verifyJwtToken` — a legacy API key
verification block where the condition (`!payload.type && type ===
API_KEY`) was logically impossible. Also removes the now-unused
`isLegacyApiKey` parameter and `generateAppSecretLegacy` method.
- Adds explicit token type validation after JWT decode in
`verifyLoginToken`, `verifyRefreshToken`, and `verifyTransientToken`.
Each function now rejects tokens whose `type` field doesn't match what's
expected (defense-in-depth — the HMAC secret already binds the type, but
this makes the contract explicit).
## Test plan
- [x] Updated existing specs for login-token, refresh-token, renew-token
services — all 16 tests passing
- [x] Lint clean
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## Summary
- Deletes `use-throttler.ts` — an envelop plugin that was never
registered in the GraphQL config plugin chain (dead code since
introduction)
- Removes the `graphql-rate-limit` dependency from `package.json` (only
consumer was the deleted file)
API rate limiting continues to work via `ThrottlerService` in the query
runner layer (for API key auth). Global coverage should be handled at
the infrastructure level (Cloudflare).
## Test plan
- [x] Confirmed no imports of `useThrottler`, `ThrottlerPluginOptions`,
or `UnauthenticatedError` from this file exist anywhere in the codebase
- [x] `graphql-rate-limit` has no other consumers
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Renames `REFRESH_TOKEN_COOL_DOWN` to
`REFRESH_TOKEN_REUSE_GRACE_PERIOD` — the old name was misleading and
suggested a security mechanism rather than what it actually is: a grace
period for concurrent refresh token use (e.g. two browser tabs
refreshing simultaneously).
- Makes the token revocation in `renew-token.service.ts` conditional
(`revokedAt: IsNull()`), so if the token was already revoked by a
concurrent request, the original `revokedAt` timestamp is preserved and
the grace window stays anchored.
- Updates comments and config description to clarify intent.
## Test plan
- [x] Existing unit tests updated and passing
(`refresh-token.service.spec.ts`, `renew-token.service.spec.ts`)
- [x] Lint clean
Made with [Cursor](https://cursor.com)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
- Validates that the timezone parameter in group-by date expressions is
a recognized IANA timezone
- Adds SQL string literal escaping as a defense-in-depth measure for the
timezone value interpolated into SQL expressions
- Moves the `IANA_TIME_ZONES` constant to `twenty-shared` so it can be
reused across frontend and server packages
- Adds `INVALID_TIMEZONE` error code mapped to 400 Bad Request in both
GraphQL and REST API exception handlers
## Test plan
- [x] Unit tests for `validateIanaTimeZone` (valid IANA zones, fixed
offsets, rejects invalid strings)
- [x] Unit tests for `escapeSqlStringLiteral`
- [x] Integration tests for `getGroupByExpression` covering timezone
validation and granularity handling
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
## Context
- Add missing conditional display
- Set default tab as null for most of the page layout, letting the FE
handle that
- Fix duplicate "Note" widget in both task and note pages
- Simplify typing, removing redundant layoutName
# Introduction
Requiring the spreaded `__universal` record that aggregates all the
universal identifier ( relations fk and aggregators ) of an entity to
its root
It's blockin for https://github.com/twentyhq/twenty/pull/17687 to be
finalized because if we don't we would have to migrated all related
entities at once in order for them to always have the universal
properties
## `resolveEntityRelationUniversalIdentifiers`
Introduced `resolveEntityRelationUniversalIdentifiers` a centralized
utility that resolves foreign key IDs to universal identifiers using
ALL_METADATA_RELATIONS metadata. It provides strict typing for both
input (foreign keys) and output (universal identifiers), with
nullability dynamically inferred from entity relation types.
Strictly and dynamically typed for both output and input
To do so added a new type and const/runtime grain to
ALL_METADATA_RELATIONS `isNullable`to many-to-one entries, derived from
the entity relation property types. And fixed incorrectly typed typeorm
entities
### Usage
```ts
const {
availabilityObjectMetadataUniversalIdentifier,
frontComponentUniversalIdentifier,
} = resolveEntityRelationUniversalIdentifiers({
metadataName: 'commandMenuItem',
foreignKeyValues: {
availabilityObjectMetadataId:
createCommandMenuItemInput.availabilityObjectMetadataId,
frontComponentId: createCommandMenuItemInput.frontComponentId,
},
flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps },
});
```
On iterator creation, we get an error "Bad configuration". Looks like a
race condition with generation that happens before apollo cache gets
updated. Adding defensive checks
**What this PR does:**
- Migrates existing `Favorite` and `FavoriteFolder` entities to the new
`NavigationMenuItem` structure
- Preserves user-level vs workspace-level ownership
- Preserves folder structure, positions, and relationships
- Handles both view-based favorites (linked to views) and record-based
favorites (linked to records)
- Soft-deletes original favorites and folders after successful migration
- Enables the `IS_NAVIGATION_MENU_ITEM_ENABLED` feature flag
post-migration
- Skips migration if the feature flag is already enabled
- Idempotent: checks for existing navigation menu items to prevent
duplicates
**Migration flow:**
1. Migrate favorite folders first (creates folder mapping)
2. Migrate favorites
3. Soft-delete migrated favorites and folders
4. Enable feature flag
**What's next:**
- After all workspaces are migrated and the navigation menu item feature
is fully rolled out, we can:
- Remove the old `Favorite` and `FavoriteFolder` entities and related
code
- Remove the feature flag check and make navigation menu items the
default
- Clean up any deprecated favorites-related code paths in the frontend
---------
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
## Context
I've seen errors in twenty-emails build where I18n from @lingui/core was
resolved to two different declaration files
```typescript
src/components/BaseEmail.tsx:20:19 - error TS2719: Type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n' is not assignable to type 'import("/Users/weiko/Projects/twenty/node_modules/@lingui/core/dist/index").I18n'. Two different types with this name exist, but they are unrelated.
Types have separate declarations of a private property '_locale'.
20 <I18nProvider i18n={i18nInstance}>
~~~~
node_modules/@lingui/react/dist/shared/react.b2b749a9.d.ts:42:5
42 i18n: I18n;
~~~~
The expected type comes from property 'i18n' which is declared here on type 'IntrinsicAttributes & Omit<I18nContext, "_"> & { children?: ReactNode; }'
```
Seems to be related to https://github.com/twentyhq/twenty/pull/17380
The tsconfig simplification changed how vite plugin resolves types during build. With the inherited moduleResolution: "node", the plugin and source code resolved @lingui/react's types differently, creating two incompatible I18n types from the same package.
## Fix
Add moduleResolution: "bundler" to packages/twenty-emails/tsconfig.json, aligning with twenty-front, twenty-ui, twenty-shared should fix the issue
## Summary
When a custom object is renamed after creation, the relation fields on
`noteTarget`, `taskTarget`, `attachment`, and `timelineActivity` keep
their old names but point to the renamed object.
For example:
- User creates custom object `solution`
- System creates field `solution` on NoteTarget → pointing to Solution
- User renames object from `solution` to `productCatalog`
- Field name stays as `solution` but points to `productCatalog`
- Migration runs: `solution` → `targetSolution`
- Now `targetSolution` points to `productCatalog` ❌
This causes the morph name computation to fail:
- `getMorphNameFromMorphFieldMetadataName("targetSolution",
"productCatalog")`
- Tries to remove `ProductCatalog` from `targetSolution` → no match
- Name stays as `targetSolution` instead of becoming `target`
- Frontend computes: `targetSolution` + `Company` =
`targetSolutionCompany` 💥
## The Fix
This command finds and fixes all such mismatches by:
1. Finding MORPH_RELATION fields where `field.name !=
target${capitalize(targetObject.nameSingular)}`
2. Renaming the column in the workspace schema
3. Updating the field metadata (name + joinColumnName in settings)
4. Invalidating the cache
## Usage
**Dry run (to see what would be fixed):**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id> --dry-run
```
**Fix a specific workspace:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names -- --workspace-id <id>
```
**Fix all workspaces:**
```bash
npx nx run twenty-server:command upgrade:1-17:fix-morph-relation-field-names
```
## SQL to find affected workspaces
```sql
SELECT
fm."workspaceId",
COUNT(*) AS mismatched_fields
FROM core."fieldMetadata" fm
JOIN core."objectMetadata" target_om ON fm."relationTargetObjectMetadataId" = target_om.id
JOIN core."objectMetadata" source_om ON fm."objectMetadataId" = source_om.id
WHERE fm.type = 'MORPH_RELATION'
AND fm.name LIKE 'target%'
AND source_om."nameSingular" IN ('noteTarget', 'taskTarget', 'attachment', 'timelineActivity')
GROUP BY fm."workspaceId"
HAVING COUNT(*) FILTER (
WHERE fm.name != 'target' || initcap(replace(target_om."nameSingular", '_', ''))
) > 0;
```
- Add FILES field on attachment
- Adapt Attachment logic in front to use new resolver/controller
- Update files-field logic to infer applicationId from fieldMetadataId +
ask for fieldMetadataId in upload resolver
- Design update
To do in next PR :
- Adapt activity files logic
## Summary
Updates the avatar URLs for seeded founders to use properly named images
instead of generic numbered placeholder images.
## Changes
Updates `prefill-people.ts` to reference new image paths in
`twentyhq/placeholder-images/founders/`:
| Person | Company | New Image Path |
|--------|---------|----------------|
| Brian Chesky | Airbnb | `founders/brian-chesky.jpg` |
| Dario Amodei | Anthropic | `founders/dario-amodei.jpg` |
| Patrick Collison | Stripe | `founders/patrick-collison.jpg` |
| Dylan Field | Figma | `founders/dylan-field.jpg` |
| Ivan Zhao | Notion | `founders/ivan-zhao.jpg` |
## Related
Requires a corresponding PR on `twentyhq/placeholder-images` to add the
actual founder headshot images to the `founders/` directory.
## Why
The previous placeholder images were generic numbered images that didn't
represent the actual people. This creates a single source of truth for
founder images that all workspaces can reference.
We generate Field widgets for relations on-the-fly, when a record page
layout is first requested by the user. When the user changed their data
model and then returned to a record page layout, the Field widgets
weren't updated. **This PR ensures Field widgets are recomputed when
relations change.**
Future subjects:
- Now that we will fetch the configuration from the backend and start
storing updates, we will have to think about how we deal with these
generated relation Field widgets.
## Before
https://github.com/user-attachments/assets/99d53b19-b231-435f-b14f-4473ba269ad2
## After
https://github.com/user-attachments/assets/357d956d-8b3b-448c-a983-82569a7dd0a0
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Issue 1: no info to debug cron trigger. Stop catching exception + using
logs instead of throwing for now
Issue 2: sentry often send timeouts errors for workflow crons. Probably
not real ones, it sends it if the job takes more than 5 minutes to run.
To fix, on each workflow cron we do:
- loop over active workspaces
- perform a query check that workspace is relevant, using count for
performances
- send a job if relevant
## Summary
Three fixes in this PR:
### 1. Migration commands now set `isActive = true` and use generic
'Target' label
When converting relation fields to `MORPH_RELATION` type, the migration
commands now:
- Set `isActive = true` to prevent inactive fields from being selected
as the representative morph field
- Update all field labels to generic **'Target'** instead of keeping
individual labels like 'Company', 'Person'
This ensures the UI shows a coherent label ('Target') alongside 'X
Objects' for the type.
**Files changed:**
- `1-17-migrate-note-target-to-morph-relations.command.ts`
- `1-17-migrate-task-target-to-morph-relations.command.ts`
### 2. Added system fields/relations toggles in Settings
The fields and relations tables in Settings > Data Model were filtering
out system fields with no way to view them. Added a "System fields" /
"System relations" toggle (visible in advanced mode) to allow viewing
these fields.
**Files changed:**
- `SettingsObjectFieldTable.tsx`
- `SettingsObjectRelationsTable.tsx`
This matches the existing behavior on the Objects table which already
has a "System objects" toggle.