diff --git a/packages/create-twenty-app/README.md b/packages/create-twenty-app/README.md index 96b3c2c44aa..c2cc49ad8b3 100644 --- a/packages/create-twenty-app/README.md +++ b/packages/create-twenty-app/README.md @@ -23,6 +23,7 @@ Create Twenty App is the official scaffolding CLI for building apps on top of [T - A Twenty workspace and an API key (create one at https://app.twenty.com/settings/api-webhooks) ## Quick start + ```bash npx create-twenty-app@latest my-twenty-app cd my-twenty-app diff --git a/packages/create-twenty-app/package.json b/packages/create-twenty-app/package.json index b4962a73187..9f037b89730 100644 --- a/packages/create-twenty-app/package.json +++ b/packages/create-twenty-app/package.json @@ -1,6 +1,6 @@ { "name": "create-twenty-app", - "version": "0.2.3", + "version": "0.2.4", "description": "Command-line interface to create Twenty application", "main": "dist/cli.cjs", "bin": "dist/cli.cjs", diff --git a/packages/create-twenty-app/src/constants/base-application/eslint.config.mjs b/packages/create-twenty-app/src/constants/base-application/eslint.config.mjs index e10679df19b..825aefbfcfa 100644 --- a/packages/create-twenty-app/src/constants/base-application/eslint.config.mjs +++ b/packages/create-twenty-app/src/constants/base-application/eslint.config.mjs @@ -1,137 +1,29 @@ import js from '@eslint/js'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import importPlugin from 'eslint-plugin-import'; -import preferArrowPlugin from 'eslint-plugin-prefer-arrow'; -import prettierPlugin from 'eslint-plugin-prettier'; -import unusedImportsPlugin from 'eslint-plugin-unused-imports'; +import tseslint from 'typescript-eslint'; export default [ - // Base JS rules + // Base JS recommended rules js.configs.recommended, - // Global ignores - { - ignores: ['**/node_modules/**', '**/dist/**', '**/coverage/**'], - }, + // TypeScript recommended rules + ...tseslint.configs.recommended, - // Base config for TS/JS files { - files: ['**/*.{js,jsx,ts,tsx}'], + files: ['**/*.ts', '**/*.tsx'], languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - plugins: { - prettier: prettierPlugin, - import: importPlugin, - 'prefer-arrow': preferArrowPlugin, - 'unused-imports': unusedImportsPlugin, - }, - rules: { - // General rules (aligned with main project) - 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], - 'no-console': [ - 'warn', - { allow: ['group', 'groupCollapsed', 'groupEnd'] }, - ], - 'no-control-regex': 0, - 'no-debugger': 'error', - 'no-duplicate-imports': 'error', - 'no-undef': 'off', - 'no-unused-vars': 'off', - - // Import rules - 'import/no-relative-packages': 'error', - 'import/no-useless-path-segments': 'error', - 'import/no-duplicates': ['error', { considerQueryString: true }], - - // Prefer arrow functions - 'prefer-arrow/prefer-arrow-functions': [ - 'error', - { - disallowPrototype: true, - singleReturnOnly: false, - classPropertiesAllowed: false, - }, - ], - - // Unused imports - 'unused-imports/no-unused-imports': 'warn', - 'unused-imports/no-unused-vars': [ - 'warn', - { - vars: 'all', - varsIgnorePattern: '^_', - args: 'after-used', - argsIgnorePattern: '^_', - }, - ], - - // Prettier (formatting as lint errors if you want) - 'prettier/prettier': 'error', - }, - }, - - // TypeScript-specific configuration - { - files: ['**/*.{ts,tsx}'], - languageOptions: { - parser: typescriptParser, parserOptions: { - ecmaFeatures: { - jsx: true, - }, + project: true, + tsconfigRootDir: import.meta.dirname, }, }, - plugins: { - '@typescript-eslint': typescriptEslint, - }, rules: { - // Turn off base rule and use TS-aware versions - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'error', - - '@typescript-eslint/ban-ts-comment': 'error', - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - prefer: 'type-imports', - fixStyle: 'inline-type-imports', - }, - ], - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-empty-interface': [ - 'error', - { - allowSingleExtends: true, - }, + // Common TypeScript-friendly tweaks + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_' }, ], '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-unused-vars': 'off', - }, - }, - - // Test files (Jest) - { - files: ['**/*.spec.@(ts|tsx|js|jsx)', '**/*.test.@(ts|tsx|js|jsx)'], - languageOptions: { - globals: { - jest: true, - describe: true, - it: true, - expect: true, - beforeEach: true, - afterEach: true, - beforeAll: true, - afterAll: true, - }, - }, - rules: { - '@typescript-eslint/no-non-null-assertion': 'off', + 'no-unused-vars': 'off', // handled by TS rule }, }, ]; diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index 149b12f56bd..975302f46f5 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -100,7 +100,7 @@ const createDefaultServerlessFunctionRoleConfig = async ({ appDirectory: string; defaultServerlessFunctionRoleUniversalIdentifier: string; }) => { - const content = `import { PermissionFlag, type RoleConfig } from 'twenty-sdk'; + const content = `import { type RoleConfig } from 'twenty-sdk'; export const functionRole: RoleConfig = { universalIdentifier: '${defaultServerlessFunctionRoleUniversalIdentifier}', @@ -168,13 +168,17 @@ const createPackageJson = async ({ uninstall: 'twenty app uninstall', help: 'twenty help', auth: 'twenty auth login', + lint: 'eslint', + 'lint-fix': 'eslint --fix', }, dependencies: { - 'twenty-sdk': '0.2.3', + 'twenty-sdk': '0.2.4', }, devDependencies: { - '@types/node': '^24.7.2', typescript: '^5.9.3', + '@types/node': '^24.7.2', + eslint: '^9.32.0', + 'typescript-eslint': '^8.50.0', }, }; diff --git a/packages/twenty-apps/hello-world/package.json b/packages/twenty-apps/hello-world/package.json index d4ee248c08b..091d99b4dc4 100644 --- a/packages/twenty-apps/hello-world/package.json +++ b/packages/twenty-apps/hello-world/package.json @@ -1,6 +1,6 @@ { "name": "hello-world", - "version": "0.2.1", + "version": "0.2.2", "license": "MIT", "engines": { "node": "^24.5.0", @@ -17,7 +17,7 @@ "auth": "twenty auth login" }, "dependencies": { - "twenty-sdk": "0.2.2" + "twenty-sdk": "0.2.4" }, "devDependencies": { "@types/node": "^24.7.2" diff --git a/packages/twenty-apps/hello-world/src/roles/function-role.ts b/packages/twenty-apps/hello-world/src/roles/function-role.ts index 82e89d4d48c..5336828b1f9 100644 --- a/packages/twenty-apps/hello-world/src/roles/function-role.ts +++ b/packages/twenty-apps/hello-world/src/roles/function-role.ts @@ -12,7 +12,6 @@ export const functionRole: RoleConfig = { canBeAssignedToAgents: false, canBeAssignedToUsers: false, canBeAssignedToApiKeys: false, - canBeAssignedToApplications: true, objectPermissions: [ { objectNameSingular: 'postCard', diff --git a/packages/twenty-cli/README.md b/packages/twenty-cli/README.md index 62b2818457c..0b9ee26b91f 100644 --- a/packages/twenty-cli/README.md +++ b/packages/twenty-cli/README.md @@ -1,89 +1,8 @@ # Deprecated: twenty-cli -This package is deprecated. Please install and use twenty-sdk instead: +This package is deprecated. Please install and use [twenty-sdk](https://www.npmjs.com/package/twenty-sdk) instead: ```bash npm uninstall twenty-cli npm install -g twenty-sdk ``` - -The command name remains the same: twenty. - -A command-line interface to easily scaffold, develop, and publish applications that extend Twenty CRM (now provided by twenty-sdk). - -## Requirements -- yarn >= 4.9.2 -- an `apiKey`. Go to `https://twenty.com/settings/api-webhooks` to generate one - -## Quick example project - -```bash -# Authenticate using your apiKey (CLI will prompt for your ) -twenty auth login - -# Init a new application called hello-world -twenty app init hello-world - -# Go to your app -cd hello-world - -# Add a serverless function to your application -twenty app add serverlessFunction - -# Add a trigger to your serverless function -twenty app add trigger - -# Add axios to your application -yarn add axios - -# Start dev mode: automatically syncs changes to your Twenty workspace, so you can test new functions/objects instantly. -twenty app dev - -# Or use one time sync (also generates SDK automatically) -twenty app sync - -# List all available commands -twenty help -``` - -## Application Structure - -Each application in this package follows the standard application structure: - -``` -app-name/ -├── package.json -├── README.md -├── serverlessFunctions # Custom backend logic (runs on demand) -└── ... -``` - -## Publish your application - -Applications are currently stored in twenty/packages/twenty-apps. - -You can share your application with all twenty users. - -```bash -# pull twenty project -git clone https://github.com/twentyhq/twenty.git -cd twenty - -# create a new branch -git checkout -b feature/my-awesome-app -``` - -- copy your app folder into twenty/packages/twenty-apps -- commit your changes and open a pull request on https://github.com/twentyhq/twenty - -```bash -git commit -m "Add new application" -git push -``` - -Our team reviews contributions for quality, security, and reusability before merging. - -## Contributing - -- see our [Hacktoberfest 2025 notion page](https://twentycrm.notion.site/Hacktoberfest-27711d8417038037a149d4638a9cc510) -- our [Discord](https://discord.gg/cx5n4Jzs57) diff --git a/packages/twenty-cli/package.json b/packages/twenty-cli/package.json index 310899c4922..cfde72a247c 100644 --- a/packages/twenty-cli/package.json +++ b/packages/twenty-cli/package.json @@ -1,6 +1,6 @@ { "name": "twenty-cli", - "version": "0.3.0", + "version": "0.3.1", "description": "[DEPRECATED] Use twenty-sdk instead: https://www.npmjs.com/package/twenty-sdk", "scripts": { "build": "echo 'use npx nx build'", diff --git a/packages/twenty-docs/developers/extend/capabilities/apps.mdx b/packages/twenty-docs/developers/extend/capabilities/apps.mdx index dc11da9f67c..690651a74be 100644 --- a/packages/twenty-docs/developers/extend/capabilities/apps.mdx +++ b/packages/twenty-docs/developers/extend/capabilities/apps.mdx @@ -19,5 +19,498 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf **Coming soon:** - Custom UI layouts and components +## Prerequisites -## Getting Started (Coming Soon) +- 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 + +# Authenticate using your API key (you'll be prompted) +yarn auth + +# Start dev mode: automatically syncs local changes to your workspace +yarn dev +``` + +From here you can: + +```bash filename="Terminal" +# Add a new entity to your application (guided) +yarn create-entity + +# Generate a typed Twenty client and workspace entity types +yarn generate + +# Run a one‑time sync (instead of watch mode) +yarn sync + +# Watch your application's functions logs +yarn logs + +# Uninstall the application from the current workspace +yarn uninstall + +# Display commands' help +yarn 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/ + releases/ + yarn-4.9.2.cjs + install-state.gz + eslint.config.mjs + tsconfig.json + README.md + src/ + application.config.ts + role.config.ts + // your entities, actions, and other app files +``` + +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/**: 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.config.ts`: Default function role used by your serverless functions. See “Default function role” below. + - Future entities, actions/functions, and any supporting code you add. + +Later commands will add more files and folders: + +- `yarn generate` will create a `generated/` folder (typed Twenty client + workspace types). +- `yarn create-entity` will add entity definition files under `src/` for your custom objects. + + +## Authentication + +The first time you run `yarn auth`, 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 using `--workspace `. + +Examples: + +```bash filename="Terminal" +# Login interactively (recommended) +yarn auth + +# Use a specific workspace profile +yarn auth --workspace my-custom-workspace +``` + +## Use the SDK resources (types & config) + +The twenty-sdk provides typed building blocks you use inside your app. Below are the key pieces you'll touch most often. + +### Defining objects + +Custom objects are regular TypeScript classes annotated with decorators from `twenty-sdk`. They live under `src/objects/` in your app and describe both schema and behavior for records in your workspace. + +Here is an example `postCard` object from the Hello World app: + +```typescript +import { type Note } from '../../generated'; + +import { + type AddressField, + Field, + FieldType, + type FullNameField, + Object, + OnDeleteAction, + Relation, + RelationType, + STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, +} from 'twenty-sdk'; + +enum PostCardStatus { + DRAFT = 'DRAFT', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + RETURNED = 'RETURNED', +} + +@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; + + @Field({ + universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac', + type: FieldType.FULL_NAME, + label: 'Recipient name', + icon: 'IconUser', + }) + recipientName: FullNameField; + + @Field({ + universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266', + type: FieldType.ADDRESS, + label: 'Recipient address', + icon: 'IconHome', + }) + recipientAddress: AddressField; + + @Field({ + universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e', + 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' }, + ], + }) + status: PostCardStatus; + + @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[]; + + @Field({ + universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433', + type: FieldType.DATE_TIME, + label: 'Delivered at', + icon: 'IconCheck', + isNullable: true, + defaultValue: null, + }) + deliveredAt?: Date; +} +``` + +Key points: + +- The `@Object` decorator defines the object identity and labels used across the workspace; its `universalIdentifier` must be unique and stable across deployments. +- Each `@Field` decorator defines a field on the object with a type, label, and its own stable `universalIdentifier`. +- `@Relation` wires this object to other objects (standard or custom) and controls cascade behavior with `onDelete`. +- You can scaffold new objects using `yarn create-entity`, which guides you through naming, fields, and relationships, then generates object files similar to the `postCard` example. + + +### 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. + +When you scaffold a new app, you start with a minimal config: + +```typescript +import { type ApplicationConfig } from 'twenty-sdk'; + +const config: ApplicationConfig = { + universalIdentifier: '', + displayName: 'My Twenty App', + description: 'My first Twenty app', + functionRoleUniversalIdentifier: '', +}; + +export default config; +``` + +You can gradually extend this file as your app grows. For example, you can add an icon and application-scoped variables: + +```typescript +import { type ApplicationConfig } from 'twenty-sdk'; + +const config: ApplicationConfig = { + universalIdentifier: '', + displayName: 'My App', + description: 'What your app does', + icon: 'IconWorld', // Choose an icon by name + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '', + description: 'Default recipient used by functions', + value: 'Jane Doe', + isSecret: false, + }, + }, + functionRoleUniversalIdentifier: '', +}; + +export default config; +``` + +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 `role.config.ts` (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.config.ts) + +When you scaffold a new app, the CLI also creates `src/role.config.ts`. This file exports the default role your serverless functions will use at runtime: + +```typescript +import { PermissionFlag, type RoleConfig } from 'twenty-sdk'; + +export const functionRole: RoleConfig = { + universalIdentifier: '', + label: 'My Twenty App default function role', + description: 'My Twenty App default function role', + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: false, +}; +``` + +The `universalIdentifier` of this role is automatically wired into `application.config.ts` as `functionRoleUniversalIdentifier`. In other words: + +- **role.config.ts** defines what the default function role can do. +- **application.config.ts** points to that role so your functions inherit its permissions. + +As you move beyond the initial scaffold, you should tighten this role and make it explicit about what it can access. A more production-ready role might look closer to: + +```typescript +import { PermissionFlag, type RoleConfig } from 'twenty-sdk'; + +export const functionRole: RoleConfig = { + universalIdentifier: '', + 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: ['APPLICATIONS'], +}; +``` + +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 exports a main handler and a config describing its triggers. You can mix multiple trigger types. + +```typescript +// src/actions/create-new-post-card.ts +import type { + FunctionConfig, + DatabaseEventPayload, + ObjectRecordCreateEvent, + CronPayload, +} from 'twenty-sdk'; +import Twenty, { type Person } from '../generated'; + +// main handler can accept parameters from route, cron, or database events +export const main = async ( + params: + | { name?: string } + | DatabaseEventPayload> + | CronPayload, +) => { + const client = new Twenty(); // generated typed client + const name = 'name' in params + ? params.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 const config: FunctionConfig = { + universalIdentifier: '', + name: 'create-new-post-card', + timeoutSeconds: 2, + triggers: [ + // Public HTTP route trigger '/s/post-card/create' + { + universalIdentifier: '', + type: 'route', + path: '/post-card/create', + httpMethod: 'GET', + isAuthRequired: false, + }, + // Cron trigger (CRON pattern) + { + universalIdentifier: '', + type: 'cron', + pattern: '0 0 1 1 *', + }, + // Database event trigger + { + universalIdentifier: '', + type: 'databaseEvent', + eventName: 'person.created', + }, + ], +}; +``` + +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 `/s/post-card/create` +- cron: Runs your function on a schedule using a CRON expression. +- databaseEvent: Runs on workspace object lifecycle events +> e.g. `person.created` + +You can create new functions in two ways: + +- **Scaffolded**: Run `yarn create-entity --path ` and choose the option to add a new function. This generates a starter file under `` with a `main` handler and a `config` block similar to the example above. +- **Manual**: Create a new file and export `main` and `config` yourself, following the same pattern. + +### Generated typed client + +Run yarn 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 generate`. Re-run after changing your objects and `yarn 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 dev`, `yarn sync`, etc. + +## Troubleshooting + +- Authentication errors: run `yarn auth` 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 generate` and then `yarn dev`. +- Dev mode not syncing: ensure `yarn dev` is running and that changes are not ignored by your environment. + +Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322 diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 14ecee2938e..c072df28fbf 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -915,7 +915,6 @@ export type CreateRoleInput = { canAccessAllTools?: InputMaybe; canBeAssignedToAgents?: InputMaybe; canBeAssignedToApiKeys?: InputMaybe; - canBeAssignedToApplications?: InputMaybe; canBeAssignedToUsers?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; @@ -4424,7 +4423,6 @@ export type UpdateRolePayload = { canAccessAllTools?: InputMaybe; canBeAssignedToAgents?: InputMaybe; canBeAssignedToApiKeys?: InputMaybe; - canBeAssignedToApplications?: InputMaybe; canBeAssignedToUsers?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index ee3d41dcc94..264c776d327 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -898,7 +898,6 @@ export type CreateRoleInput = { canAccessAllTools?: InputMaybe; canBeAssignedToAgents?: InputMaybe; canBeAssignedToApiKeys?: InputMaybe; - canBeAssignedToApplications?: InputMaybe; canBeAssignedToUsers?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; @@ -4253,7 +4252,6 @@ export type UpdateRolePayload = { canAccessAllTools?: InputMaybe; canBeAssignedToAgents?: InputMaybe; canBeAssignedToApiKeys?: InputMaybe; - canBeAssignedToApplications?: InputMaybe; canBeAssignedToUsers?: InputMaybe; canDestroyAllObjectRecords?: InputMaybe; canReadAllObjectRecords?: InputMaybe; diff --git a/packages/twenty-sdk/README.md b/packages/twenty-sdk/README.md index c3d3dac14a3..fcf6f69e4d7 100644 --- a/packages/twenty-sdk/README.md +++ b/packages/twenty-sdk/README.md @@ -30,66 +30,152 @@ npm install twenty-sdk yarn add twenty-sdk ``` -## Getting started -You can either scaffold a new app or add the SDK to an existing one. +## Usage -- Start new (recommended): - ```bash - npx create-twenty-app@latest my-twenty-app - cd my-twenty-app - ``` -- Existing project: install the SDK as shown above, then use the CLI below. +``` +Usage: twenty [options] [command] + +CLI for Twenty application development + +Options: + --workspace Use a specific workspace configuration (default: "default") + -V, --version output the version number + -h, --help display help for command + +Commands: + auth Authentication commands + app Application development commands + help [command] display help for command +``` + +## Global Options + +- `--workspace `: Use a specific workspace configuration profile. Defaults to `default`. See Configuration for details. + +## Commands + +### Auth + +Authenticate the CLI against your Twenty workspace. + +- `twenty auth login` — Authenticate with Twenty. + - Options: + - `--api-key `: API key for authentication. + - `--api-url `: Twenty API URL (defaults to your current profile's value or `http://localhost:3000`). + - Behavior: Prompts for any missing values, persists them to the active workspace profile, and validates the credentials. + +- `twenty auth logout` — Remove authentication credentials for the active workspace profile. + +- `twenty auth status` — Print the current authentication status (API URL, masked API key, validity). + +Examples: -## CLI quickstart ```bash -# Authenticate using your API key (CLI will prompt for it) +# Login interactively (recommended) twenty auth login -# Add a new entity to your application (guided prompts) -twenty app add +# Provide values in flags +twenty auth login --api-key $TWENTY_API_KEY --api-url https://api.twenty.com -# Generate a typed Twenty client and TypeScript definitions for your workspace entities -twenty app generate +# Login interactively for a specific workspace profile +twenty auth login --workspace my-custom-workspace -# Start dev mode: automatically syncs changes to your workspace for instant testing +# Check status +twenty auth status + +# Logout current profile +twenty auth logout +``` + +### App + +Application development commands. + +- `twenty app sync [appPath]` — One-time sync of the application to your Twenty workspace. +- Behavior: Compute your application's manifest and send it to your workspace to sync your application + +- `twenty app dev [appPath]` — Watch and sync local application changes. + - Options: + - `-d, --debounce `: Debounce delay in milliseconds (default: `1000`). + - Behavior: Performs an initial sync, then watches the directory for changes and re-syncs after debounced edits. Press Ctrl+C to stop. + +- `twenty app uninstall [appPath]` — Uninstall the application from the current workspace. + - Note: `twenty app delete` exists as a hidden alias for backward compatibility. + +- `twenty app add [entityType]` — Add a new entity to your application. + - Arguments: + - `entityType`: one of `function` or `object`. If omitted, an interactive prompt is shown. + - Options: + - `--path `: The path where the entity file should be created (relative to the current directory). + - Behavior: + - `object`: prompts for singular/plural names and labels, then creates a new object definition file. + - `function`: prompts for a name and scaffolds a serverless function file. + +- `twenty app generate [appPath]` — Generate the typed Twenty client for your application. + +- `twenty app logs [appPath]` — Stream application function logs. + - Options: + - `-u, --functionUniversalIdentifier `: Only show logs for a specific function universal ID. + - `-n, --functionName `: Only show logs for a specific function name. + +Examples: + +```bash +# Start dev mode with default debounce twenty app dev -# One‑time sync of local changes +# Start dev mode with custom workspace profile +twenty app dev --workspace my-custom-workspace + +# Dev mode with custom debounce +twenty app dev --debounce 1500 + +# One-time sync of the current directory twenty app sync -# Uninstall the application from the current workspace -twenty app uninstall +# Add a new object interactively +twenty app add + +# Generate client types +twenty app generate + +# Watch all function logs +twenty app logs + +# Watch logs for a specific function by name +twenty app logs -n my-function ``` -## Usage (SDK) -```typescript -// Example: import what you need from the SDK -import { /* your exports */ } from 'twenty-sdk'; +## Configuration + +The CLI stores configuration per user in a JSON file: + +- Location: `~/.twenty/config.json` +- Structure: Profiles keyed by workspace name. The active profile is selected with `--workspace `. + +Example configuration file: + +```json +{ + "profiles": { + "default": { + "apiUrl": "http://localhost:3000", + "apiKey": "" + }, + "prod": { + "apiUrl": "https://api.twenty.com", + "apiKey": "" + } + } +} ``` -## Publish your application -Applications are currently stored in [`twenty/packages/twenty-apps`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps). +Notes: -You can share your application with all Twenty users: +- If a profile is missing, `apiUrl` defaults to `http://localhost:3000` until set. +- `twenty auth login` writes the `apiUrl` and `apiKey` for the default profile. +- `twenty auth login --workspace custom-workspace` writes the `apiUrl` and `apiKey` for a custom `custom-workspace` profile. -```bash -# pull the Twenty project -git clone https://github.com/twentyhq/twenty.git -cd twenty - -# create a new branch -git checkout -b feature/my-awesome-app -``` - -- Copy your app folder into `twenty/packages/twenty-apps`. -- Commit your changes and open a pull request on https://github.com/twentyhq/twenty - -```bash -git commit -m "Add new application" -git push -``` - -Our team reviews contributions for quality, security, and reusability. ## Troubleshooting - Auth errors: run `twenty auth login` again and ensure the API key has the required permissions. diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 4fcc3c273cb..2ebb4ae222f 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -1,6 +1,6 @@ { "name": "twenty-sdk", - "version": "0.2.3", + "version": "0.2.4", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/packages/twenty-sdk/src/cli/commands/app.command.ts b/packages/twenty-sdk/src/cli/commands/app.command.ts index 9de39db0ce1..d357a10ff6c 100644 --- a/packages/twenty-sdk/src/cli/commands/app.command.ts +++ b/packages/twenty-sdk/src/cli/commands/app.command.ts @@ -106,7 +106,7 @@ export class AppCommand { }); appCommand - .command('generate [outputPath]') + .command('generate [appPath]') .description('Generate Twenty client') .action(async (appPath?: string) => { await this.generateCommand.execute(formatPath(appPath)); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1766077618558-removeCanBeAssignedToApplications.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1766077618558-removeCanBeAssignedToApplications.ts new file mode 100644 index 00000000000..7eee3567bac --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1766077618558-removeCanBeAssignedToApplications.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class RemoveCanBeAssignedToApplications1766077618558 + implements MigrationInterface +{ + name = 'RemoveCanBeAssignedToApplications1766077618558'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."role" DROP COLUMN "canBeAssignedToApplications"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."role" ADD "canBeAssignedToApplications" boolean NOT NULL DEFAULT true`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts index 2358f6516f5..4338974bce8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant.ts @@ -13,5 +13,4 @@ export const FLAT_ROLE_EDITABLE_PROPERTIES: (keyof FlatRole)[] = [ 'canBeAssignedToUsers', 'canBeAssignedToAgents', 'canBeAssignedToApiKeys', - 'canBeAssignedToApplications', ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts index 573b32033af..91b8c5c88d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util.ts @@ -44,8 +44,6 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({ canBeAssignedToUsers: createRoleInput.canBeAssignedToUsers ?? true, canBeAssignedToAgents: createRoleInput.canBeAssignedToAgents ?? true, canBeAssignedToApiKeys: createRoleInput.canBeAssignedToApiKeys ?? true, - canBeAssignedToApplications: - createRoleInput.canBeAssignedToApplications ?? true, isEditable: true, workspaceId, createdAt: now, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts index 574e525c985..8eb248179be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts @@ -18,7 +18,6 @@ export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => { canBeAssignedToUsers: role.canBeAssignedToUsers, canBeAssignedToAgents: role.canBeAssignedToAgents, canBeAssignedToApiKeys: role.canBeAssignedToApiKeys, - canBeAssignedToApplications: role.canBeAssignedToApplications, workspaceId: role.workspaceId, createdAt: role.createdAt.toISOString(), updatedAt: role.updatedAt.toISOString(), diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts index ae0eaf48387..489094e8953 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/create-role-input.dto.ts @@ -70,9 +70,4 @@ export class CreateRoleInput { @IsOptional() @Field({ nullable: true }) canBeAssignedToApiKeys?: boolean; - - @IsBoolean() - @IsOptional() - @Field({ nullable: true }) - canBeAssignedToApplications?: boolean; } diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts index 9774cd0977c..fec0d2a35d1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/update-role-input.dto.ts @@ -71,11 +71,6 @@ export class UpdateRolePayload { @IsOptional() @Field({ nullable: true }) canBeAssignedToApiKeys?: boolean; - - @IsBoolean() - @IsOptional() - @Field({ nullable: true }) - canBeAssignedToApplications?: boolean; } @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index a2dc1f4cb84..6d7302d1583 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -73,9 +73,6 @@ export class RoleEntity extends SyncableEntity implements Required { @Column({ nullable: false, default: true }) canBeAssignedToApiKeys: boolean; - @Column({ nullable: false, default: true }) - canBeAssignedToApplications: boolean; - @OneToMany( () => RoleTargetEntity, (roleTargets: RoleTargetEntity) => roleTargets.role, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-flat-role-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-flat-role-metadata.util.ts index 1b0a7a74e08..5db6bec967b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-flat-role-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-flat-role-metadata.util.ts @@ -24,7 +24,6 @@ export const STANDARD_FLAT_ROLE_METADATA_BUILDERS_BY_ROLE_NAME = { canBeAssignedToUsers: true, canBeAssignedToAgents: false, canBeAssignedToApiKeys: true, - canBeAssignedToApplications: false, }, }), } satisfies { diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts index 4a3796100fe..a8d5c0cb0e5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/create-standard-role-flat-metadata.util.ts @@ -20,7 +20,6 @@ export type CreateStandardRoleContext = { canBeAssignedToUsers: boolean; canBeAssignedToAgents: boolean; canBeAssignedToApiKeys: boolean; - canBeAssignedToApplications: boolean; }; export type CreateStandardRoleArgs = StandardBuilderArgs<'role'> & { @@ -43,7 +42,6 @@ export const createStandardRoleFlatMetadata = ({ canBeAssignedToUsers, canBeAssignedToAgents, canBeAssignedToApiKeys, - canBeAssignedToApplications, }, workspaceId, twentyStandardApplicationId, @@ -68,7 +66,6 @@ export const createStandardRoleFlatMetadata = ({ canBeAssignedToUsers, canBeAssignedToAgents, canBeAssignedToApiKeys, - canBeAssignedToApplications, workspaceId, applicationId: twentyStandardApplicationId, createdAt: now, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts index a7d265a6435..42f8b1b959f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts @@ -15,6 +15,5 @@ export const ADMIN_ROLE: StandardRoleDefinition = { canBeAssignedToUsers: true, canBeAssignedToAgents: false, canBeAssignedToApiKeys: true, - canBeAssignedToApplications: false, applicationId: null, // TODO: Replace with Twenty application ID }; diff --git a/packages/twenty-shared/src/application/roleManifestType.ts b/packages/twenty-shared/src/application/roleManifestType.ts index 876ae3cc74c..6c94c1c5da2 100644 --- a/packages/twenty-shared/src/application/roleManifestType.ts +++ b/packages/twenty-shared/src/application/roleManifestType.ts @@ -56,7 +56,6 @@ export type RoleManifest = SyncableEntityOptions & { canBeAssignedToUsers?: boolean; canBeAssignedToAgents?: boolean; canBeAssignedToApiKeys?: boolean; - canBeAssignedToApplications?: boolean; objectPermissions?: ObjectPermission[]; fieldPermissions?: FieldPermission[]; permissionFlags?: PermissionFlagType[];