diff --git a/packages/create-twenty-app/src/constants/template/package.json b/packages/create-twenty-app/src/constants/template/package.json
index dbcdf5482e..a41cd81902 100644
--- a/packages/create-twenty-app/src/constants/template/package.json
+++ b/packages/create-twenty-app/src/constants/template/package.json
@@ -16,15 +16,14 @@
"test": "vitest run",
"test:watch": "vitest"
},
- "dependencies": {
- "twenty-client-sdk": "TO-BE-GENERATED"
- },
+ "dependencies": {},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^19.0.0",
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "twenty-client-sdk": "TO-BE-GENERATED",
"twenty-sdk": "TO-BE-GENERATED",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
diff --git a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts
index 3882ec76df..3f15a09254 100644
--- a/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts
+++ b/packages/create-twenty-app/src/utils/__tests__/app-template.spec.ts
@@ -32,10 +32,9 @@ const TEMPLATE_PACKAGE_JSON = {
version: '0.1.0',
license: 'MIT',
scripts: { twenty: 'twenty' },
- dependencies: {
- 'twenty-client-sdk': '0.0.0',
- },
+ dependencies: {},
devDependencies: {
+ 'twenty-client-sdk': '0.0.0',
'twenty-sdk': '0.0.0',
},
};
@@ -152,7 +151,7 @@ describe('copyBaseApplicationProject', () => {
expect(packageJson.devDependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
- expect(packageJson.dependencies['twenty-client-sdk']).toBe(
+ expect(packageJson.devDependencies['twenty-client-sdk']).toBe(
createTwentyAppPackageJson.version,
);
});
diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts
index caccd952cf..ca2801f97d 100644
--- a/packages/create-twenty-app/src/utils/app-template.ts
+++ b/packages/create-twenty-app/src/utils/app-template.ts
@@ -122,7 +122,7 @@ const updatePackageJson = async ({
packageJson.name = appName;
packageJson.devDependencies['twenty-sdk'] =
createTwentyAppPackageJson.version;
- packageJson.dependencies['twenty-client-sdk'] =
+ packageJson.devDependencies['twenty-client-sdk'] =
createTwentyAppPackageJson.version;
await fs.writeFile(
diff --git a/packages/twenty-apps/examples/hello-world/package.json b/packages/twenty-apps/examples/hello-world/package.json
index 07c3fbc6e2..91c729e7d7 100644
--- a/packages/twenty-apps/examples/hello-world/package.json
+++ b/packages/twenty-apps/examples/hello-world/package.json
@@ -15,15 +15,14 @@
"test": "vitest run",
"test:watch": "vitest"
},
- "dependencies": {
- "twenty-client-sdk": "2.13.0",
- "twenty-sdk": "2.13.0"
- },
+ "dependencies": {},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^18.2.0",
"oxlint": "^0.16.0",
"react": "^18.2.0",
+ "twenty-client-sdk": "2.13.0",
+ "twenty-sdk": "2.13.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^4.0.0"
diff --git a/packages/twenty-apps/examples/postcard/package.json b/packages/twenty-apps/examples/postcard/package.json
index 1f1d99fc28..a19048c702 100644
--- a/packages/twenty-apps/examples/postcard/package.json
+++ b/packages/twenty-apps/examples/postcard/package.json
@@ -20,10 +20,7 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
},
- "dependencies": {
- "twenty-client-sdk": "2.13.0",
- "twenty-sdk": "2.13.0"
- },
+ "dependencies": {},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^24.7.2",
@@ -32,6 +29,8 @@
"playwright": "^1.60.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "twenty-client-sdk": "2.13.0",
+ "twenty-sdk": "2.13.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^4.0.0"
diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx
index 0638365d70..c4587a3a3e 100644
--- a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx
+++ b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx
@@ -38,3 +38,24 @@ my-twenty-app/
**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives.
+
+## Dependencies
+
+Both Twenty SDK packages belong under `devDependencies`, not `dependencies`:
+
+```json filename="package.json"
+{
+ "dependencies": {},
+ "devDependencies": {
+ "twenty-client-sdk": "^2.13.0",
+ "twenty-sdk": "^2.13.0"
+ }
+}
+```
+
+- **`twenty-sdk`** ships the `twenty` CLI and the build/scaffolding tooling. It only runs at development and build time and is never imported by your published app's runtime.
+- **`twenty-client-sdk`** _is_ imported by your app code (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), but Twenty provides it at runtime — logic functions get it from a generated SDK layer, and front components resolve it from server-served modules. Your installed copy is only used for typechecking and the deploy-time build, so it never needs to ship in the deployed bundle.
+
+Keeping either package under `dependencies` pulls it into the installed app's runtime bundle, where it is dead weight. `twenty build` emits a warning when either is still listed under `dependencies`.
+
+Add your app's own runtime dependencies (libraries your logic functions actually import at runtime) under `dependencies` as usual.
diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx
index 88f244ab4e..09cdeb43b3 100644
--- a/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx
+++ b/packages/twenty-docs/developers/extend/apps/getting-started/troubleshooting.mdx
@@ -9,5 +9,6 @@ icon: "wrench"
- **Yarn 4 missing** — Run `corepack enable`.
- **Dependencies broken** — `rm -rf node_modules && yarn install`.
- **`twenty-sdk` errors after upgrading to v2.8.0** — It moved from `dependencies` to `devDependencies` in v2.8.0. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies).
+- **`twenty build` warns about `twenty-client-sdk` under `dependencies`** — It is provided at runtime by Twenty, so it should be moved to `devDependencies` alongside `twenty-sdk`. See [Project Structure → Dependencies](/developers/extend/apps/getting-started/project-structure#dependencies).
Stuck? Ask on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
diff --git a/packages/twenty-sdk/CHANGELOG.md b/packages/twenty-sdk/CHANGELOG.md
index 4fff109564..445984e6a4 100644
--- a/packages/twenty-sdk/CHANGELOG.md
+++ b/packages/twenty-sdk/CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to the [Twenty SDK](https://www.npmjs.com/package/twenty-sdk
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Changed
+
+- **`twenty-client-sdk` should now be a dev dependency too.** Although app code imports it (`CoreApiClient`, `MetadataApiClient`, `RestApiClient`), Twenty provides it at runtime — logic functions get it from a generated SDK layer and front components resolve it from server-served modules — so the installed copy is only needed for typechecking and the deploy-time build. Newly scaffolded apps now place it under `devDependencies`. Moving it is recommended (not required: the server already strips it from the deployed runtime), and keeps the installed app leaner:
+
+ ```diff
+ "dependencies": {
+ - "twenty-client-sdk": "^2.13.0"
+ },
+ "devDependencies": {
+ + "twenty-client-sdk": "^2.13.0"
+ }
+ ```
+
+ `twenty build` now also emits a warning when `twenty-client-sdk` is still listed under `dependencies`.
+
## [2.8.0]
### Breaking Changes
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts
index 666db22dd4..889c4a049c 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/validate-package-json-dependencies.spec.ts
@@ -29,7 +29,7 @@ describe('validatePackageJsonDependencies', () => {
it('should warn when twenty-sdk is listed under dependencies', async () => {
appPath = await writeTempPackageJson({
- dependencies: { 'twenty-sdk': '^2.8.0', 'twenty-client-sdk': '^2.8.0' },
+ dependencies: { 'twenty-sdk': '^2.8.0' },
});
const warnings = await validatePackageJsonDependencies(appPath);
@@ -39,10 +39,40 @@ describe('validatePackageJsonDependencies', () => {
expect(warnings[0]).toContain('devDependencies');
});
- it('should not warn when twenty-sdk is listed under devDependencies', async () => {
+ it('should warn when twenty-client-sdk is listed under dependencies', async () => {
appPath = await writeTempPackageJson({
dependencies: { 'twenty-client-sdk': '^2.8.0' },
- devDependencies: { 'twenty-sdk': '^2.8.0' },
+ });
+
+ const warnings = await validatePackageJsonDependencies(appPath);
+
+ expect(warnings).toHaveLength(1);
+ expect(warnings[0]).toContain('twenty-client-sdk');
+ expect(warnings[0]).toContain('devDependencies');
+ });
+
+ it('should warn for both SDK packages when both are listed under dependencies', async () => {
+ appPath = await writeTempPackageJson({
+ dependencies: { 'twenty-sdk': '^2.8.0', 'twenty-client-sdk': '^2.8.0' },
+ });
+
+ const warnings = await validatePackageJsonDependencies(appPath);
+
+ expect(warnings).toHaveLength(2);
+ expect(warnings.some((warning) => warning.includes('twenty-sdk'))).toBe(
+ true,
+ );
+ expect(
+ warnings.some((warning) => warning.includes('twenty-client-sdk')),
+ ).toBe(true);
+ });
+
+ it('should not warn when both SDK packages are listed under devDependencies', async () => {
+ appPath = await writeTempPackageJson({
+ devDependencies: {
+ 'twenty-sdk': '^2.8.0',
+ 'twenty-client-sdk': '^2.8.0',
+ },
});
const warnings = await validatePackageJsonDependencies(appPath);
@@ -50,9 +80,9 @@ describe('validatePackageJsonDependencies', () => {
expect(warnings).toEqual([]);
});
- it('should not warn when twenty-sdk is absent from dependencies', async () => {
+ it('should not warn when the SDK packages are absent from dependencies', async () => {
appPath = await writeTempPackageJson({
- dependencies: { 'twenty-client-sdk': '^2.8.0' },
+ dependencies: { 'some-other-package': '^1.0.0' },
});
const warnings = await validatePackageJsonDependencies(appPath);
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts
index f819d1ecf4..ffa981cafe 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/validate-package-json-dependencies.ts
@@ -6,9 +6,21 @@ type PackageJsonDependencies = {
dependencies?: Record;
};
-// twenty-sdk ships the CLI and build/scaffolding tooling used only at dev and
-// build time — it is never imported by the published app's runtime. Keeping it
-// in "dependencies" ships dead weight, so it must live in "devDependencies".
+// Neither Twenty SDK package needs to be resolved by the published app at
+// runtime, so both must live in "devDependencies":
+// - twenty-sdk ships the CLI and build/scaffolding tooling used only at dev and
+// build time, and is never imported by the published app's runtime.
+// - twenty-client-sdk is imported by app code but is provided at runtime by
+// Twenty's injected SDK (Lambda SDK layer / server-served modules), so the
+// app's installed copy is only needed for typecheck/build.
+// Keeping either under "dependencies" pulls it into the Lambda deps layer.
+const BUILD_TIME_DEPENDENCY_WARNINGS: Record = {
+ 'twenty-sdk':
+ '"twenty-sdk" is listed under "dependencies" in package.json. It is a build-time only tool and should be moved to "devDependencies".',
+ 'twenty-client-sdk':
+ '"twenty-client-sdk" is listed under "dependencies" in package.json. It is provided at runtime by Twenty\'s injected SDK and should be moved to "devDependencies".',
+};
+
export const validatePackageJsonDependencies = async (
appPath: string,
): Promise => {
@@ -20,11 +32,9 @@ export const validatePackageJsonDependencies = async (
const packageJson = await readJson(packageJsonPath);
- if (isDefined(packageJson.dependencies?.['twenty-sdk'])) {
- return [
- '"twenty-sdk" is listed under "dependencies" in package.json. It is a build-time only tool and should be moved to "devDependencies".',
- ];
- }
-
- return [];
+ return Object.entries(BUILD_TIME_DEPENDENCY_WARNINGS)
+ .filter(([packageName]) =>
+ isDefined(packageJson.dependencies?.[packageName]),
+ )
+ .map(([, warning]) => warning);
};
diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts
index 130de5f27d..fbdbfb9bc6 100644
--- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts
@@ -304,6 +304,11 @@ export class LogicFunctionResourceService {
);
}
+ // twenty-sdk is build-time only and twenty-client-sdk is injected at runtime
+ // by Twenty (Lambda SDK layer / server-served modules), so neither needs to
+ // be resolved by the Lambda yarn install. Apps should already declare them as
+ // devDependencies (skipped by `yarn workspaces focus --production`); this is a
+ // safety net for apps that still list them under "dependencies".
private async removeBuildTimeSdkFromDependencies(
packageJsonPath: string,
): Promise {
@@ -312,13 +317,24 @@ export class LogicFunctionResourceService {
) as { dependencies?: Record };
const dependencies = packageJson.dependencies;
- const sdkVersionRange = dependencies?.['twenty-sdk'];
- if (!isDefined(dependencies) || !isDefined(sdkVersionRange)) {
+ if (!isDefined(dependencies)) {
return;
}
- delete dependencies['twenty-sdk'];
+ const packagesToRemove = ['twenty-sdk', 'twenty-client-sdk'];
+
+ const packagesToRemoveFromDependencies = packagesToRemove.filter(
+ (packageName) => isDefined(dependencies[packageName]),
+ );
+
+ if (packagesToRemoveFromDependencies.length === 0) {
+ return;
+ }
+
+ for (const packageName of packagesToRemoveFromDependencies) {
+ delete dependencies[packageName];
+ }
await fs.writeFile(
packageJsonPath,