Fixes https://github.com/twentyhq/core-team-issues/issues/1956 **Problem** Within an app, the `.yarn/releases/` folder contains executable Yarn binaries that run when executing any yarn command (`.yarnrc` file indicates yarn path to be `.yarn/releases/yarn-4.9.2.cjs `.) This is a supply chain attack vector: a malicious actor could submit a PR with a compromised `yarn-4.9.2.cjs binary`, which would execute arbitrary code on developers' machines or CI systems. **Fix** Actually, thanks to Corepack, we don't need to store and execute this binary. Corepack can be seen as the manager of a package manager: in `package.json` we indicate a packageManager version like `"packageManager": "[email protected]"`, and when executing `yarn` Corepack will securely fetch the verified version from npm, avoiding the risk of executing a compromised binary committed to the repository. This was already in our app's package.json template but we were not using it! We can now - remove the folder containing the binary from our app template base-application (that is scaffolded when creating an app through cli), `.yarn/releases/`, and remove `yarnPath: .yarn/releases/yarn-4.9.2.cjs` from its .yarnrc - remove them from the community apps that were already published in the repo - add .yarn to gitignore **Tested** This has been tested and works for app created in the repo, outside the repo, and existing apps in the repo
691 lines
24 KiB
Plaintext
691 lines
24 KiB
Plaintext
---
|
||
title: Twenty Apps
|
||
description: Build and manage Twenty customizations as code.
|
||
---
|
||
|
||
<Warning>
|
||
Apps are currently in alpha testing. The feature is functional but still evolving.
|
||
</Warning>
|
||
|
||
## What Are Apps?
|
||
|
||
Apps let you build and manage Twenty customizations **as code**. Instead of configuring everything through the UI, you define your data model and serverless functions in code — making it faster to build, maintain, and roll out to multiple workspaces.
|
||
|
||
**What you can do today:**
|
||
- Define custom objects and fields as code (managed data model)
|
||
- Build serverless functions with custom triggers
|
||
- Deploy the same app across multiple workspaces
|
||
|
||
**Coming soon:**
|
||
- Custom UI layouts and components
|
||
|
||
## Prerequisites
|
||
|
||
- Node.js 24+ and Yarn 4
|
||
- A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks)
|
||
|
||
## Getting Started
|
||
|
||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||
|
||
```bash filename="Terminal"
|
||
# Scaffold a new app
|
||
npx create-twenty-app@latest my-twenty-app
|
||
cd my-twenty-app
|
||
|
||
# If you don't use yarn@4
|
||
corepack enable
|
||
yarn install
|
||
|
||
# Authenticate using your API key (you'll be prompted)
|
||
yarn auth:login
|
||
|
||
# Start dev mode: automatically syncs local changes to your workspace
|
||
yarn app:dev
|
||
```
|
||
|
||
From here you can:
|
||
|
||
```bash filename="Terminal"
|
||
# Add a new entity to your application (guided)
|
||
yarn app:create-entity
|
||
|
||
# Generate a typed Twenty client and workspace entity types
|
||
yarn app:generate
|
||
|
||
# Run a one‑time sync (instead of watch mode)
|
||
yarn app:sync
|
||
|
||
# Watch your application's functions logs
|
||
yarn function:logs
|
||
|
||
# Execute a function by name
|
||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||
|
||
# Uninstall the application from the current workspace
|
||
yarn app:uninstall
|
||
|
||
# Display commands' help
|
||
yarn app:help
|
||
```
|
||
|
||
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||
|
||
## Project structure (scaffolded)
|
||
|
||
When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
|
||
|
||
- Copies a minimal base application into `my-twenty-app/`
|
||
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
|
||
- Creates config files and scripts wired to the `twenty` CLI
|
||
- Generates a default application config and a default function role
|
||
|
||
A freshly scaffolded app looks like this:
|
||
|
||
```text filename="my-twenty-app/"
|
||
my-twenty-app/
|
||
package.json
|
||
yarn.lock
|
||
.gitignore
|
||
.nvmrc
|
||
.yarnrc.yml
|
||
.yarn/
|
||
install-state.gz
|
||
eslint.config.mjs
|
||
tsconfig.json
|
||
README.md
|
||
src/
|
||
app/
|
||
application.config.ts # Required - main application configuration
|
||
default-function.role.ts # Default role for serverless functions
|
||
// your entities (*.object.ts, *.function.ts, *.role.ts)
|
||
utils/ # Optional - handler implementations & utilities
|
||
```
|
||
|
||
### Convention-over-configuration
|
||
|
||
Applications use a **convention-over-configuration** approach where entities are detected by their file suffix. This allows flexible organization within the `src/app/` folder:
|
||
|
||
| File suffix | Entity type |
|
||
|-------------|-------------|
|
||
| `*.object.ts` | Custom object definitions |
|
||
| `*.function.ts` | Serverless function definitions |
|
||
| `*.role.ts` | Role definitions |
|
||
|
||
### Supported folder organizations
|
||
|
||
You can organize your entities in any of these patterns:
|
||
|
||
**Traditional (by type):**
|
||
```text
|
||
src/app/
|
||
├── application.config.ts
|
||
├── objects/
|
||
│ └── postCard.object.ts
|
||
├── functions/
|
||
│ └── createPostCard.function.ts
|
||
└── roles/
|
||
└── admin.role.ts
|
||
```
|
||
|
||
**Feature-based:**
|
||
```text
|
||
src/app/
|
||
├── application.config.ts
|
||
└── post-card/
|
||
├── postCard.object.ts
|
||
├── createPostCard.function.ts
|
||
└── postCardAdmin.role.ts
|
||
```
|
||
|
||
**Flat:**
|
||
```text
|
||
src/app/
|
||
├── application.config.ts
|
||
├── postCard.object.ts
|
||
├── createPostCard.function.ts
|
||
└── admin.role.ts
|
||
```
|
||
|
||
At a high level:
|
||
|
||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `dev`, `sync`, `generate`, `create-entity`, `logs`, `uninstall`, and `auth` that delegate to the local `twenty` CLI.
|
||
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
|
||
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
|
||
- **.nvmrc**: Pins the Node.js version expected by the project.
|
||
- **eslint.config.mjs** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
|
||
- **README.md**: A short README in the app root with basic instructions.
|
||
- **src/app/**: The main place where you define your application-as-code:
|
||
- `application.config.ts`: Global configuration for your app (metadata and runtime wiring). See "Application config" below.
|
||
- `*.role.ts`: Role definitions used by your serverless functions. See "Default function role" below.
|
||
- `*.object.ts`: Custom object definitions.
|
||
- `*.function.ts`: Serverless function definitions.
|
||
- **src/utils/**: Optional folder for handler implementations and utilities.
|
||
|
||
Later commands will add more files and folders:
|
||
|
||
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
|
||
- `yarn app:create-entity` will add entity definition files under `src/app/` for your custom objects, functions, or roles.
|
||
l
|
||
|
||
## Authentication
|
||
|
||
The first time you run `yarn auth:login`, you'll be prompted for:
|
||
|
||
- API URL (defaults to http://localhost:3000 or your current workspace profile)
|
||
- API key
|
||
|
||
Your credentials are stored per-user in `~/.twenty/config.json`. You can maintain multiple profiles and switch between them.
|
||
|
||
### Managing workspaces
|
||
|
||
```bash filename="Terminal"
|
||
# Login interactively (recommended)
|
||
yarn auth:login
|
||
|
||
# Login to a specific workspace profile
|
||
yarn auth:login --workspace my-custom-workspace
|
||
|
||
# List all configured workspaces
|
||
yarn auth:list
|
||
|
||
# Switch the default workspace (interactive)
|
||
yarn auth:switch
|
||
|
||
# Switch to a specific workspace
|
||
yarn auth:switch production
|
||
|
||
# Check current authentication status
|
||
yarn auth:status
|
||
```
|
||
|
||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||
|
||
## Use the SDK resources (types & config)
|
||
|
||
The twenty-sdk provides typed building blocks and helper functions you use inside your app. Below are the key pieces you'll touch most often.
|
||
|
||
### Helper functions
|
||
|
||
The SDK provides four helper functions with built-in validation for defining your app entities:
|
||
|
||
| Function | Purpose |
|
||
|----------|---------|
|
||
| `defineApp()` | Configure application metadata |
|
||
| `defineObject()` | Define custom objects with fields |
|
||
| `defineFunction()` | Define serverless functions with handlers |
|
||
| `defineRole()` | Configure role permissions and object access |
|
||
|
||
These functions validate your configuration at runtime and provide better IDE autocompletion and type safety.
|
||
|
||
### Defining objects
|
||
|
||
Custom objects describe both schema and behavior for records in your workspace. Use `defineObject()` to define objects with built-in validation:
|
||
|
||
```typescript
|
||
// src/app/postCard.object.ts
|
||
import { defineObject, FieldType } from 'twenty-sdk';
|
||
|
||
enum PostCardStatus {
|
||
DRAFT = 'DRAFT',
|
||
SENT = 'SENT',
|
||
DELIVERED = 'DELIVERED',
|
||
RETURNED = 'RETURNED',
|
||
}
|
||
|
||
export default defineObject({
|
||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||
nameSingular: 'postCard',
|
||
namePlural: 'postCards',
|
||
labelSingular: 'Post Card',
|
||
labelPlural: 'Post Cards',
|
||
description: 'A post card object',
|
||
icon: 'IconMail',
|
||
fields: [
|
||
{
|
||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||
name: 'content',
|
||
type: FieldType.TEXT,
|
||
label: 'Content',
|
||
description: "Postcard's content",
|
||
icon: 'IconAbc',
|
||
},
|
||
{
|
||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||
name: 'recipientName',
|
||
type: FieldType.FULL_NAME,
|
||
label: 'Recipient name',
|
||
icon: 'IconUser',
|
||
},
|
||
{
|
||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||
name: 'recipientAddress',
|
||
type: FieldType.ADDRESS,
|
||
label: 'Recipient address',
|
||
icon: 'IconHome',
|
||
},
|
||
{
|
||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||
name: 'status',
|
||
type: FieldType.SELECT,
|
||
label: 'Status',
|
||
icon: 'IconSend',
|
||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||
options: [
|
||
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
|
||
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
|
||
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
|
||
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
|
||
],
|
||
},
|
||
{
|
||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||
name: 'deliveredAt',
|
||
type: FieldType.DATE_TIME,
|
||
label: 'Delivered at',
|
||
icon: 'IconCheck',
|
||
isNullable: true,
|
||
defaultValue: null,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Key points:
|
||
|
||
- Use `defineObject()` for built-in validation and better IDE support.
|
||
- The `universalIdentifier` must be unique and stable across deployments.
|
||
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
||
- The `fields` array is optional — you can define objects without custom fields.
|
||
- You can scaffold new objects using `yarn app:create-entity`, which guides you through naming, fields, and relationships.
|
||
|
||
<Note>
|
||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
|
||
</Note>
|
||
|
||
<Accordion title="Alternative: Decorator-based syntax">
|
||
You can also define objects using TypeScript decorators. This approach uses class-based syntax with `@Object`, `@Field`, and `@Relation` decorators:
|
||
|
||
```typescript
|
||
import {
|
||
type AddressField,
|
||
Field,
|
||
FieldType,
|
||
type FullNameField,
|
||
Object,
|
||
OnDeleteAction,
|
||
Relation,
|
||
RelationType,
|
||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||
} from 'twenty-sdk';
|
||
import { type Note } from '../../generated';
|
||
|
||
@Object({
|
||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||
nameSingular: 'postCard',
|
||
namePlural: 'postCards',
|
||
labelSingular: 'Post card',
|
||
labelPlural: 'Post cards',
|
||
description: 'A post card object',
|
||
icon: 'IconMail',
|
||
})
|
||
export class PostCard {
|
||
@Field({
|
||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||
type: FieldType.TEXT,
|
||
label: 'Content',
|
||
description: "Postcard's content",
|
||
icon: 'IconAbc',
|
||
})
|
||
content: string;
|
||
|
||
@Relation({
|
||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||
type: RelationType.ONE_TO_MANY,
|
||
label: 'Notes',
|
||
icon: 'IconComment',
|
||
inverseSideTargetUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note,
|
||
onDelete: OnDeleteAction.CASCADE,
|
||
})
|
||
notes: Note[];
|
||
}
|
||
```
|
||
|
||
Note: The decorator approach requires `experimentalDecorators` in your TypeScript config.
|
||
</Accordion>
|
||
|
||
|
||
### Application config (application.config.ts)
|
||
|
||
Every app has a single `application.config.ts` file that describes:
|
||
|
||
- **Who the app is**: identifiers, display name, and description.
|
||
- **How its functions run**: which role they use for permissions.
|
||
- **(Optional) variables**: key–value pairs exposed to your functions as environment variables.
|
||
|
||
Use `defineApp()` to define your application configuration:
|
||
|
||
```typescript
|
||
// src/app/application.config.ts
|
||
import { defineApp } from 'twenty-sdk';
|
||
import { DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER } from './default-function.role';
|
||
|
||
export default defineApp({
|
||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||
displayName: 'My Twenty App',
|
||
description: 'My first Twenty app',
|
||
icon: 'IconWorld',
|
||
applicationVariables: {
|
||
DEFAULT_RECIPIENT_NAME: {
|
||
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
|
||
description: 'Default recipient name for postcards',
|
||
value: 'Jane Doe',
|
||
isSecret: false,
|
||
},
|
||
},
|
||
functionRoleUniversalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||
});
|
||
```
|
||
|
||
Notes:
|
||
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
|
||
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||
- `functionRoleUniversalIdentifier` must match the role you define in your `*.role.ts` file (see below).
|
||
|
||
#### Roles and permissions
|
||
|
||
Applications can define roles that encapsulate permissions on your workspace's objects and actions. The field `functionRoleUniversalIdentifier` in `application.config.ts` designates the default role used by your app's serverless functions.
|
||
|
||
- The runtime API key injected as `TWENTY_API_KEY` is derived from this default function role.
|
||
- The typed client will be restricted to the permissions granted to that role.
|
||
- Follow least‑privilege: create a dedicated role with only the permissions your functions need, then reference its universal identifier.
|
||
|
||
##### Default function role (*.role.ts)
|
||
|
||
When you scaffold a new app, the CLI also creates a default role file. Use `defineRole()` to define roles with built-in validation:
|
||
|
||
```typescript
|
||
// src/app/default-function.role.ts
|
||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||
|
||
export const DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER =
|
||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||
|
||
export default defineRole({
|
||
universalIdentifier: DEFAULT_FUNCTION_ROLE_UNIVERSAL_IDENTIFIER,
|
||
label: 'Default function role',
|
||
description: 'Default role for function Twenty client',
|
||
canReadAllObjectRecords: false,
|
||
canUpdateAllObjectRecords: false,
|
||
canSoftDeleteAllObjectRecords: false,
|
||
canDestroyAllObjectRecords: false,
|
||
canUpdateAllSettings: false,
|
||
canBeAssignedToAgents: false,
|
||
canBeAssignedToUsers: false,
|
||
canBeAssignedToApiKeys: false,
|
||
objectPermissions: [
|
||
{
|
||
objectNameSingular: 'postCard',
|
||
canReadObjectRecords: true,
|
||
canUpdateObjectRecords: true,
|
||
canSoftDeleteObjectRecords: false,
|
||
canDestroyObjectRecords: false,
|
||
},
|
||
],
|
||
fieldPermissions: [
|
||
{
|
||
objectNameSingular: 'postCard',
|
||
fieldName: 'content',
|
||
canReadFieldValue: false,
|
||
canUpdateFieldValue: false,
|
||
},
|
||
],
|
||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||
});
|
||
```
|
||
|
||
The `universalIdentifier` of this role is then referenced in `application.config.ts` as `functionRoleUniversalIdentifier`. In other words:
|
||
|
||
- **\*.role.ts** defines what the default function role can do.
|
||
- **application.config.ts** points to that role so your functions inherit its permissions.
|
||
|
||
Notes:
|
||
- Start from the scaffolded role, then progressively restrict it following least‑privilege.
|
||
- Replace the `objectPermissions` and `fieldPermissions` with the objects/fields your functions need.
|
||
- `permissionFlags` control access to platform-level capabilities. Keep them minimal; add only what you need.
|
||
- See a working example in the Hello World app: [`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||
|
||
### Serverless function config and entrypoint
|
||
|
||
Each function file uses `defineFunction()` to export a configuration with a handler and optional triggers. Use the `*.function.ts` file suffix for automatic detection.
|
||
|
||
```typescript
|
||
// src/app/createPostCard.function.ts
|
||
import { defineFunction } from 'twenty-sdk';
|
||
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
|
||
import Twenty, { type Person } from '../../generated';
|
||
|
||
const handler = async (
|
||
params:
|
||
| RoutePayload
|
||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||
| CronPayload,
|
||
) => {
|
||
const client = new Twenty(); // generated typed client
|
||
const name = 'name' in params.queryStringParameters
|
||
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||
: 'Hello world';
|
||
|
||
const result = await client.mutation({
|
||
createPostCard: {
|
||
__args: { data: { name } },
|
||
id: true,
|
||
name: true,
|
||
},
|
||
});
|
||
return result;
|
||
};
|
||
|
||
export default defineFunction({
|
||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||
name: 'create-new-post-card',
|
||
timeoutSeconds: 2,
|
||
handler,
|
||
triggers: [
|
||
// Public HTTP route trigger '/s/post-card/create'
|
||
{
|
||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||
type: 'route',
|
||
path: '/post-card/create',
|
||
httpMethod: 'GET',
|
||
isAuthRequired: false,
|
||
},
|
||
// Cron trigger (CRON pattern)
|
||
{
|
||
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||
type: 'cron',
|
||
pattern: '0 0 1 1 *',
|
||
},
|
||
// Database event trigger
|
||
{
|
||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||
type: 'databaseEvent',
|
||
eventName: 'person.updated',
|
||
updatedFields: ['name'],
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
Common trigger types:
|
||
- **route**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||
> e.g. `path: '/post-card/create',` -> call on `<APP_URL>/s/post-card/create`
|
||
- **cron**: Runs your function on a schedule using a CRON expression.
|
||
- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function.
|
||
> e.g. `person.updated`
|
||
|
||
Notes:
|
||
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
|
||
- You can mix multiple trigger types in a single function.
|
||
|
||
### Route trigger payload
|
||
|
||
<Warning>
|
||
**Breaking change (v1.16, January 2026):** The route trigger payload format has changed. Prior to v1.16, query parameters, path parameters, and body were sent directly as the payload. Starting with v1.16, they are nested inside a structured `RoutePayload` object.
|
||
|
||
**Before v1.16:**
|
||
```typescript
|
||
const handler = async (params) => {
|
||
const { param1, param2 } = params; // Direct access
|
||
};
|
||
```
|
||
|
||
**After v1.16:**
|
||
```typescript
|
||
const handler = async (event: RoutePayload) => {
|
||
const { param1, param2 } = event.body; // Access via .body
|
||
const { queryParam } = event.queryStringParameters;
|
||
const { id } = event.pathParameters;
|
||
};
|
||
```
|
||
|
||
**To migrate existing functions:** Update your handler to destructure from `event.body`, `event.queryStringParameters`, or `event.pathParameters` instead of directly from the params object.
|
||
</Warning>
|
||
|
||
When a route trigger invokes your function, it receives a `RoutePayload` object that follows the AWS HTTP API v2 format. Import the type from `twenty-sdk`:
|
||
|
||
```typescript
|
||
import { defineFunction, type RoutePayload } from 'twenty-sdk';
|
||
|
||
const handler = async (event: RoutePayload) => {
|
||
// Access request data
|
||
const { headers, queryStringParameters, pathParameters, body } = event;
|
||
|
||
// HTTP method and path are available in requestContext
|
||
const { method, path } = event.requestContext.http;
|
||
|
||
return { message: 'Success' };
|
||
};
|
||
```
|
||
|
||
The `RoutePayload` type has the following structure:
|
||
|
||
| Property | Type | Description |
|
||
|----------|------|-------------|
|
||
| `headers` | `Record<string, string \| undefined>` | HTTP headers (only those listed in `forwardedRequestHeaders`) |
|
||
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) |
|
||
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern (e.g., `/users/:id` → `{ id: '123' }`) |
|
||
| `body` | `object \| null` | Parsed request body (JSON) |
|
||
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded |
|
||
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) |
|
||
| `requestContext.http.path` | `string` | Raw request path |
|
||
|
||
### Forwarding HTTP headers
|
||
|
||
By default, HTTP headers from incoming requests are **not** passed to your serverless function for security reasons. To access specific headers, explicitly list them in the `forwardedRequestHeaders` array:
|
||
|
||
```typescript
|
||
export default defineFunction({
|
||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||
name: 'webhook-handler',
|
||
handler,
|
||
triggers: [
|
||
{
|
||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||
type: 'route',
|
||
path: '/webhook',
|
||
httpMethod: 'POST',
|
||
isAuthRequired: false,
|
||
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
In your handler, you can then access these headers:
|
||
|
||
```typescript
|
||
const handler = async (event: RoutePayload) => {
|
||
const signature = event.headers['x-webhook-signature'];
|
||
const contentType = event.headers['content-type'];
|
||
|
||
// Validate webhook signature...
|
||
return { received: true };
|
||
};
|
||
```
|
||
|
||
<Note>
|
||
Header names are normalized to lowercase. Access them using lowercase keys (for example, `event.headers['content-type']`).
|
||
</Note>
|
||
|
||
You can create new functions in two ways:
|
||
|
||
- **Scaffolded**: Run `yarn app:create-entity` and choose the option to add a new function. This generates a starter file with a handler and config.
|
||
- **Manual**: Create a new `*.function.ts` file and use `defineFunction()`, following the same pattern.
|
||
|
||
### Generated typed client
|
||
|
||
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
|
||
|
||
```typescript
|
||
import Twenty from './generated';
|
||
|
||
const client = new Twenty();
|
||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||
```
|
||
|
||
The client is re-generated by `yarn app:generate`. Re-run after changing your objects and `yarn app:sync` or when onboarding to a new workspace.
|
||
|
||
#### Runtime credentials in serverless functions
|
||
|
||
When your function runs on Twenty, the platform injects credentials as environment variables before your code executes:
|
||
|
||
- `TWENTY_API_URL`: Base URL of the Twenty API your app targets.
|
||
- `TWENTY_API_KEY`: Short‑lived key scoped to your application's default function role.
|
||
|
||
Notes:
|
||
- You do not need to pass URL or API key to the generated client. It reads `TWENTY_API_URL` and `TWENTY_API_KEY` from process.env at runtime.
|
||
- The API key's permissions are determined by the role referenced in your `application.config.ts` via `functionRoleUniversalIdentifier`. This is the default role used by serverless functions of your application.
|
||
- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `functionRoleUniversalIdentifier` to that role's universal identifier.
|
||
|
||
|
||
### Hello World example
|
||
|
||
Explore a minimal, end-to-end example that demonstrates objects, functions, and multiple triggers [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||
|
||
## Manual setup (without the scaffolder)
|
||
|
||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
|
||
|
||
```bash filename="Terminal"
|
||
yarn add -D twenty-sdk
|
||
```
|
||
|
||
Then add scripts like these:
|
||
|
||
```json filename="package.json"
|
||
{
|
||
"scripts": {
|
||
"auth": "twenty auth login",
|
||
"generate": "twenty app generate",
|
||
"dev": "twenty app dev",
|
||
"sync": "twenty app sync",
|
||
"uninstall": "twenty app uninstall",
|
||
"logs": "twenty app logs",
|
||
"create-entity": "twenty app add",
|
||
"help": "twenty --help"
|
||
}
|
||
}
|
||
```
|
||
|
||
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:sync`, etc.
|
||
|
||
## Troubleshooting
|
||
|
||
- Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions.
|
||
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
|
||
- Types or client missing/outdated: run `yarn app:generate` and then `yarn app:dev`.
|
||
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
|
||
|
||
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
|