## Summary
Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.
This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.
## What changes
- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
- Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.
## Why this is safe
- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.
## Measurements (github-connector)
| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.
## Summary
- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).
## What's in this PR
**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.
**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.
**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.
**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.
**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.
**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.
**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.
## Stats
- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean
## Behavior deltas
- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.
## Risks
- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.
## Test plan
- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.
## What's NOT in this PR (PR 2 scope)
- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.
This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:
- **Reorganized SDK source.** All server-side definition factories now
live under `src/sdk/define/` (agents, application, fields,
logic-functions, objects, page-layouts, roles, skills, views,
navigation-menu-items, etc.). All front-component runtime
(components, hooks, host APIs, command primitives) lives under
`src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
removed; the bare `twenty-sdk` entry no longer exists.
- **Split the build configs by purpose / runtime env.** Replaced
`vite.config.sdk.ts` with two purpose-specific configs:
- `vite.config.define.ts` — node target, externals from package
`dependencies`, emits to `dist/define/**`
- `vite.config.front-component.ts` — browser/React target, emits to
`dist/front-component/**`
Both use `preserveModules: true` so each leaf ships as its own `.mjs`.
- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
unreferenced re-exports.
- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
at the new per-module dist layout.
- **Migrated every internal/example/community app** to the new subpath
imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
`twenty-sdk/ui`).
- **Added \`bundle-investigation\` internal app** that reproduces the
bundle bloat and demonstrates the fix.
- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
front-component story builder, the call-recording app, and the SDK
tsconfig.
## Bundle size impact
Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):
| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |
That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.
## Test plan
- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green
Made with [Cursor](https://cursor.com)
## Summary
- `twenty-shared` is private and never published to npm, so SDK
consumers couldn't resolve type imports like `from
'twenty-shared/types'` in the generated `.d.ts` files
- Replace `vite.config.sdk.ts` with `rollup-plugin-dts` which compiles
`src/sdk/index.ts` directly into a self-contained `dist/sdk/index.d.ts`
with all `twenty-shared` types inlined
- The JS output from `vite.config.sdk.ts` (`dist/sdk/*.js`) was unused —
the main export already maps to `dist/index.mjs` from the node Vite
config
## Changes
- **Deleted** `vite.config.sdk.ts` — its preserved-module JS output
wasn't referenced by any `package.json` export
- **Added** `rollup.config.dts.mjs` — uses `rollup-plugin-dts` to
compile SDK types from source with `twenty-shared` inlined (~850ms)
- **Updated** `project.json` — build/dev/build:sdk targets now use
rollup instead of the removed vite config
- **Updated** `tsconfig.json` — removed `vite.config.sdk.ts` from
include
## Test plan
- [ ] Run `npx nx build twenty-sdk` and verify `dist/sdk/index.d.ts`
contains no `twenty-shared` references
- [ ] Verify `dist/index.mjs` and `dist/index.cjs` are still produced
correctly
- [ ] Verify CLI (`dist/cli.cjs`) still works
- [ ] Verify `npx nx build:sdk twenty-sdk` works standalone
Made with [Cursor](https://cursor.com)
## 1. The `twenty-client-sdk` Package (Source of Truth)
The monorepo package at `packages/twenty-client-sdk` ships with:
- A **pre-built metadata client** (static, generated from a fixed
schema)
- A **stub core client** that throws at runtime (`CoreApiClient was not
generated...`)
- Both ESM (`.mjs`) and CJS (`.cjs`) bundles in `dist/`
- A `package.json` with proper `exports` map for
`twenty-client-sdk/core`, `twenty-client-sdk/metadata`, and
`twenty-client-sdk/generate`
## 2. Generation & Upload (Server-Side, at Migration Time)
**When**: `WorkspaceMigrationRunnerService.run()` executes after a
metadata schema change.
**What happens in `SdkClientGenerationService.generateAndStore()`**:
1. Copies the stub `twenty-client-sdk` package from the server's assets
(resolved via `SDK_CLIENT_PACKAGE_DIRNAME` — from
`dist/assets/twenty-client-sdk/` in production, or from `node_modules`
in dev)
2. Filters out `node_modules/` and `src/` during copy — only
`package.json` + `dist/` are kept (like an npm publish)
3. Calls `replaceCoreClient()` which uses `@genql/cli` to introspect the
**application-scoped** GraphQL schema and generates a real
`CoreApiClient`, then compiles it to ESM+CJS and overwrites
`dist/core.mjs` and `dist/core.cjs`
4. Archives the **entire package** (with `package.json` + `dist/`) into
`twenty-client-sdk.zip`
5. Uploads the single archive to S3 under
`FileFolder.GeneratedSdkClient`
6. Sets `isSdkLayerStale = true` on the `ApplicationEntity` in the
database
## 3. Invalidation Signal
The `isSdkLayerStale` boolean column on `ApplicationEntity` is the
invalidation mechanism:
- **Set to `true`** by `generateAndStore()` after uploading a new client
archive
- **Checked** by both logic function drivers before execution — if
`true`, they rebuild their local layer
- **Set back to `false`** by `markSdkLayerFresh()` after the driver has
successfully consumed the new archive
Default is `false` so existing applications without a generated client
aren't affected.
## 4a. Logic Functions — Local Driver
**`ensureSdkLayer()`** is called before every execution:
1. Checks if the local SDK layer directory exists AND `isSdkLayerStale`
is `false` → early return
2. Otherwise, cleans the local layer directory
3. Calls `downloadAndExtractToPackage()` which streams the zip from S3
directly to disk and extracts the full package into
`<tmpdir>/sdk/<workspaceId>-<appId>/node_modules/twenty-client-sdk/`
4. Calls `markSdkLayerFresh()` to set `isSdkLayerStale = false`
**At execution time**, `assembleNodeModules()` symlinks everything from
the deps layer's `node_modules/` **except** `twenty-client-sdk`, which
is symlinked from the SDK layer instead. This ensures the logic
function's `import ... from 'twenty-client-sdk/core'` resolves to the
generated client.
## 4b. Logic Functions — Lambda Driver
**`ensureSdkLayer()`** is called during `build()`:
1. Checks if `isSdkLayerStale` is `false` and an existing Lambda layer
ARN exists → early return
2. Otherwise, deletes all existing layer versions for this SDK layer
name
3. Calls `downloadArchiveBuffer()` to get the raw zip from S3 (no disk
extraction)
4. Calls `reprefixZipEntries()` which streams the zip entries into a
**new zip** with the path prefix
`nodejs/node_modules/twenty-client-sdk/` — this is the Lambda layer
convention path. All done in memory, no disk round-trip
5. Publishes the re-prefixed zip as a new Lambda layer via
`publishLayer()`
6. Calls `markSdkLayerFresh()`
**At function creation**, the Lambda is created with **two layers**:
`[depsLayerArn, sdkLayerArn]`. The SDK layer is listed last so it
overwrites the stub `twenty-client-sdk` from the deps layer (later
layers take precedence in Lambda's `/opt` merge).
## 5. Front Components
Front components are built by `app:build` with `twenty-client-sdk/core`
and `twenty-client-sdk/metadata` as **esbuild externals**. The stored
`.mjs` in S3 has unresolved bare import specifiers like `import {
CoreApiClient } from 'twenty-client-sdk/core'`.
SDK import resolution is split between the **frontend host** (fetching &
caching SDK modules) and the **Web Worker** (rewriting imports):
**Server endpoints**:
- `GET /rest/front-components/:id` —
`FrontComponentService.getBuiltComponentStream()` returns the **raw
`.mjs`** directly from file storage. No bundling, no SDK injection.
- `GET /rest/sdk-client/:applicationId/:moduleName` —
`SdkClientController` reads a single file (e.g. `dist/core.mjs`) from
the generated SDK archive via
`SdkClientGenerationService.readFileFromArchive()` and serves it as
JavaScript.
**Frontend host** (`FrontComponentRenderer` in `twenty-front`):
1. Queries `FindOneFrontComponent` which returns `applicationId`,
`builtComponentChecksum`, `usesSdkClient`, and `applicationTokenPair`
2. If `usesSdkClient` is `true`, renders
`FrontComponentRendererWithSdkClient` which calls the
`useApplicationSdkClient` hook
3. `useApplicationSdkClient({ applicationId, accessToken })` checks the
Jotai atom family cache for existing blob URLs. On cache miss, fetches
both SDK modules from `GET /rest/sdk-client/:applicationId/core` and
`/metadata`, creates **blob URLs** for each, and stores them in the atom
family
4. Once the blob URLs are cached, passes them as `sdkClientUrls`
(already blob URLs, not server URLs) to `SharedFrontComponentRenderer` →
`FrontComponentWorkerEffect` → worker's `render()` call via
`HostToWorkerRenderContext`
**Worker** (`remote-worker.ts` in `twenty-sdk`):
1. Fetches the raw component `.mjs` source as text
2. If `sdkClientUrls` are provided and the source contains SDK import
specifiers (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`),
**rewrites** the bare specifiers to the blob URLs received from the host
(e.g. `'twenty-client-sdk/core'` → `'blob:...'`)
3. Creates a blob URL for the rewritten source and `import()`s it
4. Revokes only the component blob URL after the module is loaded — the
SDK blob URLs are owned and managed by the host's Jotai cache
This approach eliminates server-side esbuild bundling on every request,
caches SDK modules per application in the frontend, and keeps the
worker's job to a simple string rewrite.
## Summary Diagram
```
app:build (SDK)
└─ twenty-client-sdk stub (metadata=real, core=stub)
│
▼
WorkspaceMigrationRunnerService.run()
└─ SdkClientGenerationService.generateAndStore()
├─ Copy stub package (package.json + dist/)
├─ replaceCoreClient() → regenerate core.mjs/core.cjs
├─ Zip entire package → upload to S3
└─ Set isSdkLayerStale = true
│
┌────────┴────────────────────┐
▼ ▼
Logic Functions Front Components
│ │
├─ Local Driver ├─ GET /rest/sdk-client/:appId/core
│ └─ downloadAndExtract │ → core.mjs from archive
│ → symlink into │
│ node_modules ├─ Host (useApplicationSdkClient)
│ │ ├─ Fetch SDK modules
└─ Lambda Driver │ ├─ Create blob URLs
└─ downloadArchiveBuffer │ └─ Cache in Jotai atom family
→ reprefixZipEntries │
→ publish as Lambda ├─ GET /rest/front-components/:id
layer │ → raw .mjs (no bundling)
│
└─ Worker (browser)
├─ Fetch component .mjs
├─ Rewrite imports → blob URLs
└─ import() rewritten source
```
## Next PR
- Estimate perf improvement by implementing a redis caching for front
component client storage ( we don't even cache front comp initially )
- Implem frontent blob invalidation sse event from server
---------
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
# Introduction
Verifies whole following flow:
- Create and sdk app build and publication
- Global create-twenty-app installation
- Creating an app
- installing app dependencies
- auth:login
- app:build
- function:execute
- Running successfully auto-generated integration tests
## Create twenty app options refactor
Allow having a flow that do not require any prompt
# Intoduction
Closes https://github.com/twentyhq/core-team-issues/issues/2289
In this PR all the clients becomes available under `twenty-sdk/clients`,
this is a breaking change but generated was too vague and thats still
the now or never best timing to do so
## CoreClient
The core client is now shipped with a default stub empty class for both
the schema and the client
Allowing its import, will still raises typescript errors when consumed
as generated but not generated
## MetadataClient
The metadata client is workspace agnostic, it's now generated and
commited in the repo. added a ci that prevents any schema desync due to
twenty-server additions
Same behavior than for the twenty-front generated graphql schema
## Summary
- **Merge queue optimization**: Created a dedicated
`ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on
`ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7
existing CI workflows (front, server, shared, website, sdk, zapier,
docker-compose). The merge queue goes from ~30+ parallel jobs to a
single focused E2E job.
- **Label-based merge queue simulation**: Added `run-merge-queue` label
support so developers can trigger the exact merge queue E2E pipeline on
any open PR before it enters the queue.
- **Prettier in lint**: Chained `prettier --check` into `lint` and
`prettier --write` into `lint --configuration=fix` across `nx.json`
defaults, `twenty-front`, and `twenty-server`. Prettier formatting
errors are now caught by `lint` and fixed by `lint:fix` /
`lint:diff-with-main --configuration=fix`.
## After merge (manual repo settings)
Update GitHub branch protection required status checks:
1. Remove old per-workflow merge queue checks (`ci-front-status-check`,
`ci-e2e-status-check`, `ci-server-status-check`, etc.)
2. Add `ci-merge-queue-status-check` as the required check for the merge
queue
## Add API client generation to SDK dev mode and refactor orchestrator
into step-based pipeline
### Why
The SDK dev mode lacked typed API client generation, forcing developers
to work without auto-generated GraphQL types when building applications.
Additionally, the orchestrator was a monolithic class that mixed watcher
management, token handling, and sync logic — making it difficult to
extend with new steps like client generation.
### How
- **Refactored the orchestrator** into a step-based pipeline with
dedicated classes: `CheckServer`, `EnsureValidTokens`,
`ResolveApplication`, `BuildManifest`, `UploadFiles`,
`GenerateApiClient`, `SyncApplication`, and `StartWatchers`. Each step
has typed input/output/status, managed by a new `OrchestratorState`
class.
- **Added `GenerateApiClientOrchestratorStep`** that detects
object/field schema changes and regenerates a typed GraphQL client (via
`@genql/cli`) into `node_modules/twenty-sdk/generated` for seamless
imports.
- **Replaced `checkApplicationExist`** with `findOneApplication` on both
server resolver and SDK API service, returning the entity data instead
of a boolean.
- **Added application token pair mutations**
(`generateApplicationToken`, `renewApplicationToken`) to the API
service, with the server now returning `ApplicationTokenPairDTO`
containing both access and refresh tokens.
- **Restructured the dev UI** into `dev/ui/components/` with dedicated
panel, section, and event log components.
- **Simplified `AppDevCommand`** from ~180 lines of watcher management
down to ~40 lines that delegate entirely to the orchestrator.
## Split twenty-sdk build into separate Node and browser targets
The SDK was bundling Node.js code (CLI, SDK API) and browser code (UI
components, front-component renderer) through a single Vite config. This
caused incorrect externalization — Node builtins leaked into browser
bundles and browser-specific chunking logic applied to CLI output.
This PR splits the build into `vite.config.node.ts` and
`vite.config.browser.ts` so each target gets the right externals and
output format.
Also includes a few housekeeping renames:
- `front-component` export path → `front-component-renderer` (matches
what it actually is)
- `front-component-common` merged into `front-component-api` (was a
needless extra module)
## Summary
- Removed `vite-plugin-dts` (which used `tsc` internally) from the Vite
build and replaced DTS generation with `tsgo` as a sequential post-build
step — **~0.7s vs 1-10s**.
- Disabled `reportCompressedSize` to skip gzip computation for 64 output
files.
- Converted the build target to an explicit `nx:run-commands` executor
with sequential `vite build` → `tsgo` commands.
The `twenty-emails:build` step goes from ~22s to ~7s under load.
## Test plan
- [x] `nx build twenty-emails` produces both JS (64 files) and DTS (74
files) correctly
- [x] `dist/index.d.ts` exports match the source `src/index.ts`
- [x] Full `nx build twenty-server` succeeds end-to-end
- [ ] CI build passes
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## PR description
This PR:
- Creates a TypeScript-based extractor that discovers all exported
twenty-ui components by scanning barrel files, extracting
props/slots/events via ts-morph type analysis, and generating the remote
DOM bindings automatically.
- Adds a new ESLint rule which enforces all *Props types in twenty-ui
components to be exported, which is required for the extractor to
discover component prop types. Existing twenty-ui components are updated
to comply with this rule.
- Extends the remote DOM generation to support slots, per-component
events, forwardRef wrappers, and richer property types (array, object,
function)
## Edge cases to fix in another PR
- Icons cannot be rendered inside buttons
- IconButtons throw an error when mounted
- MenuItems are not displayed correctly
- MenuItemNavigate throws on click
## Video Demo
https://github.com/user-attachments/assets/c2ed67cf-6a15-4896-9fec-e83fac0e862b
- An app will declare its `twenty-sdk` version in the manifest.
- `twenty-sdk` will be served by a CDN to be imported in front component
host.
- When we render the host we will load twenty-ui through the correct
version of the sdk
We will proceed this way because twenty-ui is not mature enough yet to
be deployed as its own package. So instead, since the sdk is already
deployed as its own package, we will serve the ui with it. It allows us
to have versioning to handle breaking changes in twenty-ui.
# Improve cross-entity duplicate detection in manifest validation
- Refactored findDuplicates to receive the full manifest, enabling
cross-entity duplicate checks
-ObjectExtensionEntityBuilder now validates that extension field IDs
don't conflict with object field IDs
- Renamed DuplicateId type to EntityIdWithLocation for clarity
- Updated all entity builders to use the new signature
## Description
This PR improves the developer experience for the `twenty-sdk` and
`create-twenty-app` packages by reorganizing commands, adding
development tooling, and improving documentation.
## Changes
### twenty-sdk
#### Command Refactoring
- **Renamed `app-watch` → `app-dev`**: Renamed `AppWatchCommand` to
`AppDevCommand` and moved to `app-dev.ts` for consistent naming with the
CLI command `app:dev`
- **Moved `app-add` → `entity-add`**: Relocated entity creation logic
from `app/app-add.ts` to `entity/entity-add.ts` and renamed
`AppAddCommand` to `EntityAddCommand` for better separation of concerns
#### Development Tooling
- **Added `dev` target**: New Nx target `npx nx run twenty-sdk:dev` that
runs the build in watch mode for faster development iteration
### create-twenty-app
#### Improved Scaffolded Project
- **Enhanced base-application README** with:
- Updated Getting Started to recommend `yarn dev` for development
- Added "Available Commands" section listing all available scripts
#### Better Onboarding
- **Improved success message** after app creation with formatted next
steps:
First PR to implement application tokens
- add new application role in twenty-server
- move duplicated constants and types to twenty-shared
- will add role configuration utils into twenty-sdk in another PR
# Introduction
Important note: for the moment testing this locally will require some
hack due to latest twenty-sdk not being published.
You will need to build twenty-cli and `cd packages/twenty-cli && yarn
link`
To finally sync the app in your app folder as `cd app-folder && twenty
app sync`
close https://github.com/twentyhq/core-team-issues/issues/1863
In this PR is introduced the generate sdk programmatic call to
[genql](https://genql.dev/) exposed in a `client` barrel of `twenty-sdk`
located in this package as there's high chances that will add a codegen
layer above it at some point ?
The cli calls this method after a sync application and writes a client
in a generated folder. It will make a graql introspection query on the
whole workspace. We should later improve that and only filter by current
applicationId and its dependencies ( when twenty-standard application is
introduced )
Fully typesafe ( input, output, filters etc ) auto-completed client
## Hello-world app serverless refactor
<img width="2480" height="1326" alt="image"
src="https://github.com/user-attachments/assets/b18ea372-b21d-4560-8fbc-1dc348427a95"
/>
---------
Co-authored-by: martmull <martmull@hotmail.fr>