+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+See the [public assets section](/l/de/developers/extend/apps/config/public-assets) for details.
+
+## Styling
+
+Front components support multiple styling approaches. You can use:
+
+* **Inline styles** — `style={{ color: 'red' }}`
+* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
+* **Emotion** — CSS-in-JS with `@emotion/react`
+* **Styled-components** — `styled.div` patterns
+* **Tailwind CSS** — utility classes
+* **Any CSS-in-JS library** compatible with React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..2684760e48 --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/pt/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/pt/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/pt/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/pt/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..c2d4c0d2bc --- /dev/null +++ b/packages/twenty-docs/l/pt/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/pt/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + +
+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+See the [public assets section](/l/pt/developers/extend/apps/config/public-assets) for details.
+
+## Styling
+
+Front components support multiple styling approaches. You can use:
+
+* **Inline styles** — `style={{ color: 'red' }}`
+* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
+* **Emotion** — CSS-in-JS with `@emotion/react`
+* **Styled-components** — `styled.div` patterns
+* **Tailwind CSS** — utility classes
+* **Any CSS-in-JS library** compatible with React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..92393ec357 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/ro/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/ro/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/ro/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/ro/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..1c7eaa0011 --- /dev/null +++ b/packages/twenty-docs/l/ro/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/ro/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + +
+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+Consultați [secțiunea despre resurse publice](/l/ro/developers/extend/apps/config/public-assets) pentru detalii.
+
+## Stilizare
+
+Componentele front-end acceptă mai multe abordări de stilizare. Puteți folosi:
+
+* **Stiluri inline** — `style={{ color: 'red' }}`
+* **Componente Twenty UI** — import din `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar și altele)
+* **Emotion** — CSS-in-JS cu `@emotion/react`
+* **Styled-components** — pattern-uri `styled.div`
+* **Tailwind CSS** — clase utilitare
+* **Orice bibliotecă CSS-in-JS** compatibilă cu React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### Cum funcționează împachetarea + +Pasul de build folosește esbuild pentru a produce un singur fișier autonom pentru fiecare funcție logică și pentru fiecare componentă frontend. Toate pachetele importate sunt integrate în bundle. + +**Funcțiile logice** rulează într-un mediu Node.js. Modulele built-in Node (`fs`, `path`, `crypto`, `http` etc.) sunt disponibile și nu trebuie instalate. + +**Componentele frontend** rulează într-un Web Worker. Modulele built-in Node nu sunt disponibile — doar API-urile de browser și pachetele npm care funcționează într-un mediu de browser. + +Ambele medii au `twenty-client-sdk/core` și `twenty-client-sdk/metadata` disponibile ca module pre-furnizate — acestea nu sunt incluse în bundle, ci sunt rezolvate la rulare de către server. + +## Configurare + +Aplicația generată (scaffolded) include deja Vitest. Dacă o configurați manual, instalați dependențele: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Creați un `vitest.config.ts` în rădăcina aplicației: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Creați un fișier de configurare care verifică faptul că serverul este accesibil înainte de rularea testelor: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## API-uri SDK programatice + +Subruta `twenty-sdk/cli` exportă funcții pe care le puteți apela direct din codul de test: + +| Funcție | Descriere | +| -------------- | --------------------------------------------------------- | +| `appBuild` | Construiți aplicația și, opțional, împachetați un tarball | +| `appDeploy` | Încărcați un tarball pe server | +| `appInstall` | Instalați aplicația în spațiul de lucru activ | +| `appUninstall` | Dezinstalați aplicația din spațiul de lucru activ | + +Fiecare funcție returnează un obiect rezultat cu `success: boolean` și fie `data`, fie `error`. + +## Scrierea unui test de integrare + +Iată un exemplu complet care construiește, distribuie și instalează aplicația, apoi verifică faptul că aceasta apare în spațiul de lucru: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Rularea testelor + +Asigurați-vă că serverul Twenty local rulează, apoi: + +```bash filename="Terminal" +yarn test +``` + +Sau în modul watch în timpul dezvoltării: + +```bash filename="Terminal" +yarn test:watch +``` + +## Verificarea tipurilor + +Puteți rula și verificarea tipurilor pe aplicație fără a rula testele: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +Aceasta rulează `tsc --noEmit` și raportează orice erori de tip. + +## CI cu GitHub Actions + +Scaffolderul generează un workflow GitHub Actions gata de utilizare în `.github/workflows/ci.yml`. Rulează automat testele de integrare la fiecare push pe `main` și la pull request-uri. + +Workflow-ul: + +1. Preia codul +2. Pornește un server Twenty temporar folosind acțiunea `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` +3. Instalează dependențele cu `yarn install --immutable` +4. Rulează `yarn test` cu `TWENTY_API_URL` și `TWENTY_API_KEY` injectate din rezultatele acțiunii + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +Nu trebuie să configurați niciun secret — acțiunea `spawn-twenty-docker-image` pornește un server Twenty efemer direct în runner și oferă detaliile de conectare. Secretul `GITHUB_TOKEN` este furnizat automat de GitHub. + +Pentru a fixa o versiune Twenty specifică în loc de `latest`, modificați variabila de mediu `TWENTY_VERSION` din partea de sus a workflow-ului. diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..46af2e8c9d --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/ru/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/ru/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/ru/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/ru/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..38efdbe1e9 --- /dev/null +++ b/packages/twenty-docs/l/ru/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/ru/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + +
+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+See the [public assets section](/l/ru/developers/extend/apps/config/public-assets) for details.
+
+## Styling
+
+Front components support multiple styling approaches. You can use:
+
+* **Inline styles** — `style={{ color: 'red' }}`
+* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
+* **Emotion** — CSS-in-JS with `@emotion/react`
+* **Styled-components** — `styled.div` patterns
+* **Tailwind CSS** — utility classes
+* **Any CSS-in-JS library** compatible with React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..633d12dc0f --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/tr/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/tr/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/tr/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/tr/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..cb66b87a8e --- /dev/null +++ b/packages/twenty-docs/l/tr/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/tr/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + +
+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+See the [public assets section](/l/tr/developers/extend/apps/config/public-assets) for details.
+
+## Styling
+
+Front components support multiple styling approaches. You can use:
+
+* **Inline styles** — `style={{ color: 'red' }}`
+* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
+* **Emotion** — CSS-in-JS with `@emotion/react`
+* **Styled-components** — `styled.div` patterns
+* **Tailwind CSS** — utility classes
+* **Any CSS-in-JS library** compatible with React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx new file mode 100644 index 0000000000..eba2fab940 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/application.mdx @@ -0,0 +1,65 @@ +--- +title: Application Config +description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication. +icon: rocket +--- + +Every app must have exactly one `defineApplication` call. It declares: + +* **Identity** — universal identifier, display name, description. +* **Permissions** — which role its logic functions and front components run under. +* **Variables** *(optional)* — key–value pairs exposed to your code as environment variables. +* **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/l/zh/developers/extend/apps/logic/logic-functions). + +```ts src/application-config.ts +import { defineApplication } from 'twenty-sdk/define'; +import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role'; + +export default defineApplication({ + universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d', + displayName: 'My Twenty App', + description: 'My first Twenty app', + applicationVariables: { + DEFAULT_RECIPIENT_NAME: { + universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de', + description: 'Default recipient name for postcards', + value: 'Jane Doe', + isSecret: false, + }, + }, + defaultRoleUniversalIdentifier: DEFAULT_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 and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`). +* `defaultRoleUniversalIdentifier` must reference a role defined with [`defineRole()`](/l/zh/developers/extend/apps/config/roles). +* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`. + +## Default function role + +The `defaultRoleUniversalIdentifier` controls what the app's logic functions and front components can access: + +* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role. +* The typed API client is restricted to the permissions granted to that role. +* Follow least-privilege: declare only the permissions your functions need. + +When you scaffold a new app, the CLI creates a starter role file at `src/roles/default-role.ts`. See [Roles & Permissions](/l/zh/developers/extend/apps/config/roles) for the full reference. + +## Marketplace metadata + +If you plan to [publish your app](/l/zh/developers/extend/apps/operations/publishing), these optional fields control how it appears in the marketplace: + +| Field | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------- | +| `author` | Author or company name | +| `category` | App category for marketplace filtering | +| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) | +| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) | +| `aboutDescription` | Longer markdown description for the "About" tab. If omitted, the marketplace uses the package's `README.md` from npm | +| `websiteUrl` | Link to your website | +| `termsUrl` | Link to terms of service | +| `emailSupport` | Support email address | +| `issueReportUrl` | Link to issue tracker | diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx new file mode 100644 index 0000000000..734ffc2828 --- /dev/null +++ b/packages/twenty-docs/l/zh/developers/extend/apps/config/install-hooks.mdx @@ -0,0 +1,206 @@ +--- +title: Install Hooks +description: Run logic before or after the install — seed data, back up records, validate the upgrade. +icon: wrench +--- + +Install hooks are special logic functions that run during the install or upgrade lifecycle. They share the same handler runtime as regular [logic functions](/l/zh/developers/extend/apps/logic/logic-functions) and receive an `InstallPayload`, but they're declared with their own define functions — `definePostInstallLogicFunction()` and `definePreInstallLogicFunction()` — and live outside the normal trigger model (HTTP, cron, database events). + +Each app may define **at most one pre-install** and **at most one post-install** function. The manifest build will error if more than one of either is detected. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ install flow │ +│ │ +│ upload package → [pre-install] → metadata migration → │ +│ generate SDK → [post-install] │ +│ │ +│ old schema visible new schema visible │ +└─────────────────────────────────────────────────────────────┘ +``` + +
+
+
+
+
+
+
+
+This component renders inside Twenty.
+
+User: {userId}
+Record: {recordId ?? 'No record context'}
+Component: {componentId}
+Archive this record?
+ +Export {selectedRecordIds.length} selected record(s)?
+ +
;
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'logo',
+ component: Logo,
+});
+```
+
+See the [public assets section](/l/zh/developers/extend/apps/config/public-assets) for details.
+
+## Styling
+
+Front components support multiple styling approaches. You can use:
+
+* **Inline styles** — `style={{ color: 'red' }}`
+* **Twenty UI components** — import from `twenty-sdk/ui` (Button, Tag, Status, Chip, Avatar, and more)
+* **Emotion** — CSS-in-JS with `@emotion/react`
+* **Styled-components** — `styled.div` patterns
+* **Tailwind CSS** — utility classes
+* **Any CSS-in-JS library** compatible with React
+
+```tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { Button, Tag, Status } from 'twenty-sdk/ui';
+
+const StyledWidget = () => {
+ return (
+ Today is {format(new Date(), 'MMMM do, yyyy')}
; +}; + +export default defineFrontComponent({ + universalIdentifier: '...', + name: 'date-widget', + component: DateWidget, +}); +``` + +### How bundling works + +The build step uses esbuild to produce a single self-contained file per logic function and per front component. All imported packages are inlined into the bundle. + +**Logic functions** run in a Node.js environment. Node built-in modules (`fs`, `path`, `crypto`, `http`, etc.) are available and do not need to be installed. + +**Front components** run in a Web Worker. Node built-in modules are **not** available — only browser APIs and npm packages that work in a browser environment. + +Both environments have `twenty-client-sdk/core` and `twenty-client-sdk/metadata` available as pre-provided modules — these are not bundled but resolved at runtime by the server. + +## Setup + +The scaffolded app already includes Vitest. If you set it up manually, install the dependencies: + +```bash filename="Terminal" +yarn add -D vitest vite-tsconfig-paths +``` + +Create a `vitest.config.ts` at the root of your app: + +```ts vitest.config.ts +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + tsconfigPaths({ + projects: ['tsconfig.spec.json'], + ignoreConfigErrors: true, + }), + ], + test: { + testTimeout: 120_000, + hookTimeout: 120_000, + include: ['src/**/*.integration-test.ts'], + setupFiles: ['src/__tests__/setup-test.ts'], + env: { + TWENTY_API_URL: 'http://localhost:2020', + TWENTY_API_KEY: 'your-api-key', + }, + }, +}); +``` + +Create a setup file that verifies the server is reachable before tests run: + +```ts src/__tests__/setup-test.ts +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { beforeAll } from 'vitest'; + +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test'); + +beforeAll(async () => { + // Verify the server is running + const response = await fetch(`${TWENTY_API_URL}/healthz`); + + if (!response.ok) { + throw new Error( + `Twenty server is not reachable at ${TWENTY_API_URL}. ` + + 'Start the server before running integration tests.', + ); + } + + // Write a temporary config for the SDK + fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true }); + + fs.writeFileSync( + path.join(TEST_CONFIG_DIR, 'config.json'), + JSON.stringify({ + remotes: { + local: { + apiUrl: process.env.TWENTY_API_URL, + apiKey: process.env.TWENTY_API_KEY, + }, + }, + defaultRemote: 'local', + }, null, 2), + ); +}); +``` + +## Programmatic SDK APIs + +The `twenty-sdk/cli` subpath exports functions you can call directly from test code: + +| Function | Description | +| -------------- | ------------------------------------------- | +| `appBuild` | Build the app and optionally pack a tarball | +| `appDeploy` | Upload a tarball to the server | +| `appInstall` | Install the app on the active workspace | +| `appUninstall` | Uninstall the app from the active workspace | + +Each function returns a result object with `success: boolean` and either `data` or `error`. + +## Writing an integration test + +Here is a full example that builds, deploys, and installs the app, then verifies it appears in the workspace: + +```ts src/__tests__/app-install.integration-test.ts +import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config'; +import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const APP_PATH = process.cwd(); + +describe('App installation', () => { + beforeAll(async () => { + const buildResult = await appBuild({ + appPath: APP_PATH, + tarball: true, + onProgress: (message: string) => console.log(`[build] ${message}`), + }); + + if (!buildResult.success) { + throw new Error(`Build failed: ${buildResult.error?.message}`); + } + + const deployResult = await appDeploy({ + tarballPath: buildResult.data.tarballPath!, + onProgress: (message: string) => console.log(`[deploy] ${message}`), + }); + + if (!deployResult.success) { + throw new Error(`Deploy failed: ${deployResult.error?.message}`); + } + + const installResult = await appInstall({ appPath: APP_PATH }); + + if (!installResult.success) { + throw new Error(`Install failed: ${installResult.error?.message}`); + } + }); + + afterAll(async () => { + await appUninstall({ appPath: APP_PATH }); + }); + + it('should find the installed app in the workspace', async () => { + const metadataClient = new MetadataApiClient(); + + const result = await metadataClient.query({ + findManyApplications: { + id: true, + name: true, + universalIdentifier: true, + }, + }); + + const installedApp = result.findManyApplications.find( + (app: { universalIdentifier: string }) => + app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, + ); + + expect(installedApp).toBeDefined(); + }); +}); +``` + +## Running tests + +Make sure your local Twenty server is running, then: + +```bash filename="Terminal" +yarn test +``` + +Or in watch mode during development: + +```bash filename="Terminal" +yarn test:watch +``` + +## Type checking + +You can also run type checking on your app without running tests: + +```bash filename="Terminal" +yarn twenty typecheck +``` + +This runs `tsc --noEmit` and reports any type errors. + +## CI with GitHub Actions + +The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests. + +The workflow: + +1. Checks out your code +2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action +3. Installs dependencies with `yarn install --immutable` +4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs + +```yaml .github/workflows/ci.yml +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + TWENTY_VERSION: latest + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Spawn Twenty instance + id: twenty + uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main + with: + twenty-version: ${{ env.TWENTY_VERSION }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Run integration tests + run: yarn test + env: + TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }} + TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }} +``` + +You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub. + +To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow.