Commit Graph

34 Commits

Author SHA1 Message Date
Charles Bochet 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
Charles Bochet 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
Félix Malfait 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
Charles Bochet 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
neo773 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
Paul Rastoin 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
Félix Malfait 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
Félix Malfait 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
Etienne 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
martmull 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
Félix Malfait 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
Félix Malfait 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
Kartik Pant 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
Marie 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
Weiko 85752f8a61 Bump 2.3.0 (#20169) 2026-04-30 19:34:43 +02:00
nitin 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
martmull 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
Etienne 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
Charles Bochet 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
Félix Malfait 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
Weiko 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
Félix Malfait 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
Charles Bochet 4ed6fcd19e chore: move TABLE_WIDGET view type migration to 1.23 fast instance command (#19797)
## Summary

- Relocates `AddTableWidgetViewTypeFastInstanceCommand` from `1-22/` to
`1-23/` and bumps its `@RegisteredInstanceCommand` version from `1.22.0`
to `1.23.0`. The original timestamp `1775752190522` is preserved so the
command slots chronologically into the existing 1.23 sequence;
auto-discovered via `@RegisteredInstanceCommand`, no module wiring
change needed.
- Same pattern as #19792 (move
`pageLayoutWidget.conditionalAvailabilityExpression` to 1.23).
2026-04-17 12:44:35 +00:00
Charles Bochet beeb8b7406 chore: move pageLayoutWidget.conditionalAvailabilityExpression migration to 1.23 fast instance command (#19792)
## Summary

- Replaces the standalone TypeORM migration
`1775654781000-addConditionalAvailabilityExpressionToPageLayoutWidget.ts`
with a registered fast instance command under
`packages/twenty-server/src/database/commands/upgrade-version-command/1-23/`,
so the `pageLayoutWidget.conditionalAvailabilityExpression` column is
created through the unified upgrade pipeline.
- Uses `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so the new
instance command is a safe no-op for environments that already applied
the previous TypeORM migration.
- Keeps the original timestamp `1775654781000` so the command slots
chronologically into the existing 1.23 sequence; auto-discovered via
`@RegisteredInstanceCommand`, no module wiring needed.

## Context

Reported error when creating a new workspace on `main`:

> column PageLayoutWidgetEntity.conditionalAvailabilityExpression does
not exist

Aligns this column addition with the rest of the 1.23 schema changes
that already use the instance-command pattern.
2026-04-17 11:40:05 +00:00
Raphaël Bosi 2b5b8a8b13 Link command menu items to specific page layout (#19706)
- Add a `pageLayoutId` foreign key to `CommandMenuItem`, allowing
command menu items to be scoped to a specific page layout instead of
being globally available
- Filter command menu items by the current page layout on the frontend.
Items with a `pageLayoutId` only appear when viewing that layout, while
items without one remain globally visible
- Create an effect to track the current page layout ID
- Include a seed example: a "Show Notification" command pinned to the
Star history standalone page layout

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-16 12:07:36 +00:00
Etienne 94b8e34362 Object view widget - Introduce new TABLE_WIDGET view type (#19545)
closes
https://discord.com/channels/1130383047699738754/1491549365263667230/1491804729397743666
2026-04-15 13:30:37 +00:00
Raphaël Bosi a88d1f4442 Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type
(`STANDALONE_PAGE`) that can be rendered independently at
`/page/:pageLayoutId`, not tied to any record or object context.

- New `STANDALONE_PAGE` page layout type
- New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId`
foreign key to `NavigationMenuItemEntity`, allowing sidebar items to
link directly to standalone pages
- New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates
object-context-dependent commands (Create Record, Import, Export, See
Deleted, Create View, Hide Deleted) from truly global ones, so
standalone pages only show relevant commands
- Frontend routing & rendering: adds a `/page/:pageLayoutId` route with
its own page component, header, and command menu
- Widget rendering refactor
- Instance commands: two fast 1.22 migrations: `pageLayoutId` column +
`STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type
enum
- Workspace command: backfills existing command menu items from `GLOBAL`
to `GLOBAL_OBJECT_CONTEXT` where appropriate
- Dev seeds: adds a sample "Star History" standalone page with an iframe
widget for local development
2026-04-14 15:52:45 +00:00
Félix Malfait 455022f652 Add ClickHouse-backed metered credit cap enforcement (#19586)
## Summary
Implements a ClickHouse-backed polling system to enforce metered-credit
caps for workflow executions, replacing reliance on Stripe billing
alerts. The system re-evaluates tier caps against live pricing on every
poll cycle, allowing price/tier changes to propagate immediately without
recreating Stripe alert objects.

## Key Changes

- **BillingUsageCapService**: New service that queries ClickHouse for
current-period credit usage and evaluates whether a subscription has
reached its metered-credit allowance (tier cap + credit balance)
  - `isClickHouseEnabled()`: Checks if ClickHouse is configured
- `getCurrentPeriodCreditsUsed()`: Sums creditsUsedMicro from usageEvent
table for a workspace within a billing period
- `evaluateCap()`: Determines if usage has reached the allowance by
reading live pricing from the subscription

- **EnforceUsageCapJob**: Cron job that polls all active subscriptions
and updates `hasReachedCurrentPeriodCap` on metered items
  - Runs every 2 minutes to keep cap enforcement in sync with live usage
- Supports shadow mode (log-only) via
`BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag for safe rollout
- Continues processing after per-subscription errors with detailed
logging

- **EnforceUsageCapCronCommand**: CLI command to register the
enforcement cron job

- **MeteredCreditService**: Extracted
`extractMeteredPricingInfoFromSubscription()` as a pure function for
callers that already hold the subscription with pricing loaded, avoiding
redundant DB queries

- **Configuration**: Added `BILLING_USAGE_CAP_CLICKHOUSE_ENABLED` flag
to control enforcement mode (active vs. shadow)

- **Constants**: Added `METERED_OPERATION_TYPES` to define which
operation types count toward the metered product's credit cap

## Implementation Details

- The service queries ClickHouse for the sum of `creditsUsedMicro` in
the current billing period, matching Stripe meter semantics
- Pricing is re-read on every evaluation, so tier changes propagate
within one poll cycle without Stripe alert recreation
- The cron job only updates the database when the cap state actually
changes (no-op if already in the correct state)
- Shadow mode allows safe validation before enabling enforcement;
transitions are logged but not persisted
- Comprehensive test coverage for both the service and cron job,
including error handling and state transitions

https://claude.ai/code/session_01VksTSrYLXJVCPVBQhQdBTe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-13 21:17:41 +02:00
Paul Rastoin 21142d98fe Implement cross version upgrade (#19559)
# Introduction
Refactoring the upgrade engine to handle cross version upgrade,
completely getting rid of the semver `version` at db and runtime level
It remains a visual a listing indicator for or CD process but also
during devenv in order to prepare next release
Will write a release process runbook documentation on how to handle
upgrade step patch, command insertion etc as it needs to be cascaded
across all the involved supported version

**The upgrade sequence model:**

The sequence is a flat, ordered array of upgrade steps
(`UpgradeStep[]`), built from the registry by chaining all versions in
order, each version contributing its fast-instance → slow-instance →
workspace commands sorted by timestamp. Version is metadata for logging,
not used in the algorithm.

**Segments:**

The sequence naturally splits into alternating segments of contiguous
instance steps and contiguous workspace steps. The runner processes
segments in order:

- **Instance segment:** Run sequentially from the instance cursor. Each
step runs once globally.
- **Workspace segment:** Each workspace independently walks from its own
cursor through the end of the segment. Workspaces are independent within
a segment — they can be at different positions.
- **Synchronization (workspace → instance):** The runner blocks before
entering an instance segment. All active/suspended workspaces must have
completed the last workspace step of the preceding workspace segment. If
any workspace failed, abort. This is the only explicit synchronization
point.
- Instance → workspace ordering is implicit — the runner processes
segments sequentially, so the instance segment naturally completes
before the workspace segment begins.


full docs
https://gist.github.com/prastoin/e62106d455fd72d6b6ebada8351e5492

## Version constants & type-level deprecation

Version management is split into three atomic constants:
`TWENTY_PREVIOUS_VERSIONS`, `TWENTY_CURRENT_VERSION`, and
`TWENTY_NEXT_VERSIONS`. Two derived constants compose them:
`CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current — what the engine
runs) and `ALL_TWENTY_VERSIONS` (the full ordered tuple including next).
The registry service validates at module init that no version is
duplicated across constants and that at least one previous version
exists.

A `DeprecatedSinceVersion<RemoveAtVersion, T>` type utility resolves to
`T` while `TWENTY_CURRENT_VERSION` is below `RemoveAtVersion`, and to
`never` once it reaches it — turning deprecation into a compile-time
guarantee via `IndexOf` and `IsGreaterOrEqual` generics in
`twenty-shared`.

### `workspace.version` column deprecation

The column is replaced by cursor-based state inference from
`UpgradeMigration` records, but cannot be dropped in 1.22: workspaces
activated during 1.21 predate the cursor system and need their initial
cursor backfilled first (`backfillWorkspaceCreatedIn1_21_0Cursors`).
This backfill itself depends on a new `isInitial` column on
`UpgradeMigration`, bootstrapped via a targeted TypeORM migration before
the upgrade sequence runs.

Both functions and the entity field are typed with
`DeprecatedSinceVersion<'1.23.0', ...>`. When `TWENTY_CURRENT_VERSION`
reaches `1.23.0`, compile errors force their removal — and the
pre-declared `DropWorkspaceVersionColumnFastInstanceCommand` takes over
to drop the column.

## What's next
- ci cross version upgrade ( wip )
- banner asking to contact twenty administrator if workspace is outdated
- upgrade healthcheck cli 

## New unit/integ test pattern
Create a dedicated `createNestApp` that consumes a real database in
order not to have to mack any database interaction to the
`upgradeMigrations` allowing full coverage of the whole
`upgradeRunnerService.run` core logic
2026-04-13 11:42:27 +02:00
Charles Bochet f6423f5925 Remove DataSourceService and clean up datasource migration logic (#19532)
## Summary

- **Drop the `objectMetadata.dataSourceId` foreign key and index** via a
1-22 fast instance command — column kept nullable for data preservation
- **Delete `DataSourceService`, `DataSourceModule`, and
`DataSourceException`** — all code now uses `workspace.databaseSchema`
directly
- **Remove `IS_DATASOURCE_MIGRATED` feature flag** from default flags
and all branching logic
- **Simplify workspace/object creation pipelines** —
`WorkspaceManagerService`, `DevSeederService`, and the object creation
action handler no longer route through `DataSourceService`
- **Keep `DataSourceEntity` and the `dataSource` table** for historical
data — entity stripped of all ORM relations
2026-04-10 07:34:05 +00:00
neo773 8a10071253 add workspaceId to indirect entities (#19522)
Required for `workspace:export` command

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-09 19:30:28 +00:00
Charles Bochet 07745947ee Fix rolesPermissions cache query cartesian product (62k → 162 rows) (#19511)
## Summary
- **Split the `rolesPermissions` cache query into parallel queries** to
eliminate a 5-way LEFT JOIN cartesian product. A workspace with a role
having 5 objectPermissions × 4 permissionFlags × 124 fieldPermissions ×
5 RLP predicates × 5 RLP groups was returning 62,000 rows from just ~162
distinct records, causing 10s+ query timeouts on view updates.
- **Added missing `roleId` index on `permissionFlag` table** — the only
permission table without one, which was causing sequential scans on the
JOIN.
2026-04-09 16:13:33 +00:00
Paul Rastoin 5e3cf7cd2b Prepare 1.21 (#19501)
Migrating some 1.21 migrations to the new pattern so it writes in the
history
When release in prod we will ahve to manually inject them as they have
already been run
It's mainly for self host so when releasing the 1.22 we will be able to
rely on the 1.21 history
2026-04-09 13:53:21 +00:00
Paul Rastoin 23874848a4 Instance commands and upgrade_migrations table (#19356)
# Introduction
Now only using typeorm to generate migrations up and down statement
We handle and maintain our own migration table history

## What's new
Now all the instance commands will live within the same module and
folder than the upgrade commands
Sequentiality comes from the timestamp located in the filename
Same sequentiality also applies to the workspace commands in the future,
for the moment still expected a as code explicit declaration

( below screen is an example see below section )
<img width="1382" height="634" alt="image"
src="https://github.com/user-attachments/assets/5610a246-4eae-485e-99f4-98fb89ad5ac8"
/>

## Existing 1.21 migrations
We won't start following this pattern in 1.21 yet at least not with the
migration that has already been released as typeorm migrations in cloud
production as they would rerun


## Small duplication
Duplicating the legacy typeorm and instance commands run in the
`run-instance-commands` to avoid any merge of interest for the moment

## Concurrency
Not handling any run in parrallel of the upgrade for the moment
2026-04-07 08:55:17 +00:00