diff --git a/packages/twenty-docs/developers/self-host/capabilities/setup.mdx b/packages/twenty-docs/developers/self-host/capabilities/setup.mdx
index 56deccd33a..c6757f15ac 100644
--- a/packages/twenty-docs/developers/self-host/capabilities/setup.mdx
+++ b/packages/twenty-docs/developers/self-host/capabilities/setup.mdx
@@ -288,3 +288,44 @@ yarn command:prod cron:workflow:automated-cron-trigger
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
+
+## Serverless Functions
+
+Twenty supports serverless functions for workflows and custom logic. The execution environment is configured via the `SERVERLESS_TYPE` environment variable.
+
+
+**Security Notice:** The local serverless driver (`SERVERLESS_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, we highly recommend using `SERVERLESS_TYPE=LAMBDA` or `SERVERLESS_TYPE=DISABLED`.
+
+
+### Available Drivers
+
+| Driver | Environment Variable | Use Case | Security Level |
+|--------|---------------------|----------|----------------|
+| Disabled | `SERVERLESS_TYPE=DISABLED` | Disable serverless functions entirely | N/A |
+| Local | `SERVERLESS_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
+| Lambda | `SERVERLESS_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
+
+### Recommended Configuration
+
+**For development:**
+```bash
+SERVERLESS_TYPE=LOCAL # default
+```
+
+**For production (AWS):**
+```bash
+SERVERLESS_TYPE=LAMBDA
+SERVERLESS_LAMBDA_REGION=us-east-1
+SERVERLESS_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
+SERVERLESS_LAMBDA_ACCESS_KEY_ID=your-access-key
+SERVERLESS_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
+```
+
+**To disable serverless functions:**
+```bash
+SERVERLESS_TYPE=DISABLED
+```
+
+
+When using `SERVERLESS_TYPE=DISABLED`, any attempt to execute a serverless function will return an error. This is useful if you want to run Twenty without serverless function capabilities.
+
diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example
index c6a47df1dc..4dd179de61 100644
--- a/packages/twenty-server/.env.example
+++ b/packages/twenty-server/.env.example
@@ -5,6 +5,7 @@ REDIS_URL=redis://localhost:6379
APP_SECRET=replace_me_with_a_random_string
SIGN_IN_PREFILLED=true
CODE_INTERPRETER_TYPE=local
+IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=false
FRONTEND_URL=http://localhost:3001
diff --git a/packages/twenty-server/.env.test b/packages/twenty-server/.env.test
index 4be39a0226..78a972934f 100644
--- a/packages/twenty-server/.env.test
+++ b/packages/twenty-server/.env.test
@@ -9,6 +9,8 @@ SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sen
MUTATION_MAXIMUM_RECORD_AFFECTED=100
IS_MULTIWORKSPACE_ENABLED=true
FRONTEND_URL=http://localhost:3001
+IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=false
+
AUTH_GOOGLE_ENABLED=false
MESSAGING_PROVIDER_GMAIL_ENABLED=false
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/disabled.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/disabled.driver.ts
new file mode 100644
index 0000000000..2de17e7ae4
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/disabled.driver.ts
@@ -0,0 +1,22 @@
+import {
+ type ServerlessDriver,
+ type ServerlessExecuteResult,
+} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
+
+import {
+ ServerlessFunctionException,
+ ServerlessFunctionExceptionCode,
+} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
+
+export class DisabledDriver implements ServerlessDriver {
+ async delete(): Promise {
+ // No-op when disabled
+ }
+
+ async execute(): Promise {
+ throw new ServerlessFunctionException(
+ 'Serverless function execution is disabled. Set SERVERLESS_TYPE to LOCAL or LAMBDA to enable.',
+ ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts
index da0e03cc1f..c333e953c3 100644
--- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts
+++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts
@@ -284,9 +284,13 @@ export class LocalDriver implements ServerlessDriver {
stack?: string;
stdout: string;
stderr: string;
- }>((resolve, _) => {
+ }>((resolve) => {
+ // Strip NODE_OPTIONS to prevent tsx loader from being inherited
+ const { NODE_OPTIONS: _n1, ...cleanProcessEnv } = process.env;
+ const { NODE_OPTIONS: _n2, ...cleanUserEnv } = env;
+
const child = spawn(process.execPath, [runnerPath], {
- env: { ...process.env, ...env },
+ env: { ...cleanProcessEnv, ...cleanUserEnv },
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts
index 0b77c1b732..424bd82381 100644
--- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts
+++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts
@@ -1,12 +1,12 @@
-import { statSync, promises as fs } from 'fs';
-import { promisify } from 'util';
-import { exec } from 'child_process';
+import { execFile } from 'child_process';
+import { promises as fs, statSync } from 'fs';
import { join } from 'path';
+import { promisify } from 'util';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
-const execPromise = promisify(exec);
+const execFilePromise = promisify(execFile);
export const copyAndBuildDependencies = async (
buildDirectory: string,
@@ -32,23 +32,35 @@ export const copyAndBuildDependencies = async (
recursive: true,
});
+ const localYarnPath = join(buildDirectory, '.yarn/releases/yarn-4.9.2.cjs');
+
+ // Strip NODE_OPTIONS to prevent tsx loader from interfering with yarn
+ const { NODE_OPTIONS: _nodeOptions, ...cleanEnv } = process.env;
+
try {
- await execPromise('yarn', { cwd: buildDirectory });
+ await execFilePromise(process.execPath, [localYarnPath], {
+ cwd: buildDirectory,
+ env: cleanEnv,
+ });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
- throw new Error(error.stdout);
+ const errorMessage =
+ [error?.stdout, error?.stderr].filter(Boolean).join('\n') ||
+ 'Failed to install serverless dependencies';
+
+ throw new Error(errorMessage);
}
const objects = await fs.readdir(buildDirectory);
- objects.forEach((object) => {
- const fullPath = join(buildDirectory, object);
+ await Promise.all(
+ objects
+ .filter((object) => object !== 'node_modules')
+ .map((object) => {
+ const fullPath = join(buildDirectory, object);
- if (object === 'node_modules') return;
-
- if (statSync(fullPath).isDirectory()) {
- fs.rm(fullPath, { recursive: true, force: true });
- } else {
- fs.rm(fullPath);
- }
- });
+ return statSync(fullPath).isDirectory()
+ ? fs.rm(fullPath, { recursive: true, force: true })
+ : fs.rm(fullPath);
+ }),
+ );
};
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/serverless-module.factory.ts b/packages/twenty-server/src/engine/core-modules/serverless/serverless-module.factory.ts
index 31c602f91b..0f112b7b0e 100644
--- a/packages/twenty-server/src/engine/core-modules/serverless/serverless-module.factory.ts
+++ b/packages/twenty-server/src/engine/core-modules/serverless/serverless-module.factory.ts
@@ -15,6 +15,11 @@ export const serverlessModuleFactory = async (
const options = { fileStorageService };
switch (driverType) {
+ case ServerlessDriverType.DISABLED: {
+ return {
+ type: ServerlessDriverType.DISABLED,
+ };
+ }
case ServerlessDriverType.LOCAL: {
return {
type: ServerlessDriverType.LOCAL,
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/serverless.interface.ts b/packages/twenty-server/src/engine/core-modules/serverless/serverless.interface.ts
index 704c18e8d8..72cf6d4c9f 100644
--- a/packages/twenty-server/src/engine/core-modules/serverless/serverless.interface.ts
+++ b/packages/twenty-server/src/engine/core-modules/serverless/serverless.interface.ts
@@ -4,10 +4,15 @@ import { type LambdaDriverOptions } from 'src/engine/core-modules/serverless/dri
import { type LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
export enum ServerlessDriverType {
+ DISABLED = 'DISABLED',
LAMBDA = 'LAMBDA',
LOCAL = 'LOCAL',
}
+export interface DisabledDriverFactoryOptions {
+ type: ServerlessDriverType.DISABLED;
+}
+
export interface LocalDriverFactoryOptions {
type: ServerlessDriverType.LOCAL;
options: LocalDriverOptions;
@@ -19,6 +24,7 @@ export interface LambdaDriverFactoryOptions {
}
export type ServerlessModuleOptions =
+ | DisabledDriverFactoryOptions
| LocalDriverFactoryOptions
| LambdaDriverFactoryOptions;
diff --git a/packages/twenty-server/src/engine/core-modules/serverless/serverless.module.ts b/packages/twenty-server/src/engine/core-modules/serverless/serverless.module.ts
index c355a6a522..6dd2da6c41 100644
--- a/packages/twenty-server/src/engine/core-modules/serverless/serverless.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/serverless/serverless.module.ts
@@ -1,6 +1,7 @@
import { type DynamicModule, Global } from '@nestjs/common';
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
+import { DisabledDriver } from 'src/engine/core-modules/serverless/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
@@ -19,9 +20,21 @@ export class ServerlessModule {
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
- return config?.type === ServerlessDriverType.LOCAL
- ? new LocalDriver(config.options)
- : new LambdaDriver(config.options);
+ switch (config?.type) {
+ case ServerlessDriverType.DISABLED:
+ return new DisabledDriver();
+ case ServerlessDriverType.LOCAL:
+ return new LocalDriver(config.options);
+ case ServerlessDriverType.LAMBDA:
+ return new LambdaDriver(config.options);
+ default: {
+ const unknownConfig = config as { type?: string };
+
+ throw new Error(
+ `Unknown serverless driver type: ${unknownConfig?.type}`,
+ );
+ }
+ }
},
inject: options.inject || [],
};
diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
index 95ec914748..dda2f62bfa 100644
--- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
+++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts
@@ -375,7 +375,7 @@ export class ConfigVariables {
type: ConfigVariableType.BOOLEAN,
})
@IsOptional()
- IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = false;
+ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = true;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.STORAGE_CONFIG,
diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts
index 8c035e9dfd..d4a7fac93d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.exception.ts
@@ -14,6 +14,7 @@ export enum ServerlessFunctionExceptionCode {
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
SERVERLESS_FUNCTION_CREATE_FAILED = 'SERVERLESS_FUNCTION_CREATE_FAILED',
SERVERLESS_FUNCTION_EXECUTION_TIMEOUT = 'SERVERLESS_FUNCTION_EXECUTION_TIMEOUT',
+ SERVERLESS_FUNCTION_DISABLED = 'SERVERLESS_FUNCTION_DISABLED',
}
const getServerlessFunctionExceptionUserFriendlyMessage = (
@@ -38,6 +39,8 @@ const getServerlessFunctionExceptionUserFriendlyMessage = (
return msg`Failed to create function.`;
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_TIMEOUT:
return msg`Function execution timed out.`;
+ case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED:
+ return msg`Serverless function execution is disabled.`;
default:
assertUnreachable(code);
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts
index 5448fe0964..cbd32b71f2 100644
--- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils.ts
@@ -29,6 +29,8 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED:
throw error;
+ case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED:
+ throw new ForbiddenError(error);
default: {
return assertUnreachable(error.code);
}
diff --git a/packages/twenty-server/test/integration/metadata/suites/serverless-function/serverless-function-execution.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/serverless-function/serverless-function-execution.integration-spec.ts
new file mode 100644
index 0000000000..f8c7dcd238
--- /dev/null
+++ b/packages/twenty-server/test/integration/metadata/suites/serverless-function/serverless-function-execution.integration-spec.ts
@@ -0,0 +1,231 @@
+import { createOneServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/create-one-serverless-function.util';
+import { deleteServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/delete-serverless-function.util';
+import { executeServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/execute-serverless-function.util';
+import { publishServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/publish-serverless-function.util';
+import { updateServerlessFunction } from 'test/integration/metadata/suites/serverless-function/utils/update-serverless-function.util';
+
+import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
+
+// Test function using external packages from default layer (lodash.groupby)
+const EXTERNAL_PACKAGES_FUNCTION_CODE = {
+ 'src/index.ts': `import groupBy from 'lodash.groupby';
+
+export const main = async (params: { items: Array<{ category: string; name: string }> }): Promise