## Summary
- Adds an `authType` field (`'api_key' | 'access_key' | 'iam_role'`) to
AI provider config
- Providers like Amazon Bedrock that authenticate via IAM role (instance
profile) can now be registered without explicit API keys or access keys
- Backend: `isProviderConfigured()` checks `apiKey || accessKeyId ||
authType`
- Frontend admin panel: shows green "Configured" badge and "IAM role"
description for providers with `authType: "iam_role"`
- Provider detail page shows "IAM role (instance profile)" in the
credentials row
## Companion PR
- twentyhq/twenty-infra#528 — patches `authType: "iam_role"` into
dev/staging Bedrock catalogs
## Changed files
- **Backend**: new `AiProviderAuthType` type, `isProviderConfigured`
util, updated registry + resolver
- **Frontend**: new `AiProviderAuthType` type, updated provider list
card + detail page
## Test plan
- [ ] Deploy with a Bedrock catalog that includes `"authType":
"iam_role"` — verify Bedrock shows "Configured" in admin AI panel
- [ ] Verify OpenAI/Anthropic with `apiKey` still show "Configured"
- [ ] Verify a provider with no credentials and no `authType` still
shows "No credentials"
Made with [Cursor](https://cursor.com)
This PR solves multiple problems :
- An infinite loop that was happening on board with initial and fetch
more queries
- Warning messages that get triggered when we drag and drop multiple
times in a row
- An attempt to fix an existing error in Sentry, that couldn't be
reproduced for now.
Fixes https://github.com/twentyhq/twenty/issues/18949
## Problem
- Creating a record from filters with DATE_TIME fields produced empty
objects instead of dates — `isPlainObject` from `twenty-shared` treats
`Date` instances as plain objects, so `mergeCompositeValues` spread them
into `{}`
- `buildRecordInputFromFilter` applied composite merge logic to all
field types indiscriminately, including primitives, dates, and strings
- DATE_TIME "is before" filters produced exact boundary values instead
of subtracting a minute
## Fix
- Composite and non-composite fields are now handled in separate
branches — `buildRecordInputFromFilter` uses `isCompositeFieldType` to
decide whether to merge or assign directly
- `mergeCompositeValues` extracted to its own file with a properly typed
signature (`Record<string, unknown>`) — no more runtime type guessing
- DATE_TIME fields with `IS_BEFORE` operand subtract one minute using
`subMinutes` from `date-fns`
- 13 unit tests for `mergeCompositeValues` covering all composite field
types (currency, address, full name, links, emails, phones) and
successive sub-field accumulation
<img width="1946" height="792" alt="image"
src="https://github.com/user-attachments/assets/d0abf62b-85d4-4f5f-b6dc-f2cc1c691f7e"
/>
Avoid re-exporting twenty-ui icons bundle that are massive ~4MB
As discussed with @charlesBochet the problem should rather be treated at
twenty-ui level at some point, that's quite a quick workaround in order
to avoid overloading the twenty-sdk build size
When time comes, where twenty-ui is mature enough to get published we
will work on its bundle size
The Kanban view builds a query in two layers:
- Inner query — selects actual records from the table (has all the
permission context)
- Outer query — wraps the inner query's raw SQL string to do
grouping/pagination
The problem: the inner query's SQL is copied out as a plain string
before RLS predicates are added to it. RLS predicates are normally added
lazily when you execute the query, but here the execution happens on the
outer query — which doesn't know about the entity or its RLS rules.
So RLS predicates are never applied anywhere.
The fix: explicitly apply RLS predicates to the inner query before its
SQL is extracted.
Additonnaly, fixed a temporal issue in Datetime pickers.
## Summary
### Externalize `twenty-client-sdk` from `twenty-sdk`
Previously, `twenty-client-sdk` was listed as a `devDependency` of
`twenty-sdk`, which caused Vite to bundle it inline into the dist
output. This meant end-user apps had two copies of `twenty-client-sdk`:
one hidden inside `twenty-sdk`'s bundle, and one installed explicitly in
their `node_modules`. These copies could drift apart since they weren't
guaranteed to be the same version.
**Change:** Moved `twenty-client-sdk` from `devDependencies` to
`dependencies` in `twenty-sdk/package.json`. Vite's `external` function
now recognizes it and keeps it as an external `require`/`import` in the
dist output. End users get a single deduplicated copy resolved by their
package manager.
### Externalize `twenty-sdk` from `create-twenty-app`
Similarly, `create-twenty-app` had `twenty-sdk` as a `devDependency`
(bundled inline). After refactoring `create-twenty-app` to
programmatically import operations from `twenty-sdk` (instead of
shelling out via `execSync`), it became a proper runtime dependency.
**Change:** Moved `twenty-sdk` from `devDependencies` to `dependencies`
in `create-twenty-app/package.json`.
### Switch E2E CI to `yarn npm publish`
The `workspace:*` protocol in `dependencies` is a Yarn-specific feature.
`npm publish` publishes it as-is (which breaks for consumers), while
`yarn npm publish` automatically replaces `workspace:*` with the
resolved version at publish time (e.g., `workspace:*` becomes `=1.2.3`).
**Change:** Replaced `npm publish` with `yarn npm publish` in
`.github/workflows/ci-create-app-e2e.yaml`.
### Replace `execSync` with programmatic SDK calls in
`create-twenty-app`
`create-twenty-app` was shelling out to `yarn twenty remote add` and
`yarn twenty server start` via `execSync`, which assumed the `twenty`
binary was already installed in the scaffolded app. This was fragile and
created an implicit circular dependency.
**Changes:**
- Replaced `execSync('yarn twenty remote add ...')` with a direct call
to `authLoginOAuth()` from `twenty-sdk/cli`
- Replaced `execSync('yarn twenty server start')` with a direct call to
`serverStart()` from `twenty-sdk/cli`
- Deleted the duplicated `setup-local-instance.ts` from
`create-twenty-app`
### Centralize `serverStart` as a dedicated operation
The Docker server start logic was previously inline in the `server
start` CLI command handler (`server.ts`), and `setup-local-instance.ts`
was shelling out to `yarn twenty server start` to invoke it -- meaning
`twenty-sdk` was calling itself via a child process.
**Changes:**
- Extracted the Docker container management logic into a new
`serverStart` operation (`cli/operations/server-start.ts`)
- Merged the detect-or-start flow from `setup-local-instance.ts` into
`serverStart` (detect across multiple ports, start Docker if needed,
poll for health)
- Deleted `setup-local-instance.ts` from `twenty-sdk`
- Added `onProgress` callback (consistent with other operations like
`appBuild`) instead of direct `console.log` calls
- Both the `server start` CLI command and `create-twenty-app` now call
`serverStart()` programmatically
related to https://github.com/twentyhq/twenty-infra/pull/525
## Summary
- Fixes intermittent `INVALID_QUERY_INPUT` errors (152 occurrences / 2
users in Sentry) caused by the single record picker sending `{ id: { in:
[] } }` to the Search query when no records are selected
- The `skip` guard is logically correct but Apollo Client v4 can briefly
fire queries during React 18 render transitions before processing the
skip flag
- Makes `selectedIdsFilter` conditional on `hasSelectedIds`, so
variables contain a safe empty filter `{}` regardless of skip behavior —
matching the existing defensive pattern used by the third query's
`notFilter`
## Test plan
- [ ] Open a relation field picker (e.g., Person on an Opportunity) with
no existing value — search should load without errors
- [ ] Open a relation field picker with an existing value — selected
record should appear and search should work
- [ ] Clear a selected relation and reopen the picker — no console
errors
Made with [Cursor](https://cursor.com)
## Summary
- Adds `isSearchable: true` to the workflow standard object definition
so new workspaces get searchable workflows automatically
- Adds a `upgrade:1-20:make-workflow-searchable` migration command that
flips the `isSearchable` flag on the `objectMetadata` row for existing
workspaces, with proper cache invalidation and metadata version
increment
- Registers the command in the 1-20 upgrade module and the
`upgrade.command.ts` orchestrator
The `searchVector` stored generated column already exists on the
workflow table, so no data backfill is needed — this is purely a
metadata flag change that makes the search service include workflows in
results.
## Test plan
- [x] `--dry-run` logs what it would do without making changes
- [x] Actual run updates both workspaces and invalidates caches
- [x] Idempotent: re-running skips already-searchable workspaces
- [x] Typecheck passes
- [x] Lint passes on changed files
Made with [Cursor](https://cursor.com)
Fixes https://github.com/twentyhq/twenty/issues/18148
## Problem
- Scrolling down in the "no value" kanban column never loads additional
records when it's the only column that needs pagination
- `useTriggerRecordBoardFetchMore` — `.filter(isDefined)` removes `null`
from the list of group values to fetch, and `null` is the value used by
"no value" columns, causing early exit
- `useTriggerRecordBoardFetchMore` — the inline `{ in: [...values] }`
filter cannot express a NULL match, so even without the early exit the
query would be wrong
## Fix
- "No value" column now triggers pagination correctly —
`useTriggerRecordBoardFetchMore` (removed `.filter(isDefined)` so `null`
values pass through)
- Query filter correctly matches NULL field values —
`useTriggerRecordBoardFetchMore` (replaced inline `{ in: [...] }` with
`computeRecordGroupOptionsFilter` which generates `{ is: 'NULL' }` for
null values and `{ in: [...] }` for non-null values)
## Summary
- Chrome 142 rejects `var()` references in SVG `width`/`height`
attributes, causing icons to fall back to `100%` size
- Replaced `themeCssVariables.icon.size.*` /
`themeCssVariables.icon.stroke.*` (raw `var()` strings) with resolved
`theme.icon.size.*` / `theme.icon.stroke.*` (numeric values from
`ThemeContext`) when passed as icon component props
- Affects 3 navigation menu item components: `NavigationMenuItemFolder`,
`NavigationMenuItemLinkDisplay`, `LinkIconWithLinkOverlay`
## Test plan
- [ ] Verify navigation drawer folder chevron icons render at correct
size
- [ ] Verify navigation drawer link arrow icons render at correct size
- [ ] Verify link overlay icons render at correct size
- [ ] Test in Chrome 142+ to confirm the fix
- [ ] Test in Firefox/Safari to confirm no regression
When an If/Else branch skips an Iterator step (because the branch wasn't
taken), the executor incorrectly entered the iterator's loop body. This
happened because getNextStepIdsToExecute checked !hasProcessedAllItems
on an undefined result, which evaluated to true, causing it to return
initialLoopStepIds instead of the post-loop nextStepIds.
Fix: Add a !executedStepOutput.shouldSkipStepExecution guard to the
iterator condition in getNextStepIdsToExecute, consistent with the
existing shouldFailSafely guard.
Clear the SSE client before swapping auth tokens during impersonation,
preventing a userWorkspaceId mismatch between the existing event stream
(created under the admin's identity) and the new impersonation token.
Treat NOT_AUTHORIZED event stream errors as recoverable (destroy +
recreate), matching the existing behavior for
EVENT_STREAM_DOES_NOT_EXIST and EVENT_STREAM_ALREADY_EXISTS.
Introduce a new feature flag for the record table widget, enabling
conditional rendering and state management based on its status. Update
related components and tests accordingly.
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
- Fix issue with name that must be first in the view field list
- Improve dry run and logs of backfill page layouts command
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
## Summary
Fixes the merge-queue E2E failures that started after #18912 landed.
`filterReadableActiveObjectMetadataItems` (introduced in #18912) treats
a **missing** entry in the permissions map as "deny read". The previous
helper (`getObjectPermissionsFromMapByObjectMetadataId`) treated it as
**"allow read"** via a `?? { canReadObjectRecords: true, … }` fallback.
Because the permissions map is empty on initial render (before the API
round-trip completes), **every object was temporarily filtered out**.
This made `useDefaultHomePagePath` return the settings-profile path,
triggering a redirect race that broke Settings navigation in three E2E
tests:
- `signup_invite_email.spec.ts` — timed out waiting for
`getByRole('link', { name: 'Members' })`
- `create-kanban-view.spec.ts` — timed out waiting for
`getByRole('link', { name: 'Data model' })`
- `workflow-creation.spec.ts` — timed out waiting for sidebar Workflows
button
**Evidence from CI:**
- Last passing merge-queue run: base `13ea3ec75c` (before #18912)
- First failing merge-queue run: base `5f0d6553f6` (#18912)
- All individual PR CI runs continued to pass (they don't rebase on
latest main)
- Screenshot artifact shows the app stuck on the main page — the
Settings drawer never opened
## Fix
When an object has no entry in the permissions map, default to allowing
read (matching the old behavior). An empty map means permissions haven't
loaded yet, not that the user lacks access.
```diff
objectMetadataItems.filter((objectMetadataItem) => {
+ if (!objectMetadataItem.isActive) {
+ return false;
+ }
+
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
-
- return (
- isDefined(objectPermissions) &&
- objectPermissions.canReadObjectRecords &&
- objectMetadataItem.isActive
- );
+
+ if (!isDefined(objectPermissions)) {
+ return true;
+ }
+
+ return objectPermissions.canReadObjectRecords;
});
```
## Summary
- When `STORAGE_S3_PRESIGNED_URL_BASE` is configured, the file
controller returns a **302 redirect** to a presigned S3 URL instead of
proxying every byte through the server. This eliminates server bandwidth
and CPU overhead for S3-backed deployments.
- For local storage or S3 without a public endpoint, behavior is
unchanged (stream + pipe with security headers).
- Added `getPresignedUrl` to the `StorageDriver` interface (required
method returning `string | null`), with implementations in S3Driver
(uses a separate presign client with the public endpoint), LocalDriver
(returns `null`), and ValidatedStorageDriver (path traversal protection
+ delegation).
- Added a unified `getFileResponseById` method in `FileService` that
performs a single DB lookup and returns either a redirect URL or a
stream, avoiding double lookups.
- Extracted `getContentDisposition` from the header util so both the
proxy path and presigned URL path share the same inline/attachment
allowlist.
- Added MinIO service to `docker-compose.dev.yml` (optional `s3`
profile) for local S3 testing.
- Documented S3 presigned URL setup, CORS, and `nosniff` requirements in
the self-hosting docs.
## Test plan
- [x] All 63 unit tests pass across 5 test suites (util, S3 driver,
validated driver, file storage service, controller)
- [x] `npx nx typecheck twenty-server` passes
- [ ] Manual E2E test with MinIO: `docker compose --profile s3 up -d`,
configure S3 env vars, verify `curl -I` returns 302 with `Location`
header pointing to MinIO
- [ ] Verify local storage (no `STORAGE_S3_PRESIGNED_URL_BASE`) still
streams files with 200 + security headers
- [ ] Verify public assets endpoint still proxies (no redirect)
Made with [Cursor](https://cursor.com)
## Summary
- Adds an object type filter dropdown to the side panel, allowing users
to scope record search results to a specific object type (e.g. People,
Companies, Opportunities)
- The filter appears as a funnel icon next to the search input in both
the global search (SearchRecords page) and the "Pick a record" sidebar
flow
- Replaces the AI sparkles button on the search records page with the
filter icon
- Uses colored icons matching the navigation menu style
(NavigationMenuItemStyleIcon + getStandardObjectIconColor)
## Test plan
- [ ] Open the side panel, type something to enter SearchRecords mode,
verify the filter icon appears
- [ ] Click the filter icon, verify the dropdown opens with "Object"
header, search input, "All Objects" and individual object types with
colored icons
- [ ] Select an object type, verify only records of that type appear in
search results
- [ ] Select "All Objects", verify all record types appear again
- [ ] Verify the filter icon turns blue when a filter is active
- [ ] In layout customization mode, add a new sidebar item > Record,
verify the filter dropdown also works there
- [ ] Close and reopen the side panel, verify the filter resets to "All
Objects"
- [ ] Verify the AI sparkles button no longer appears on the command
menu root page
- [ ] Verify the AI edit icon still appears on Ask AI pages
Made with [Cursor](https://cursor.com)
## Summary
- The 1-20 backfill command menu items upgrade command was mixing
standard and custom application flat entities into a single
`validateBuildAndRunWorkspaceMigration` call. Since each migration run
is tied to a single application, this split the backfill into two
separate runs: standard items under `twentyStandardFlatApplication` and
workflow trigger items under `workspaceCustomFlatApplication`.