snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
title: 애플리케이션 구성
description: "`defineApplication`을 사용하여 앱의 식별 정보, 기본 역할, 변수 및 마켓플레이스 메타데이터를 선언합니다."
icon: rocket
---
모든 앱에는 `defineApplication` 호출이 정확히 하나 있어야 합니다. 다음 항목을 선언합니다:
* **식별 정보** — 범용 식별자, 표시 이름, 설명.
* **권한** — 로직 함수와 프런트 컴포넌트가 어떤 역할로 실행되는지.
* **변수** *(선택 사항)* — 코드에 환경 변수로 노출되는 키–값 쌍.
* **설치 전/설치 후/제거 훅** *(선택 사항)* — [Logic Functions](/l/ko/developers/extend/apps/logic/logic-functions)를 참조하세요.
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
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,
},
},
});
```
노트:
* `universalIdentifier` 필드는 여러분이 소유하는 변하지 않는 고유 ID입니다. 한 번만 생성하고 이후 동기화 동안에도 변하지 않도록 유지하세요.
* `applicationVariables`는 함수와 프런트 컴포넌트의 환경 변수가 됩니다. 로직 함수(서버 사이드)에서는 `process.env.VARIABLE_NAME`으로 사용할 수 있습니다. 프런트 컴포넌트에서는 `twenty-sdk/front-component`의 `getApplicationVariable('VARIABLE_NAME')`을 사용하세요. `isSecret: true`로 표시된 변수는 로직 함수에만 주입됩니다. 프런트 컴포넌트는 비밀이 아닌 변수만 받습니다.
* 기본 역할은 [`defineApplicationRole()`](/l/ko/developers/extend/apps/config/roles)로 표시된 역할 파일에서 자동으로 감지되므로, `defineApplication()`에서 이를 참조할 필요가 없습니다.
* 설치 전, 설치 후, 제거 함수는 매니페스트 빌드 중 자동으로 감지됩니다 — `defineApplication()`에서 별도로 참조할 필요가 없습니다.
* 하위 호환성을 위해 `defaultRoleUniversalIdentifier`를 명시적으로 전달하는 방식도 계속 지원되지만, 이제는 `defineApplicationRole()` 사용을 권장하며 이전 방식은 더 이상 권장되지 않습니다.
* `serverVariables`는 인스턴스 범위의 구성 및 비밀(예: API 키)입니다. `applicationVariables`와 달리, 매니페스트에는 값을 선언하지 않으며, 워크스페이스 운영자가 앱 설정에서 값을 채워 넣으면 설정된 이후에만 로직 함수에 주입됩니다.
* 두 종류의 변수 모두 `isDeprecated: true`를 허용합니다. 변수를 삭제하는 대신 사용 중지할 때 사용하십시오. 키 선언을 유지하면 저장된 값이 보존되고(삭제하면 운영자가 입력한 값이 사라집니다), 변수는 여전히 주입되므로 코드에서 `process.env.NEW_API_KEY ?? process.env.API_KEY`로 폴백할 수 있습니다. 더 이상 사용되지 않는 변수는 값이 없어지면 앱 설정에서 사라지며, 앱의 구성 검사에 절대 포함되지 않습니다. 따라서 `isDeprecated`가 `isRequired`보다 우선합니다.
* 앱의 **Settings** 탭에서 기본 변수 구성 섹션 대신 사용자 지정 구성 UI를 렌더링하려면, 별도의 파일에서 [`defineSettingsFrontComponent()`](/l/ko/developers/extend/apps/layout/front-components#custom-settings-component)를 사용하여 프론트 컴포넌트를 선언하세요. 앱당 하나만 허용됩니다. 시스템에서 관리하는 섹션(auto-upgrade, App URL, connections)은 항상 표시된 상태로 유지됩니다.
## 변수 유형
`applicationVariables`와 `serverVariables`는 모두 선택적인 `type`을 허용하며, `SELECT` / `MULTI_SELECT`의 경우 `options` 목록을 허용합니다. 지원되는 타입: `TEXT`(기본값), `BOOLEAN`, `NUMBER`, `NUMERIC`, `DATE`, `DATE_TIME`, `SELECT`, `MULTI_SELECT`, `ARRAY`, `RAW_JSON`, `RICH_TEXT`.
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
`type`은 **표현과 검증**에만 영향을 줍니다. 즉, 워크스페이스 설정 UI에서 해당하는 입력 요소(토글, 숫자 필드, 드롭다운, 날짜 선택기, JSON 편집기 등)를 선택합니다. 또한 빌드가 구성(config)을 검증하도록 합니다(예를 들어 `SELECT` / `MULTI_SELECT`는 비어 있지 않은 `options`를 선언해야 합니다). 값이 코드로 전달되는 방식은 **변경되지 않습니다**.
값은 **항상 문자열로 주입**됩니다. 이는 환경 변수의 특성 때문입니다(`process.env.*`는 문자열만 허용). 로직 함수가 실행될 때, 실행기는 선언된 `type`에 따라 각 값을 직렬화하여 `process.env`를 구성하므로, 값이 어떻게 설정되었는지(매니페스트 기본값, 설정 UI, 또는 이전 버전)와 관계없이 문자열 형식이 일관되게 유지됩니다:
| 유형 | `process.env` 문자열 |
| ------------------------------------- | --------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | 원시 값(`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | 10진수 문자열(`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | JSON 배열(`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | JSON 객체(`'{"retries":3}'`) |
문자열을 다시 기대하는 타입으로 파싱하세요:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
`getApplicationVariable('VARIABLE_NAME')`을 통해 값을 읽는 프런트 컴포넌트에도 동일하게 적용됩니다. 반환되는 값은 문자열이므로, 필요에 따라 파싱해야 합니다.
## 기본 함수 역할
[`defineApplicationRole()`](/l/ko/developers/extend/apps/config/roles)로 선언된 역할은 앱의 로직 함수와 프런트엔드 컴포넌트가 무엇에 접근할 수 있는지를 제어합니다:
* 로직 함수에 주입되는 런타임 토큰은 이 역할에서 파생됩니다. 사용자 권한으로 작동하는 호출은 해당 사용자가 할 수 있는 작업으로 더욱 제한되므로, 어느 쪽의 권한도 초과할 수 없습니다. [호출이 사용하는 액세스 권한](/l/ko/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses)을 참조하세요.
* 타입드 API 클라이언트는 해당 역할에 부여된 권한으로 제한됩니다.
* 최소 권한 원칙을 따르세요: 함수에 필요한 권한만 선언하세요.
새 앱을 스캐폴딩하면 CLI가 `src/roles/default-role.ts`에 시작용 역할 파일을 생성합니다. 전체 내용은 [Roles & Permissions](/l/ko/developers/extend/apps/config/roles)를 참조하세요.
## 마켓플레이스 메타데이터
앱을 [게시](/l/ko/developers/extend/apps/operations/publishing)할 계획이라면, 다음 선택적 필드로 마켓플레이스에서 표시되는 방식을 제어합니다:
| 필드 | 설명 |
| ------------------ | ------------------------------------------------------------------- |
| `author` | 작성자 또는 회사 이름 |
| `category` | 마켓플레이스 필터링을 위한 앱 카테고리 |
| `logo` | `public/`에 번들된 앱 로고의 경로(예: `public/logo.png`) |
| `galleryImages` | `public/`에 번들된 갤러리 이미지 경로의 배열(예: `public/screenshot-1.png`) |
| `aboutDescription` | "About" 탭에 대한 더 긴 마크다운 설명. 생략하면 마켓플레이스는 npm의 패키지 `README.md`를 사용합니다 |
| `websiteUrl` | 웹사이트 링크 |
| `termsUrl` | 서비스 약관 링크 |
| `emailSupport` | 지원 이메일 주소 |
| `issueReportUrl` | 이슈 트래커 링크 |
<Note>
`logoUrl` 및 `screenshots`는 `logo` 및 `galleryImages`의 더 이상 사용되지 않는 별칭입니다. 다음 필드에는 외부 절대 URL(`http://` 또는 `https://`)이 지원되지 않습니다. 빌드 시 경고와 함께 제거됩니다. 대신 앱의 `public/` 폴더에 이미지를 번들하세요.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: 설치 훅
description: 설치, 업그레이드 또는 제거 라이프사이클 동안 로직을 실행하여 데이터를 시드하고, 레코드를 백업하고, 업그레이드를 검증하고, 외부 리소스를 정리합니다.
icon: wrench
---
설치 훅은 설치, 업그레이드 또는 제거 라이프사이클 동안 실행되는 특수한 로직 함수입니다. 이들은 일반 [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions)와 동일한 핸들러 런타임을 공유하지만, 자체 define 함수로 선언되며 일반 트리거 모델(HTTP, cron, 데이터베이스 이벤트) 외부에서 동작합니다. 설치 훅은 `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — 새로 설치하는 경우 `previousVersion`은 `undefined`임)을 받고, 제거 훅은 제거되는 버전을 나타내는 `UninstallPayload` (`{ version?: string }`)을 받습니다.
각 앱은 각 훅(pre-install, post-install, uninstall)을 **최대 하나씩만** 정의할 수 있습니다. 어떤 종류든 둘 이상 감지되면 매니페스트 빌드에서 오류가 발생합니다.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## 한눈에 보기
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
| 실행 | 메타데이터 마이그레이션 이전 — **이전** 스키마와 데이터는 그대로 유지됨 | 마이그레이션 및 SDK 생성 이후 — **새로운** 스키마가 적용됨 |
| 실행 | 항상 동기식; 설치를 차단함 | 기본적으로 비동기(대기열에 등록, 최대 3회 재시도); `shouldRunSynchronously: true`로 동기 실행을 선택할 수 있음 |
| 실패 시 | 스키마 변경 이전에 설치가 **중단**됨 | 비동기: 최대 3회까지 재시도됩니다. 동기: 호출자는 `POST_INSTALL_ERROR`를 받음(스키마 변경은 **롤백되지 않습니다**) |
| 일반적인 사용 | 마이그레이션으로 손실될 데이터를 백업하거나 수정함; 예외를 던져 위험한 업그레이드를 거부 | 기본 데이터 시딩, 워크스페이스 구성, 외부 리소스 등록 |
**기본 원칙:** 기본적으로 post-install을 사용하세요. 마이그레이션 자체가 파괴적이며 이전 상태가 사라지기 전에 이를 가로채야 할 때에만 pre-install을 사용하세요.
| 원하는 작업... | 사용 |
| -------------------------------- | --------------------------------------------------- |
| 데이터 시딩, 워크스페이스 구성, 외부 리소스 등록 | `post-install` |
| 설치 응답을 차단해서는 안 되는 장시간 작업 | `post-install` (기본 비동기 모드, 워커 재시도 포함) |
| 설치 호출이 반환된 직후 호출자가 즉시 의존하는 빠른 설정 | `shouldRunSynchronously: true`를 사용하는 `post-install` |
| 곧 진행될 마이그레이션으로 손실될 데이터를 읽거나 백업 | `pre-install` |
| 기존 데이터를 손상시킬 업그레이드를 거부 | `pre-install` (핸들러에서 예외를 던짐) |
| 모든 업그레이드 시 상태 조정 수행 | `shouldRunOnVersionUpgrade: true`가 설정된 어느 훅이든 사용 |
## 두 훅에 공통으로 적용되는 동작
* 구성은 트리거 설정을 제외한 `defineLogicFunction` 구성에 `shouldRunOnVersionUpgrade`가 추가된 형태입니다.
* **실행 시점**: 기본적으로 신규 설치에서만 실행됩니다. 업그레이드 시에도 실행하려면 `shouldRunOnVersionUpgrade: true`를 설정합니다. 업그레이드 경로에 따라 분기하기 위해 `previousVersion` / `newVersion`을 사용합니다.
* **멱등성이 중요합니다**: 비동기 post-install은 재시도될 수 있고, `shouldRunOnVersionUpgrade`가 켜져 있으면 두 훅 모두 업그레이드 시 다시 실행됩니다.
* 일반적인 로직 함수 환경(`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`)이 주입되므로, 앱의 토큰으로 Twenty API를 호출할 수 있습니다.
* 훅은 빌드 시 애플리케이션 매니페스트에 자동으로 연결됩니다(`preInstallLogicFunction` / `postInstallLogicFunction`) — [`defineApplication()`](/l/ko/developers/extend/apps/config/application)에서 참조할 것은 없습니다.
* 기본 `timeoutSeconds`는 데이터 시딩과 같은 더 긴 설정 작업을 허용하기 위해 300으로 설정되어 있습니다.
* **dev 모드에서는 실행되지 않음**: `yarn twenty dev`는 설치 플로우를 건너뛰고 파일을 직접 동기화하므로, 해당 환경에서는 훅이 전혀 실행되지 않습니다. 대신 수동으로 트리거하세요:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="워크스페이스 메타데이터 마이그레이션이 적용된 후에 실행됩니다.">
앱 설치가 완료된 후 한 번 실행됩니다: 메타데이터 동기화 완료, SDK 클라이언트 생성, 새로운 스키마 쿼리 가능 상태. 예시 — 신규 설치에서 기본 레코드를 시딩하기:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
`shouldRunSynchronously` 플래그가 실행 모델을 제어합니다:
* `false` *(기본값)* — 메시지 큐에 등록되고(`retryLimit: 3`), 워커에 의해 실행됩니다. 작업이 큐에 등록되면 설치 응답이 즉시 반환됩니다. **장시간 작업에 사용** — 대용량 데이터셋 시딩, 지연이 긴 서드파티 API 호출 등.
* `true` — 설치 플로우 중에 인라인으로 실행됩니다. 설치 요청은 핸들러가 종료될 때까지 블로킹되며, 예외가 발생하면 호출자에게 `POST_INSTALL_ERROR`로 전달됩니다(재시도 없음). **빠르고, 응답 전에 반드시 완료되어야 하는 작업에 사용하세요.** 이 시점에는 이미 마이그레이션이 적용되었으므로, 실패하더라도 스키마 변경은 롤백되지 않고 오류만 노출됩니다.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="워크스페이스 메타데이터 마이그레이션이 적용되기 전에 실행됩니다.">
메타데이터 마이그레이션 이전, **이전** 스키마를 대상으로 실행됩니다 — 마이그레이션으로 손실될 데이터를 백업하거나, 위험한 업그레이드를 거부하기에 적절한 위치입니다. 실행에 앞서, 서버는 순수 추가식의 "간소화된 동기화"를 수행하여 새 버전의 pre-install 함수만 등록하고, 나머지 — 이전 버전의 오브젝트, 필드, 데이터 — 는 핸들러가 실행될 때까지 변경하지 않습니다.
pre-install은 항상 **동기식**이며 설치를 차단합니다. 핸들러에서 예외를 던지면, 어떤 스키마 변경도 적용되기 전에 설치가 중단되며 — 워크스페이스는 일관된 상태로 이전 버전에 머무릅니다. 이는 의도된 동작입니다: pre-install은 위험한 업그레이드를 거부할 수 있는 마지막 기회입니다.
예시 — 마이그레이션이 기존 필드를 삭제하기 전에 해당 필드 값을 복사하기:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## 제거 훅
`defineUninstallLogicFunction`은 사용자가 앱을 제거할 때 실행되는 훅을 선언합니다. 이 훅은 앱의 메타데이터, 데이터, 코드가 제거되기 **이전**에 실행됩니다. 삭제 마이그레이션이 실행된 후에는 더 이상 실행할 것이 남지 않으므로, 핸들러는 여전히 앱의 오브젝트와 레코드를 쿼리할 수 있습니다. 이를 외부 리소스 정리에 사용하십시오. 예를 들어 API 리소스 프로비저닝 해제, 남은 봇 삭제, 웹훅 해지에 사용할 수 있습니다.
노트:
* 이 훅은 최대한 시도(best-effort) 방식으로 동작합니다. 동기적으로 실행되지만, 실패해도 로그에만 기록되고 **제거를 차단하지 않습니다**. 정리 작업 때문에 앱을 제거할 수 없게 만들어서는 안 됩니다.
* 이 훅은 제거되는 버전을 나타내는 `UninstallPayload` (`{ version?: string }`)을 받습니다.
* 새 설치가 실패하여 롤백될 때는 이 훅이 **실행되지 않습니다**. 앱 설치가 끝까지 완료되지 않았기 때문입니다.
* 앱이 제거된 후에는 이 훅을 실행할 수 없으므로, 앱 데이터(예: 레코드에 저장된 봇 ID)에 의존하는 외부 정리 작업은 외부 예약 작업이 아니라 여기에서 수행해야 합니다.
* 설치 훅과 마찬가지로, 이 훅은 **개발 모드에서는 실행되지 않습니다**. 대신 수동으로 트리거해야 합니다:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: 개요
description: 앱 자체를 구성합니다. 앱의 ID, 기본 권한, 그리고 설치 시점에 무엇이 실행되는지를 설정합니다.
icon: screwdriver-wrench
---
Twenty 앱의 **구성 레이어(config layer)** 는 앱의 ID, 보유한 권한, 설치나 업그레이드 시 실행되는 코드를 포함해, 앱을 *플랫폼에 어떻게 설명할지* 정의합니다. 이 선언들은 새로운 데이터 구조나 런타임 동작을 추가하지 않습니다. 대신 Twenty에게 앱이 무엇인지와 어떻게 설정해야 하는지를 알려줍니다.
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## 이 섹션에서 다루는 내용
<CardGroup cols={2}>
<Card title="애플리케이션 구성" icon="rocket" href="/l/ko/developers/extend/apps/config/application">
`defineApplication` — ID, 기본 역할, 변수, 마켓플레이스 메타데이터를 정의합니다.
</Card>
<Card title="역할 및 권한" icon="shield-halved" href="/l/ko/developers/extend/apps/config/roles">
`defineRole` — 앱의 로직 함수가 무엇을 읽고 쓸 수 있는지 선언합니다.
</Card>
<Card title="설치 훅" icon="wrench" href="/l/ko/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction`, 그리고 `defineUninstallLogicFunction` — 데이터를 백업하고, 기본값을 시드하며, 업그레이드를 검증하고, 제거 시 정리를 수행합니다.
</Card>
</CardGroup>
## 구성 요소 간의 관계
* **애플리케이션**이 진입점입니다. 모든 앱에는 정확히 한 번의 `defineApplication()` 호출이 있으며, 이 호출은 하나의 \*\*역할(Role)\*\*을 기본값으로 가리킵니다.
* \*\*역할(Role)\*\*은 앱의 로직 함수와 프런트 컴포넌트가 무엇을 읽고 쓸 수 있는지를 제어합니다. 최소 권한 원칙을 따르세요. 코드에 실제로 필요한 권한만 부여하세요.
* \*\*설치 훅(Install Hooks)\*\*은 설치나 업그레이드 중에 실행됩니다. 메타데이터 마이그레이션 이전에 실행되는 사전 설치 훅은 위험한 업그레이드를 거부할 수 있고, 마이그레이션 이후에 실행되는 사후 설치 훅은 새 스키마에 맞춰 기본 데이터를 시드할 수 있습니다. 제거 훅은 앱이 제거되기 직전에 실행되므로, 앱의 데이터를 아직 읽을 수 있을 때 외부 리소스를 정리할 수 있습니다.
<Note>
설치 훅은 [로직 함수](/l/ko/developers/extend/apps/logic/logic-functions) 런타임을 공유합니다. 동일한 핸들러 시그니처, 동일한 환경 변수, 동일한 타입이 지정된 API 클라이언트를 사용하지만, 자체 define 함수로 선언되고 일반 트리거 모델(HTTP, cron, 데이터베이스 이벤트) 외부에서 동작합니다.
</Note>

View File

@@ -0,0 +1,67 @@
---
title: 퍼블릭 에셋
description: 정적 파일(이미지, 아이콘, 폰트 등)을 public/ 폴더를 통해 앱과 함께 제공합니다.
icon: folder-open
---
앱 루트의 `public/` 폴더에는 정적 파일이 있습니다 — 이미지, 아이콘, 폰트 또는 런타임에 필요한 기타 에셋. 이 파일들은 빌드에 자동 포함되고, 개발 모드에서 동기화되며, 서버로 업로드됩니다.
`public/`에 있는 파일은 다음과 같은 특성을 가집니다:
* **공개적으로 접근 가능** — 서버로 동기화되면 에셋은 공개 URL로 제공됩니다. 접근하려면 인증이 필요하지 않습니다.
* **프런트 컴포넌트에서 사용 가능** — React 컴포넌트 내부에서 이미지, 아이콘 또는 미디어를 표시하기 위해 에셋 URL을 사용하세요.
* **로직 함수에서 사용 가능** — 이메일, API 응답 또는 서버 측 로직에서 에셋 URL을 참조하세요.
* **마켓플레이스 메타데이터에 사용** — `defineApplication()`의 `logo` 및 `galleryImages` 필드는 이 폴더의 파일을 참조합니다(예: `public/logo.png`). 앱이 게시되면 이러한 항목이 마켓플레이스에 표시됩니다. 이 필드에서 외부 절대 URL은 무시되므로, 대신 이미지를 `public/` 폴더에 번들링하세요. 각 이미지는 10 MB를 초과해서는 안 됩니다.
* **개발 모드에서 자동 동기화** — `public/`에 파일을 추가, 업데이트 또는 삭제하면 자동으로 서버와 동기화됩니다. 재시작이 필요 없습니다.
* **빌드에 포함** — `yarn twenty dev:build`는 모든 퍼블릭 에셋을 배포 출력물에 번들합니다.
## `getPublicAssetUrl`로 퍼블릭 에셋에 접근하기
`twenty-sdk`의 `getPublicAssetUrl` 헬퍼를 사용해 `public/` 디렉터리의 파일 전체 URL을 가져오세요. 이는 로직 함수와 프런트 컴포넌트 모두에서 동작합니다.
**로직 함수에서:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**프런트 컴포넌트에서:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
`path` 인수는 앱의 `public/` 폴더를 기준으로 합니다. `getPublicAssetUrl('logo.png')`과 `getPublicAssetUrl('public/logo.png')`는 동일한 URL로 해석됩니다 — `public/` 접두사가 있으면 자동으로 제거됩니다.

View File

@@ -0,0 +1,204 @@
---
title: 역할 및 권한
description: 앱의 로직 함수와 프런트 컴포넌트가 어떤 객체와 필드를 읽고 쓸 수 있는지 선언합니다.
icon: shield-halved
---
\*\*역할(role)\*\*은 권한 집합입니다. 즉, 앱이 어떤 객체를 읽거나 쓸 수 있는지, 어떤 필드를 볼 수 있는지, 그리고 어떤 플랫폼 수준 기능을 사용할 수 있는지를 정의합니다. 모든 앱의 로직 함수와 프런트 컴포넌트는 `defineApplicationRole()`로 표시된 역할의 권한을 상속받습니다(아래 [기본 함수 역할](#the-default-function-role)을 참조하세요).
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## 행 수준 보안
오브젝트 및 필드 권한은 역할이 *어떤 오브젝트와 필드* 를 다룰 수 있는지를 결정합니다. **행 수준
권한 프레디킷** 은 더 나아가 역할이 *어떤 레코드* 를 보고 작업할 수 있는지를 결정합니다. 예를 들어, 각 외부 사용자가 자신의 레코드만 볼 수 있는 셀프 서비스 역할 같은 경우입니다.
역할에 `rowLevelPermissionPredicates` 로 프레디킷을 선언합니다. 매니페스트의 나머지와 마찬가지로,
각 프레디킷은 자체 `universalIdentifier` 를 가지고, 오브젝트와 필드를 각각의
`universalIdentifier`, `operand`, 그리고 (선택적으로) 쿼리 시점에 값이 주입되는 workspaceMember 필드를 통해 참조합니다. 이렇게 하면 "레코드의 owner relation **is** 현재 workspace member이다"를 표현할 수 있습니다:
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
프레디킷은 매니페스트에 함께 포함되어 배포되므로, 모든 설치와 업그레이드 시 역할과 함께 생성·업데이트·제거됩니다. 동기화를 유지하기 위한 별도의 설치 후 단계는 필요하지 않습니다.
### 프레디킷을 그룹과 결합하기
기본적으로 역할의 프레디킷은 `AND`로 결합됩니다. 일부를 `OR`로 결합하거나(또는 논리를 중첩하려면) `rowLevelPermissionPredicateGroups` 항목을 선언하고 각 프레디킷이 `predicateGroupUniversalIdentifier`를 통해 해당 그룹을 가리키도록 설정합니다. 이 역할은 파트너가 자신이 **소유한** 또는 **담당자**인 기회(Opportunity)를 볼 수 있도록 허용합니다:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
노트:
* 모든 프레디킷과 그룹에 안정적인 `universalIdentifier`(임의의 uuid)를 부여하세요. 이는 업그레이드 전반에 걸쳐 엔티티의 키로 사용되며, 프레디킷은 이를 통해 그룹을 참조합니다.
* 프레디킷은 앱에서 소유한 오브젝트와 필드뿐만 아니라 Twenty의 표준 오브젝트가 소유한 오브젝트와 필드를 참조할 수 있습니다.
* 행 수준 보안은 이를 포함한 플랜의 워크스페이스에 대해 적용됩니다. 다른 플랜에서도 프레디킷은 계속 동기화되지만, 적용되지는 않습니다.
## 기본 함수 역할
새 앱을 스캐폴딩하면, CLI가 `defineApplicationRole()`로 선언된 기본 역할 파일을 생성합니다:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()`는 설치 시 애플리케이션의 기본 역할로 사용할 역할을 표시하는, `defineRole()`에 대한 간단한 래퍼입니다. 검증 방식은 `defineRole`과 동일하지만, 빌드 파이프라인이 애플리케이션 매니페스트의 `defaultRoleUniversalIdentifier`에 해당 역할의 `universalIdentifier`를 자동으로 연결하므로, [`defineApplication`](/l/ko/developers/extend/apps/config/application)에서 직접 참조할 필요가 없습니다.
노트:
* 앱당 정확히 **하나의** `defineApplicationRole(...)`만 허용되며, 둘 이상 발견되면 매니페스트 빌드가 실패합니다.
* 앱에서 제공하는 **추가** 역할에는 `defineApplicationRole()`가 아니라 `defineRole()`을 사용하세요.
* 호환성을 위해 `defineApplication()`에서 `defaultRoleUniversalIdentifier`를 명시적으로 설정하는 방식도 여전히 지원되지만, 이제는 `defineApplicationRole()` 방식이 권장되며 이전 방식은 더 이상 권장되지 않습니다.
## 모범 사례
* 스캐폴딩된 역할에서 시작한 다음 점진적으로 권한을 제한하세요. 기본 설정은 광범위한 읽기 액세스를 부여하는데, 이는 프로덕션 환경에서 원하는 경우가 거의 없습니다.
* `objectPermissions`와 `fieldPermissions`를 함수에 실제로 필요한 정확한 객체와 필드로 교체하세요.
* `permissionFlagUniversalIdentifiers`는 플랫폼 수준 기능에 대한 액세스를 제어합니다. 최소한으로 유지하세요.
* 동작 예제를 참조하세요: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).