Commit Graph

127 Commits

Author SHA1 Message Date
Raphaël Bosi 0dc6272da5 Remove twenty-ui reexport from the SDK and use twenty-ui directly (#22326)
## What & why

Removes the `twenty-sdk/ui` reexport. Apps now use Twenty UI by
installing
[`twenty-ui@1.0.0-alpha.1`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)
from npm and importing its subpaths directly. The reexport re-exported
types that didn't resolve, forcing typecheck workarounds.

## Changes

- **twenty-sdk**: delete `src/ui/index.ts`, drop the `./ui` export,
remove it from the browser vite build, and rewire the CLI manifest-mock
to `twenty-ui` (`.css` falls through to the empty-CSS loader).
`twenty-ui` stays a devDependency for the CLI fixture tests.
- **Renderer + create-twenty-app template**: import from `twenty-ui`
subpaths; the template pins `twenty-ui@1.0.0-alpha.1`.
- **Docs**: new "Using Twenty UI components" section (install + subpath
imports + `useTheme()` for theme tokens), codex references, and the
cross-doc-contract validator.

The `twenty-for-twenty` / `twenty-slack` example apps are intentionally
left on `twenty-sdk/ui`: they consume the published SDK (which still
ships `./ui`), and `twenty-ui@1.0.0-alpha.1` requires react 19 + a
`monaco-editor` peer the react-18 apps can't satisfy. They migrate once
the SDK is republished.
2026-06-30 11:17:48 +02:00
Félix Malfait 5242ddf458 feat(apps): let front components open a record in the side panel (#22140)
## Why

Front components (apps) could `navigate()` to a record's **full page**,
but there was no way to open a specific record in the **side panel**.
More generally, `openSidePanelPage` could navigate to a `SidePanelPages`
enum page but couldn't pass the context most pages need.

## What

`openSidePanelPage`'s params are now a **discriminated union keyed on
`page`**, so each page declares its own typed payload (instead of a flat
bag of optionals whose validity silently depends on `page`). This is
also safer: pages that can't render without context can't be "opened"
into a broken panel.

Wired the param-bearing pages host-side, each bridging to its existing
internal hook:

| `page` | Params | Bridges to |
|---|---|---|
| `ViewRecord` | `recordId`, `objectNameSingular`,
`resetNavigationStack?` | `useOpenRecordInSidePanel` (full-page fallback
on mobile / unsupported objects) |
| `EditRichText` | `recordId`, `objectNameSingular`, `fieldName?` |
`useOpenRichTextInSidePanel` |
| `ComposeEmail` | `connectedAccountId`, `threadId?`, `defaultTo?`,
`defaultSubject?`, `defaultInReplyTo?`, `pageTitle?`, `pageIcon?` |
`useOpenComposeEmailInSidePanel` |
| `ViewFrontComponent` | `frontComponentId`, optional
`recordId`+`objectNameSingular`, `pageTitle`, `pageIcon?`,
`resetNavigationStack?` | `useOpenFrontComponentInSidePanel` |
| *(any other page)* | `pageTitle`, `pageIcon?`,
`shouldResetSearchState?` | `navigateSidePanel` |

`CommandOpenSidePanelPage` now takes the union directly, so headless
command-menu items can open any of these. Threaded through `twenty-sdk`
→ `twenty-front-component-renderer` → host
(`useFrontComponentExecutionContext`), with unit tests per page and the
mobile/unsupported fallbacks.

## Deliberately deferred: `MergeRecords`

`useOpenMergeRecordsPageInSidePanel` takes `objectNameSingular` /
`objectRecordIds` at **hook-init** (it calls `useObjectMetadataItem` /
`useLazyFindManyRecords` at render), so it can't be driven by runtime
app params without refactoring that hook + its current caller. Left out
of this PR — better as its own change.

## Worth a second look (reviewers)

- **`ViewFrontComponent`** lets an app open a front component by id.
Within an app that's clean composition; whether an app should be able to
target *another* app's component is a scoping/security question. The
render still runs under the app's access token, so cross-app fetches
would fail auth — but flagging it explicitly.

## Security note

Side-panel record/page views render natively under the **user's**
session/Apollo client, not the app's scoped token — RLS/field
permissions are enforced as if the user opened it themselves. Same trust
model as `navigate(AppPath.RecordShowPage, …)`.

## Follow-up

A separate PR will centralize the mobile + `canOpenObjectInSidePanel`
guard inside `useOpenRecordInSidePanel` (currently duplicated across
callers, missing in others).

## Validation

> [!NOTE]
> Dependencies wouldn't install in this environment (flaky network
during `yarn install`), so lint / typecheck / jest weren't run locally —
relying on CI. The diff was reviewed manually for type-consistency,
including the discriminated-union narrowing in the host switch.

https://claude.ai/code/session_01AAJFXzsCeoj6BeP3ofiTKQ
2026-06-25 10:47:39 +02:00
martmull b5958fb331 Enforce server route app configuration requirements (#22091)
## Summary
This PR enforces that applications exposing server route logic functions
must be claimed (have an owner workspace) and installed on that owner
workspace to be considered "configured". This ensures server route
resolvers have a valid workspace context to execute in.

## Key Changes
- **ApplicationRegistrationVariableService**: Enhanced
`isConfiguredBatch()` to check server route configuration in addition to
required variables
- Added `ApplicationEntity` repository injection to track app
installations
- Implemented `isServerRouteConfigured()` private method that validates:
- If app exposes server route logic functions, it must have an owner
workspace
- If it has an owner workspace, it must be installed on that workspace
  - Added comprehensive test suite covering all configuration scenarios

- **ServerRouteTriggerService**: Removed feature flag check
(`IS_SERVER_LOGIC_FUNCTION_ENABLED`)
  - Deleted `TwentyConfigService` dependency
  - Removed feature disabled exception handling
- Server route triggers are now always enabled (gated by app
configuration instead)

- **Configuration**: Removed `IS_SERVER_LOGIC_FUNCTION_ENABLED` config
variable from `ConfigVariables`

- **Exception handling**: Removed `FEATURE_DISABLED` exception code from
`ServerRouteTriggerExceptionCode`

- **UI & Documentation**: Updated messaging and docs to reflect that
server route apps require claiming and installation on owner workspace

## Implementation Details
- Server route configuration is checked alongside required variable
validation in `isConfiguredBatch()`
- Uses efficient batch queries with `Promise.all()` to fetch variables,
registrations, and installations in parallel
- Installs are tracked via a Set of `${registrationId}:${workspaceId}`
keys for O(1) lookup
- Apps without server route functions are unaffected by this change

https://claude.ai/code/session_01Ub3K25p2q4XE1LW1LGJbkG

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22091?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 16:20:05 +00:00
Félix Malfait 614bc7b7e6 feat: serve HTTP logic functions on isolated *.withtwenty.com domain (#22045)
## Summary

Implements
[core-team-issues#2473](https://github.com/twentyhq/core-team-issues/issues/2473):
serve HTTP-triggered logic functions from a dedicated, **cookieless**
public domain (`{workspaceSubdomain}.withtwenty.com`) instead of the
same-site `/s/` route, so functions can safely return **arbitrary
headers** — custom headers, `Permissions-Policy`
(camera/mic/geolocation), `Cross-Origin-Opener-Policy: same-origin`,
`Cross-Origin-Embedder-Policy: require-corp`, `Set-Cookie`, etc.

The `/s/` route stays the strict, same-site path it is today.
**Self-hosting is unchanged** — everything new is gated on
`PUBLIC_DOMAIN_URL` being set.

### Why

Today user-authored function responses are served same-site with the
Twenty app, so the response-header allow-list is restricted to 5 safe
headers and request headers are limited to a per-function allow-list.
Serving from an origin that shares nothing with `*.twenty.com` removes
that constraint safely — the same "user content domain" pattern as
GitHub (`*.githubusercontent.com`) and CodeSandbox (`*.csb.app`).

## What's in here

**Routing**
- The **root-path → `/s` rewrite happens at the nginx ingress**, not in
app code. The existing `api-ingress.yaml` already rewrites root paths
onto `/s` (host-agnostically) when the edge sets
`X-Twenty-Public-Domain: true`, so `*.withtwenty.com` and registered
custom public domains are handled by the same mechanism. (An earlier
in-app middleware was removed as a redundant, wrong-layer duplicate.)
- `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain` recognizes
`*.` subdomains, resolves the workspace by subdomain, and returns
`isIsolatedOrigin`. Explicitly registered public-domain rows still take
precedence and keep their application scoping. The ingress preserves the
`Host` header, so this resolution still fires.

**Headers (server)**
- Isolated origin → all response headers pass through and all request
headers are forwarded. Same-site `/s/` keeps the strict allow-lists.
(Global CORS already handles preflight/ACAO.)

**`/s/` deprecation for new routes (cloud only)**
- New `LOGIC_FUNCTION_LEGACY_ROUTE_CUTOFF` config var (ISO date,
optional). When `PUBLIC_DOMAIN_URL` is set, functions created on/after
the cutoff return **410 Gone** on `/s/` with the new URL. Existing
routes and self-hosted instances are untouched.

**Frontend education**
- `publicFunctionDomain` added to `ClientConfig` (from
`PUBLIC_DOMAIN_URL`).
- The logic-function **Live URL** now resolves to
`https://{workspaceSubdomain}.{publicFunctionDomain}{path}` on cloud,
falling back to `/s/` for self-hosting.
- Front components call their functions through the SDK
(`RestApiClient`), which now targets the isolated domain via the
injected `TWENTY_FUNCTIONS_URL`.
- New **"Public URL"** section on the application **Settings** tab
explaining the isolated domain (shown when the app exposes
HTTP-triggered functions).

**Docs**: note the `withtwenty.com` domain for external callers in the
apps guide.

## Infra prerequisites (not code — needs dashboard work)
- Wildcard DNS `*.withtwenty.com` (proxied) + wildcard TLS in the
public-domain Cloudflare zone.
- Edge (Cloudflare) sets `X-Twenty-Public-Domain: true` for
`*.withtwenty.com` requests, so the existing nginx ingress rewrites them
onto `/s` (same header the custom-domain flow already relies on).
- Set `PUBLIC_DOMAIN_URL=https://withtwenty.com` on cloud.
- Submit `withtwenty.com` to the **Public Suffix List** (required for
cross-tenant cookie isolation before relying on `Set-Cookie`).

## Test plan
- [x] `nx typecheck twenty-server`, `nx typecheck twenty-front`
- [x] `lint:diff-with-main` + oxfmt clean (server + front)
- [x] `npx jest route-trigger public-function-domain
domain-server-config workspace-domains build-logic-function-event
client-config` → server unit tests passing (resolution tiers, header
passthrough vs allow-list, `/s/` cutoff 410)
- [x] `npx jest getLogicFunctionHttpUrl` (front) and `nx test
twenty-client-sdk` (RestApiClient routing) passing
- [x] CI green (server, front, sdk, renderer, ui, zapier, example apps)
- [ ] Manual: hit `{subdomain}.withtwenty.com/` end-to-end once infra is
provisioned

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22045?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
2026-06-24 15:57:01 +02:00
martmull b5a1aed24b feat(server): run server-exposed logic functions in the owner workspace (#22002)
## Summary

Implements the server-level logic-function tier in the simplest shape: a
logic function is "server-exposed" iff its manifest entry carries
`serverWebhookTriggerSettings`. Execution delegates to the
owner-workspace copy of that function — billing, throttling, env vars,
and the existing executor all apply uniformly against that workspace.

Supersedes #21971 with the simplified design from that discussion (no
`applicationRegistrationLogicFunction` registry, no dedicated manifest
type, no separate SDK helper, no special throttling).

## Design

- **Manifest**: `LogicFunctionManifest` gains
`serverWebhookTriggerSettings?`. The declarative `workspaceIdResolver`
shape is dropped.
- **Materialization**: those settings become two new jsonb columns on
`LogicFunctionEntity`. The manifest → flat converter and the
create-from-source DTO/util forward them; the property-config map and
editable-properties list are extended.
- **Lookup**: a single QB query joins `logicFunction → application →
applicationRegistration` and filters on `lf.workspaceId =
reg.workspaceId` to get only the owner workspace's copy.
- **Webhook**: `POST /webhooks/server/:logicFunctionUniversalIdentifier`
→ `ServerWebhookTriggerService.handle` → join lookup →
`LogicFunctionTriggerService.run`. No registry table, no
`:applicationRegistrationUniversalIdentifier` segment, no resolver.
- **Gate**: `IS_SERVER_LOGIC_FUNCTION_ENABLED` config var (disabled by
default).

## Test plan
- [x] `npx jest server-webhook-trigger` — 9 unit tests across the
webhook service.
- [x] `npx jest logic-function` — 88 existing tests stay green.
- [x] `npx nx typecheck twenty-server`.
- [x] `npx nx lint:diff-with-main twenty-server`.
- [x] Reset DB → init → run `database:migrate:prod` → run
`database:migrate:generate --name pending-migration-check` → no drift.
- [ ] Manual: hit `/webhooks/server/<uid>` end-to-end against a manifest
carrying `serverWebhookTriggerSettings`.

https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh

---
_Generated by [Claude
Code](https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22002?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 13:34:12 +00:00
martmull 21c3574f05 docs(apps): add key-value store guide for logic functions (#22061)
## What

Adds a new docs page under **Developers → Extend → Apps → Logic**
explaining how to give logic functions key-value storage (to persist
intermediate results, cache data, and share state across runs).

Rather than introducing a dedicated storage primitive, the guide shows
how to achieve this with a small **technical object** ("KV Store") that
has a unique `key` field and a `RAW_JSON` `value` field — queried
through the existing typed `CoreApiClient`.

Closes the documentation part of
[core-team-issues#2427](https://github.com/twentyhq/core-team-issues/issues/2427).

## Contents of the new page

- `defineObject` for the `kvStore` object (`key` + `value`)
- A unique index on `key` (the recommended uniqueness primitive)
- `get` / `set` (upsert) / `del` helpers built on `CoreApiClient`
- A worked example: caching an expensive third-party call with a TTL
- Patterns & tips: namespacing, expiry, what to store,
visibility/permissions, per-record scoping

## Files

- `developers/extend/apps/logic/key-value-store.mdx` — the new page
- `navigation/base-structure.json` — navigation source-of-truth
- `docs.json` + `twenty-shared/.../DocumentationPaths.ts` — regenerated
from base-structure

## Notes

- Documentation only — no code/behavior changes.
- This is a convention (a regular custom object), not a new feature, so
it inherits the same sync, permissions, and tooling as the rest of an
app's data.

https://claude.ai/code/session_01XAgXy1mUUMff5BFkFPjtfZ

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XAgXy1mUUMff5BFkFPjtfZ)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22061?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 11:45:35 +02:00
Félix Malfait a682c8fa62 feat(sdk): declare row-level permission predicates in the role manifest (#21919)
## Why

Apps can declare object and field permissions on a role via
`defineRole`, but **not row-level security**. The RLS engine and the
metadata-sync machinery already support predicates fully — they're
first-class universal flat entities, the `FlatRole` already carries
`rowLevelPermissionPredicateUniversalIdentifiers`, and the
workspace-migration layer has builders/validators/handlers for them. The
only gap was the **manifest layer**: `RoleManifest` had no field for
predicates, so the sync converter always left them empty.

As a result, the only way to ship RLS with an app was a post-install
script that pushed predicates through the
`upsertRowLevelPermissionPredicates` mutation. That mutation assigns
predicates to the workspace's **generic custom application**, not the
app that owns the role — so a single role's definition ends up split
across two applications and drifts on every upgrade (you have to
remember to re-run the script). The Partner app does exactly this today
via `configure-partner-rls.ts`.

## What

Adds `rowLevelPermissionPredicates` and
`rowLevelPermissionPredicateGroups` to `RoleManifest` / `RoleConfig`,
mirroring how `objectPermissions` / `fieldPermissions` already flow
end-to-end:

- **twenty-shared** — predicate + predicate-group manifest types on
`RoleManifest` (referencing objects/fields by `universalIdentifier`,
operand/logical-operator from the existing GraphQL enums).
- **twenty-sdk** — `defineRole` accepts and validates them; the build
derives deterministic predicate `universalIdentifier`s (groups keep an
explicit one so predicates can reference them).
- **twenty-server** — two converters turn manifest predicates/groups
into universal flat entities during application-manifest sync, so they
are created/updated/deleted together with the role and **owned by the
app that ships it**.

### Bug fix found along the way

The migration build order ran the `rowLevelPermissionPredicate(Group)`
builders **before** the `role` builder, so a predicate declared
alongside a brand-new role failed validation with `ROLE_NOT_FOUND`. They
now run **after** the role builder, exactly like object/field
permissions.

## Partner app (second commit)

Converts `partner.role.ts` to declare its five predicates inline and
**deletes `configure-partner-rls.ts`** + the `rls:configure` scripts —
the workaround this PR is meant to retire. The predicates are
byte-for-byte the same semantics as the script produced.

> Live-deployment note: the existing script-created predicates are owned
by the *custom* application, so the Partner app sync won't touch them.
Clear them once (e.g. an empty upsert on the Partner role) around deploy
to avoid duplicates. Kept as a **separate commit** so it can be split
out if reviewers prefer.

## Testing

- **Integration (full app):** new
`successful-manifest-sync-row-level-permission-predicate.integration-spec.ts`
— installs an app whose role declares a predicate and asserts the
predicate row is created (and **owned by the app**, not the custom app),
updated in place on re-sync, removed when dropped from the manifest, and
removed on uninstall. Ran locally against a seeded test DB .
- Re-ran the existing cross-app permission + view-field manifest suites
to confirm the build-order change doesn't regress
object/field-permission sync (13/13 ).
- **Unit (utils only):** `defineRole` validation and
`fromRoleConfigToRoleManifest` deterministic-id derivation.
- Docs: new "Row-level security" section in `apps/config/roles.mdx`.

## Scope notes / possible follow-ups

- Surfacing RLS in the app-install permission summary UI was
intentionally left out (predicates *restrict* rather than grant, and
typically live on a non-default role) — easy follow-up if wanted.
- The `upsertRowLevelPermissionPredicates` mutation still homes
out-of-band predicates on the custom app for app-owned roles; making
that consistent (or rejecting it, like field permissions already do) is
a sensible follow-up.

https://claude.ai/code/session_01MipAis9z9okd4oCm9HCKEf

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MipAis9z9okd4oCm9HCKEf)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21919?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-21 22:09:19 +02:00
martmull 6423c4cd3c Add recall io webhook endpoint (#21879)
## Context

Bot-recording integrations (e.g. the Recall.ai meeting bot) receive
webhooks from a third-party provider that delivers **every
tenant's events to a single URL**. Our existing `route-trigger` (`/s/…`)
resolves the workspace from the request host, which can't
work for one shared multi-tenant webhook URL. We need an instance-scoped
ingress that identifies the target workspace from the payload
  instead.

  ## Strategy

Add a new **`ingress-trigger`** logic-function trigger, mirroring
`route-trigger`:

  - A public endpoint keyed by the app's identifiers: `POST

/webhooks/ingress/:applicationRegistrationUniversalIdentifier/:logicFunctionUniversalIdentifier`.
- The logic function declares an `ingressTriggerSettings` block in its
manifest describing how to find the workspace in the payload
  (`workspaceId: { source: 'body' | 'query' | 'header', path }`).
- Core only **resolves the workspace** (declarative, fail-closed,
prototype-safe path getter), verifies the app is installed in that
workspace, then runs the function **synchronously** so the provider sees
the response (status codes / retries).
- **Signature verification stays in the logic function** (it gets
`rawBody` + forwarded headers), keeping core provider-agnostic.
- Shared execution logic (`build event → execute → map response`)
extracted into `LogicFunctionTriggerService`, now reused by both
  `route-trigger` and `ingress-trigger`.

  ## Major changes

- **twenty-shared**: new `ingressTriggerSettings` on
`LogicFunctionManifest` (`IngressTriggerSettings` type).
- **twenty-server**: new `ingress-trigger` module (controller, service,
exception + filter, workspace-id resolver util).
- **twenty-server**: extracted `LogicFunctionTriggerService` +
`route-trigger-response.util` (response builder + sender); refactored
  `RouteTriggerService` and both controllers to reuse them.
- **twenty-docs**: documented the ingress trigger (endpoint, workspace
resolution, signature responsibility, provider HMAC examples).
  - Unit tests for the resolver and the ingress service.
2026-06-19 23:34:43 +02:00
martmull a870e034a6 Add twenty slack to internal application ci (#21849)
- adds `twenty-slack` to internal application ci 
- unify config with twenty-last-contact app
- add base oxlint config to show error twenty-shared is used in internal
app

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21849?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 14:39:42 +00:00
Charles Bochet 257f130fff feat(sdk): let docker:start choose the server version (#21690)
## What

Makes `yarn twenty docker:start` version-selectable. Same core feature
as #21686 — but here scaffolded apps default to `latest` (pinning is
**opt-in**) rather than being pinned to the scaffolder's version.

> Alternative to #21686. Pick one; the difference is only the scaffolded
default.

Two layers of resolution:

1. **Explicit flag** — `yarn twenty docker:start [version]`, mirroring
the existing `docker:upgrade [version]`.
2. **App-pinned default** — when no version is passed, `docker:start`
reads `twenty.serverVersion` from the app's `package.json`, falling back
to `latest`.

Generated apps ship `twenty.serverVersion: "latest"`, so default
behavior is unchanged. To make the local server reproducible as code,
set a version:

```json filename="package.json"
{
  "twenty": {
    "serverVersion": "2.2.0"
  }
}
```

## Changes

- `twenty-sdk`: new `getAppServerVersion()` util reads
`twenty.serverVersion` from the cwd's `package.json`; `serverStart`
gains a `version` option and resolves `option → app pin → latest`,
building the image via `getImageForVersion()`; `docker:start [version]`
(and the deprecated `server start [version]` alias) wired up.
- `create-twenty-app`: template `package.json` ships
`twenty.serverVersion: "latest"`. (`create-app` and the scaffolder are
otherwise untouched.)
- Docs: `local-server.mdx` documents version selection and the opt-in
pin.

## Behavior notes

- Default with no pin and no flag is `latest` — same as today.
- Version only matters when **creating** a fresh container — an existing
container keeps its image until `docker:upgrade` / `docker:reset`.

## Testing

- New unit tests for `getAppServerVersion` (5 cases).
- Extended the `app-template` scaffolding test to assert the `latest`
default.
- `twenty-sdk` cli vitest suite (273) and `create-twenty-app` jest suite
(9) pass; oxlint + oxfmt clean on changed files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21690?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-17 10:55:02 +02:00
martmull 306a1454aa Update Connection provider path (#21678)
## Before

After connecting to oAuth linear app connection:

<img width="1512" height="851" alt="image"
src="https://github.com/user-attachments/assets/39b94aaf-648f-46a6-8f4d-deb1cb7e22c5"
/>

## After

Redirects to Linear

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21678?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-16 13:53:13 +00:00
Paul Rastoin fdab89ae02 Move twenty-client-sdk to dev dep (#21611)
# Introduction
The `twenty-client-sdk` is always provided and injected at runtime by
the twenty-server instance
Which mean that even if in your app locally you're using
twenty-client-sdk `1.0` installing this app on twenty instance `2.0`
will result in injecting another `twenty-client-sdk`

That's the expected behavior and tradeof

The twenty-app devdep should only be used to guide local devxp following
typesafety and so on

A user can still locally generated its own twenty-client-sdk and publish
it if necessary

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21611?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-15 14:46:22 +00:00
martmull 5207493cda Add useColorScheme hook to twenty-sdk (#21595)
Ability to update front compoonent design according to the dark or white
theme of the UI

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21595?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-15 12:15:28 +00:00
martmull e293c33311 Normalize defaultValue properly (#21511)
<img width="1506" height="338" alt="image"
src="https://github.com/user-attachments/assets/3ee0d5c9-af32-4ef3-83c9-2f4671219126"
/>
2026-06-12 23:10:51 +02:00
martmull fa9aeea408 Add dev:generate-client command to sdk (#21489)
## after

`yarn twenty dev:generate-client`

<img width="1149" height="246" alt="image"
src="https://github.com/user-attachments/assets/1edcba03-2647-4bc8-8188-7ad69362ac52"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21489?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-12 12:58:20 +00:00
martmull 27aea728df 2474 add a utils to perform route trigger logic function requests in front components (#21330)
- exports `RestApiClient` from 'twenty-client-sdk/rest';`
- documents the rest client
2026-06-08 16:41:45 +00:00
martmull 77d1e8ced6 feat(app-dev): sync error hints, flatEntity labels, dev-mode summary UI, and docs (#21252)
Split out of #21240 — all remaining app-dev improvements. Stacked on
#21251 (review/merge that first).

- Actionable recovery hints on failed syncs; unified diff renderer;
`--dry-run` guard.
- Return `flatEntity` on update/delete sync actions and unify the diff
label.
- Summarize the dev-mode entity list unless `--verbose`.
- Docs: syncing & recovery guide + dry-run + open-an-issue prompt.
- Live execution mode for synced logic functions; clearer manifest
warnings.

<img width="1018" height="700" alt="image"
src="https://github.com/user-attachments/assets/5e9ce19e-0f1d-4f99-8524-4e118bde932b"
/>
2026-06-08 15:43:28 +00:00
martmull c2ca90c255 feat(sdk): add runAgent() to run app agents from logic functions (#21157)
<img width="948" height="593" alt="image"
src="https://github.com/user-attachments/assets/d990fa98-3cfd-469d-ab7f-0b2d4ccf3afc"
/>

<img width="1361" height="802" alt="image"
src="https://github.com/user-attachments/assets/1091f598-49f3-4c16-92ea-1e1c200181e2"
/>


## Add `runAgent()` to the Logic Function SDK

Lets an app's logic function run one of its own AI agents server-side
and get the result back synchronously — reusing the existing agent
executor instead of a new bespoke transport.

  ### Backend
- New **`runAgent` GraphQL mutation** (metadata schema) in
`ai-agent-execution`, wrapping the existing
`AgentAsyncExecutorService.executeAgent`. Scopes the agent lookup to the
calling
  application and runs it under an application auth context.
- New `@AuthApplication()` param decorator (mirrors `@AuthWorkspace()`)
— first GraphQL resolver authenticated by an **application access
token**.
- Guarded by `WorkspaceAuthGuard` +
`SettingsPermissionGuard(PermissionFlagType.AI)`: the app's role must
grant the `AI` permission flag.

  ### SDK
- `runAgent({ agentUniversalIdentifier, prompt })` posts the mutation to
`/metadata` with the app token via a new runtime GraphQL transport.
Returns `{ result, hasNoMoreAvailableCredits
  }`.
- Refactored the connections helpers onto a shared `postAppEndpoint`
util (removes duplicated transport logic).

  ### Frontend
- App install permission modal now shows an explicit consent line —
_"Run AI agents and bill AI credits to your workspace"_ — when the app's
role requests the `AI` flag.

  ### Docs
- Documented `runAgent` and its `AI` permission-flag requirement in
_Skills & Agents_.
- Fixed outdated role-permission examples in _Roles & Permissions_
(`permissionFlags` → `permissionFlagUniversalIdentifiers`,
`PermissionFlag` → `SystemPermissionFlag`).

  ### Test plan
- [x] SDK unit tests (`run-agent.spec.ts`) — request shape, GraphQL/HTTP
error handling, missing env vars
- [x] `twenty-server`, `twenty-front`, `twenty-shared` typecheck + lint
- [ ] Manual: install an app granting the `AI` flag, call `runAgent()`
from a logic function, confirm the agent runs and credits are billed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-06-04 16:18:27 +00:00
martmull e0d42323af Add more control on http trigger (#21216)
add "new Response" utils to define response code or content type of http
route triggered logic function responses

follow up of https://github.com/twentyhq/twenty/pull/21214

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:34:10 +00:00
martmull 0671ff3de5 Fix lambda error (#21179)
- move twenty-sdk from dependencies to devDependencies
- add documentation about breaking
- add warning about moving the package to dev dependencies
2026-06-03 16:45:51 +00:00
Raphaël Bosi b2539f5b6a Prevent conditional availability variables from being used at runtime (#21110)
Fixes https://github.com/twentyhq/twenty/issues/21094

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

- Move conditional availability variables from
`twenty-sdk/front-component` to `twenty-sdk/define`.
- Add a build-time manifest validation
(validate-conditional-availability-usage) that fails the build if these
variables are imported/used outside of
`conditionalAvailabilityExpression`.
- Update the github-connector example app to register commands via
dedicated *.command-menu-item.ts files instead of inline command config
in front components.
- Update docs (all locales) and test mocks to reflect the new import
paths.
2026-06-02 11:22:38 +00:00
Félix Malfait 7d7f32b243 docs: remove the self-host cloud providers page (#21134)
## What

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

## Changes

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

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

## Verification

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

> Note: `mintlify broken-links` can't run to completion on this branch
due to a **pre-existing** MDX parse error in the unrelated
`l/ar/.../contribute/contribute.mdx`; the grep above is the equivalent
guarantee that no link points at the removed page.
2026-06-02 11:17:37 +02:00
Raphaël Bosi 0ed2e9d82d Docs: clarify numberOfSelectedRecords usage for RECORD_SELECTION items (#21059)
Add a note to the command menu items docs explaining that
RECORD_SELECTION already guarantees a non-empty selection, so
numberOfSelectedRecords > 0 is redundant in
conditionalAvailabilityExpression.
2026-05-29 17:59:14 +02:00
Raphaël Bosi 57118a868f Docs update: Calling a logic function from a front component (#21057)
Documents how a headless front component calls a server-side logic
function over HTTP via the /s/ route, so AI agents have a clear
reference for implementing this pattern.
2026-05-29 14:04:19 +00:00
martmull 6ea637d6c5 Export STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS (#21010)
fix https://discord.com/channels/1130383047699738754/1509086323464474705
2026-05-28 12:51:01 +00:00
Weiko 08682fb3f5 Various fixes - some AI findings (#20921)
App permissions tab:
- The fallback uuidv4() for a marketplace field was generated twice, so
id and universalIdentifier could diverge; it's now computed once and
reused as it seemed to be the intention (even though I don't really
think it's a good idea)
- Renamed buildobjectMetadataItemsFromMarketplaceApp →
buildObjectMetadataItemsFromMarketplaceApp to follow camelCase.

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

Docs
- The UUID field-type row in views.mdx only listed IS; updated to the
full set supported by FILTER_OPERANDS_MAP (IS, IS_NOT, IS_EMPTY,
IS_NOT_EMPTY).
2026-05-26 14:43:50 +00:00
Félix Malfait d602f35cbd feat(data-model): custom-indexes management UI and mutations (#20846)
## Summary

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

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

## What ships

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

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

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

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

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

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

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

## Test plan
- [ ] `npx nx typecheck twenty-server / twenty-front / twenty-sdk /
twenty-shared` — passes
- [ ] `npx nx lint:diff-with-main twenty-server / twenty-front` — clean
- [ ] `npx jest index-metadata.service.spec` — green
- [ ] `npx jest from-index-manifest-to-universal-flat-index` — green
(new converter spec, 8 cases)
- [ ] `npx vitest run
src/sdk/define/indexes/__tests__/define-index.spec.ts` (twenty-sdk) —
green (6 cases)
- [ ] `npx vitest run --config vitest.integration.config.ts -t
"rich-app"` — green (rich-app app-dev integration exercises the new
manifest path with the PostCard.status index)
- [ ] Advanced mode → Settings → any object → Settings tab → Indexes
section is visible under Search
- [ ] Create a single-field BTREE index, confirm SQL index exists
(verify via `pg_indexes`)
- [ ] Create a composite-field index (`Address > City`) and confirm the
column is `addressAddressCity`
- [ ] Create an index spanning two columns; column order matches the
picker order
- [ ] Attempt to create an 11th custom index → button is disabled with
tooltip
- [ ] Delete a custom index → confirmation modal → row disappears, PG
index dropped
- [ ] System indexes have no trash icon and are hidden by default
2026-05-25 17:47:09 +02:00
Charles Bochet 91ce59d8e2 refactor(jwt): gate signing-key auto-rotation cron on SIGNING_KEY_ROTATION_DAYS (#20866)
Only register the JWT signing-key rotation cron when
`SIGNING_KEY_ROTATION_DAYS` is set, and move that variable to Advanced
Settings.
2026-05-23 09:51:06 +00:00
nitin 068d365731 feat(sdk): error on incompatible view filter operand at sync time (#20763)
view filters with mismatched operand + field type now error at sync --
was silently failing before
2026-05-22 09:01:40 +00:00
martmull 237a943947 Update twenty sdk commands (#20735)
Performs twenty-sdk cli command migration:

Summary

``` ┌─────┬──────────────────────────┬────────────────────────────┬───────────────────────┐
 │  #  │       Old command        │        New command         │        Status         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 1   │ twenty dev [appPath]     │ twenty dev [appPath]       │ Unchanged (now also   │
 │     │                          │                            │ DEFAULT)              │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 2   │ twenty dev --once        │ twenty dev --once          │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 3   │ twenty dev --watch       │ twenty dev [appPath]       │ --watch flag removed  │
 │     │ [appPath]                │                            │ (was default)         │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 4   │ twenty dev --verbose     │ twenty dev --verbose       │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 5   │ twenty dev --debug       │ twenty dev --debug         │ Unchanged             │
 │     │ [appPath]                │ [appPath]                  │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 6   │ twenty dev --debounceMs  │ twenty dev --debounceMs    │ Unchanged             │
 │     │ <ms> [appPath]           │ <ms> [appPath]             │                       │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 7   │ twenty build [appPath]   │ twenty dev:build [appPath] │ Deprecated → colon    │
 │     │                          │                            │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 8   │ twenty build --tarball   │ twenty dev:build --tarball │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 9   │ twenty typecheck         │ twenty dev:typecheck       │ Deprecated → colon    │
 │     │ [appPath]                │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 10  │ twenty logs [appPath]    │ twenty dev:fn-logs         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 11  │ twenty logs -n <name>    │ twenty dev:fn-logs -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 12  │ twenty logs -u <id>      │ twenty dev:fn-logs -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 13  │ twenty exec [appPath]    │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │                          │ [appPath]                  │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 14  │ twenty exec -n <name>    │ twenty dev:fn-exec -n      │ Deprecated → colon    │
 │     │ [appPath]                │ <name> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 15  │ twenty exec -u <id>      │ twenty dev:fn-exec -u <id> │ Deprecated → colon    │
 │     │ [appPath]                │  [appPath]                 │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 16  │ twenty exec -p <json>    │ twenty dev:fn-exec -p      │ Deprecated → colon    │
 │     │ [appPath]                │ <json> [appPath]           │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 17  │ twenty exec              │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │ --postInstall [appPath]  │ --postInstall [appPath]    │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 18  │ twenty exec --preInstall │ twenty dev:fn-exec         │ Deprecated → colon    │
 │     │  [appPath]               │ --preInstall [appPath]     │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 19  │ twenty add [entityType]  │ twenty dev:add             │ Deprecated → colon    │
 │     │                          │ [entityType]               │ command               │
 ├─────┼──────────────────────────┼────────────────────────────┼───────────────────────┤
 │ 20  │ twenty add --path <path> │ twenty dev:add --path      │ Deprecated → colon    │
 │     │  [entityType]            │ <path> [entityType]        │ command               │
 └─────┴──────────────────────────┴────────────────────────────┴───────────────────────┘

 App lifecycle commands

 ┌─────┬────────────────────────┬────────────────────────────┬─────────────────────────┐
 │  #  │      Old command       │        New command         │         Status          │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 21  │ twenty publish         │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 22  │ twenty publish --tag   │ twenty app:publish --tag   │ Deprecated → colon      │
 │     │ <tag> [appPath]        │ <tag> [appPath]            │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 23  │ twenty deploy          │ twenty app:publish         │ Deprecated → colon      │
 │     │ [appPath]              │ --private [appPath]        │ command + --private     │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 24  │ twenty install         │ twenty app:install         │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 25  │ twenty uninstall       │ twenty app:uninstall       │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 ├─────┼────────────────────────┼────────────────────────────┼─────────────────────────┤
 │ 26  │ twenty uninstall -y    │ twenty app:uninstall -y    │ Deprecated → colon      │
 │     │ [appPath]              │ [appPath]                  │ command                 │
 └─────┴────────────────────────┴────────────────────────────┴─────────────────────────┘

 Server commands

 ┌─────┬─────────────────────────┬─────────────────────────────┬──────────────────────┐
 │  #  │       Old command       │         New command         │        Status        │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 27  │ twenty server start     │ twenty docker:start         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 28  │ twenty server start -p  │ twenty docker:start -p      │ Deprecated → colon   │
 │     │ <port>                  │ <port>                      │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 29  │ twenty server start     │ twenty docker:start --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 30  │ twenty server stop      │ twenty docker:stop          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 31  │ twenty server stop      │ twenty docker:stop --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 32  │ twenty server status    │ twenty docker:status        │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 33  │ twenty server status    │ twenty docker:status --test │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 34  │ twenty server logs      │ twenty docker:logs          │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 35  │ twenty server logs -n   │ twenty docker:logs -n       │ Deprecated → colon   │
 │     │ <lines>                 │ <lines>                     │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 36  │ twenty server logs      │ twenty docker:logs --test   │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 37  │ twenty server reset     │ twenty docker:reset         │ Deprecated → colon   │
 │     │                         │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 38  │ twenty server reset     │ twenty docker:reset --test  │ Deprecated → colon   │
 │     │ --test                  │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 39  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ [version]               │ [version]                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 40  │ twenty server upgrade   │ twenty docker:upgrade       │ Deprecated → colon   │
 │     │ --test [version]        │ --test [version]            │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 41  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync            │                             │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 42  │ twenty server           │ twenty app:catalog-sync  │ Deprecated → colon   │
 │     │ catalog-sync -r <name>  │ -r <name>                   │ syntax               │
 ├─────┼─────────────────────────┼─────────────────────────────┼──────────────────────┤
 │ 43  │ twenty catalog-sync     │ (removed)                   │ Removed (was already │
 │     │                         │                             │  deprecated)         │
 └─────┴─────────────────────────┴─────────────────────────────┴──────────────────────┘

 Remote commands

 ┌─────┬────────────────────────┬──────────────────────────┬──────────────────────────┐
 │  #  │      Old command       │       New command        │          Status          │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 44  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 45  │ twenty remote add --as │ twenty remote:add --as   │ Deprecated → colon       │
 │     │  <name>                │ <name>                   │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 46  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-key <key>        │ --api-key <key>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 47  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --api-url <url>        │ --api-url <url>          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 48  │ twenty remote add      │ twenty remote:add        │ Deprecated → colon       │
 │     │ --local                │ --local                  │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 49  │ twenty remote add      │ twenty remote:add --test │ Deprecated → colon       │
 │     │ --test                 │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 50  │ twenty remote list     │ twenty remote:list       │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 51  │ twenty remote switch   │ twenty remote:use [name] │ Deprecated → colon       │
 │     │ [name]                 │                          │ syntax + renamed         │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 52  │ twenty remote status   │ twenty remote:status     │ Deprecated → colon       │
 │     │                        │                          │ syntax                   │
 ├─────┼────────────────────────┼──────────────────────────┼──────────────────────────┤
 │ 53  │ twenty remote remove   │ twenty remote:remove     │ Deprecated → colon       │
 │     │ <name>                 │ <name>                   │ syntax                   │
 └─────┴────────────────────────┴──────────────────────────┴──────────────────────────┘
```

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-20 15:12:39 +00:00
nitin a26fe3bb65 docs(sdk): document DatabaseEventPayload and simplify its type (#20754)
closes
https://discord.com/channels/1130383047699738754/1505967920163983502

Update logic-function docs to match the real `DatabaseEventPayload`
shape.

The docs now show database event payloads as record-level events with
`recordId` and `properties.before/after/diff/updatedFields`, including
compact examples for created, updated, and destroyed events. Route
payload type imports now use the preferred `twenty-sdk/logic-function`
surface.

Also clean up the shared payload type wrapper so it models event
metadata without over-promising actor fields; `userId`,
`userWorkspaceId`, and `workspaceMemberId` remain optional through the
underlying event type.
2026-05-20 09:22:58 +00:00
martmull d5ff9eb515 Create twenty app improvements (#20688)
create-twenty-app updates:
- remove --example option
- sync --once when scaffolding an applicaiton
- rename --api-url option to --workspace-url
- create a standalone page when scaffolding an app
<img width="1494" height="765" alt="image"
src="https://github.com/user-attachments/assets/0e35ed0c-b0aa-466c-9f56-7939294fd2cf"
/>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-19 13:12:47 +00:00
Charles Bochet 05f31c1837 docs(self-host): document ENCRYPTION_KEY, FALLBACK_ENCRYPTION_KEY and key rotation procedures (#20611)
## Summary
- Documents the new at-rest encryption envelope (`ENCRYPTION_KEY` /
`FALLBACK_ENCRYPTION_KEY`) introduced in v2.5+ and clarifies its
relationship to the legacy `APP_SECRET`-as-encryption-key path.
- Adds a new dedicated **Key rotation** guide covering manual /
Enterprise-cron JWT signing-key rotation, signing-key revocation, and
the online `ENCRYPTION_KEY` rotation procedure (including the new
\`secret-encryption:rotate\` CLI shipped in a follow-up PR).
- Updates the docker-compose quickstart to generate a dedicated
\`ENCRYPTION_KEY\` from day 1.
- Mentions the v2.5+ enc:v2 backfill in the upgrade guide.

English-only — the localized mirrors will be picked up by i18n CI.

## Test plan
- [ ] Mintlify build passes locally / in CI
- [ ] Sidebar entry renders under **Self-Host → Key rotation**
- [ ] Internal links to /developers/self-host/capabilities/key-rotation
resolve from setup.mdx, docker-compose.mdx and upgrade-guide.mdx

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-19 06:45:40 +00:00
nitin db0547f503 [1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)
Split of #20377.

## Summary

This PR separates available permission flags from per-role permission
flag grants.

Previously, `core.permissionFlag` stored the role assignment directly:
`roleId + flag`. This PR renames that legacy grant table to
`core.rolePermissionFlag`, then recreates `core.permissionFlag` as the
catalog of available permission flags.

## What changed

- Rename the existing `core.permissionFlag` grant table to
`core.rolePermissionFlag`.
- Add the new syncable `core.permissionFlag` catalog entity with key,
label, description, icon, permission type, relevance flags, and
custom/standard metadata.
- Add stable `SystemPermissionFlag` universal identifiers for the
built-in `PermissionFlagType` values.
- Seed the standard permission flags for every workspace under the
Twenty standard application.
- Backfill existing role grants:
  - create missing catalog rows for existing grant keys,
  - add `rolePermissionFlag.permissionFlagId`,
- migrate grants from the old string `flag` column to the new catalog
FK,
- replace the old `(flag, roleId)` uniqueness with `(permissionFlagId,
roleId)`.
- Rewire role permission flag caches, permission checks, role DTO
mapping, and `upsertPermissionFlags` to resolve through the catalog.
- Keep the existing public role permission API shape: product/app
surfaces still talk about `permissionFlags` and return `{ id, roleId,
flag }`.
- Update metadata flat-entity machinery, migration builders, validators,
action handlers, snapshots, generated schemas, docs, and app fixtures
for the new `permissionFlag` / `rolePermissionFlag` split.

## Behavior after this PR

- Existing permission flag grants keep working.
- Existing GraphQL role permission flows keep the same public naming.
- Standard permission flags are represented as catalog rows.
- Permission checks now compare grants through catalog universal
identifiers instead of the legacy `flag` column.
- Workspace deletion cleanup now verifies both `permissionFlag` and
`rolePermissionFlag`.

## What is not in this PR

- Public GraphQL CRUD for custom permission flags.
- App manifest support for declaring new custom permission flags.
- Frontend UI for creating or assigning custom permission flags beyond
the existing role permission flow.

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-05-18 11:57:47 +00:00
martmull 0cc2194399 Simplify create-twenty-app command (#20512)
## Simplify `create-twenty-app` for zero-interaction use

Makes `npx create-twenty-app@latest my-app` a fully non-interactive,
single-command experience suitable for automated environments (Codex,
Claude plugins).

  ### Changes

- **Remove all interactive prompts** — app name, display name,
description, and scaffold confirmation are now derived from CLI args
with sensible defaults. `inquirer` dependency removed
   entirely.
- **Replace OAuth with API key auth** — use the seeded dev API key
(`DEV_API_KEY`) to authenticate against the Docker instance as
`tim@apple.dev`, eliminating the browser-based OAuth
  flow.
- **Docker-first with early validation** — check Docker is installed
before scaffolding; if missing, print the install URL and exit. Detect
alternative runtimes (Podman, nerdctl).
- **Parallel image pull** — `docker pull` runs in the background during
scaffold + dependency install, saving 10-30s on typical runs.
- **Always pull latest image** — ensures the dev server is up-to-date on
every run.
- **Stop detecting port 3000** — only check port 2020 (Docker instance).
- **Update CLI flags** — remove `--skip-local-instance` and `--yes`; add
`--skip-docker`.
- **Update CI workflows and docs** — align e2e workflows, package
README, and template README/cd.yml with the new flow.
2026-05-13 16:44:27 +00:00
martmull dea1f89904 Inject none secret env variables into front components (#20511)
## Summary
- Inject non-secret application variables (`isSecret: false`) into front
component `process.env` via the existing Web Worker `setWorkerEnv`
mechanism
- Filter secret variables server-side in the resolver so they never
reach the browser
- Set application variables before system variables (`TWENTY_API_URL`,
`TWENTY_APP_ACCESS_TOKEN`) to prevent override
- Wire up environment variable keys in the logic function code editor
for TypeScript autocomplete

  ## Test plan
  - [x] Unit tests for `buildNonSecretEnvVar` (6 passing)
  - [x] Typecheck passes for `twenty-front` and `twenty-server`
- [x] Install an app with both `isSecret: false` and `isSecret: true`
variables, open a front component, verify only non-secret vars appear in
`process.env`
- [x] Open a logic function editor, verify autocomplete suggests
declared variable keys
2026-05-13 16:27:56 +00:00
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
Abdul Rahman 4aca4d1143 Add defineApplicationRole method (#20314)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 18:55:13 +00:00
Jonathan Bredo 2eae25dc34 Improved create-twenty-app documentation for AI coding agents (#20325)
Added a bit of enhanced context for better agentic coding, based on this
[Discord
conversation](https://discord.com/channels/1130383047699738754/1130383048173682821/1501538550301331477).

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-05-07 14:31:27 +02:00
Abdul Rahman cbd2a017da Improve app gallery image sizing (no cropping) (#20287)
<img width="908" height="808" alt="Screenshot 2026-05-05 at 7 38 46 PM"
src="https://github.com/user-attachments/assets/a6f0a9b7-f676-46a3-8642-48c4bd06c7f4"
/>
2026-05-05 17:28:39 +00:00
Félix Malfait 633553f729 feat(sdk): add defineCommandMenuItem (#20256)
## Summary

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

### Why

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

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

## Test plan

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

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-05 14:16:04 +00:00
Félix Malfait f9a24072b7 docs: restructure Getting Started around three explicit phases (#20283)
## Summary

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

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

## What changes


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

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

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


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

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

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

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

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

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

This PR moves it under `server`:

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

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

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

## Help output (after)

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

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

## Backwards compatibility

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

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

## Possible follow-ups

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 15:23:08 +02:00
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
Paul Rastoin 820f97f53d [Headless Front component] Support multiple selected record (#20268)
# Introduction

Support multiple selected record ids for headless front components

### Changes

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

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

Backward compatibility is preserved
2026-05-05 14:53:22 +02:00
martmull 54e22423df Improve twenty deploy cli logs (#20237)
## Before
<img width="1074" height="562" alt="image"
src="https://github.com/user-attachments/assets/a2fbe902-d34e-40e4-87c9-f344a06fd6ae"
/>

## After

<img width="1107" height="605" alt="image"
src="https://github.com/user-attachments/assets/af78276a-f4c7-42f9-9347-01d562b1a779"
/>
2026-05-04 15:25:49 +00:00
martmull 3ffda0a29e Add twenty version validation (#20227)
as title, server version is checked before app deploy, and app install
commands

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 13:55:31 +00:00
Félix Malfait 9e94045fa5 feat(apps): generic OAuth provider support for app SDK (#20181)
## Summary

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

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

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

## Architecture

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

## Reference app

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

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

## Tests

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

## Test plan

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

## Out of scope (deliberately)

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

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-04 11:26:34 +02:00
martmull d8bd717f1f Update doc screenshots (#20160)
fixes
https://discord.com/channels/1130383047699738754/1496579088889024542
2026-04-30 14:54:49 +00:00
martmull c8e405cb4e Add twenty sdk server upgrade command (#20158)
##
The command pulls the image, compares it against the one the container
was created from, and only recreates the container if the image actually
changed. Your data volumes are preserved — only the container is
replaced.
2026-04-30 13:03:41 +00:00