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: Configuration de l'application
description: Déclarez l'identité de votre application, son rôle par défaut, ses variables et ses métadonnées de la marketplace avec defineApplication.
icon: rocket
---
Chaque application doit avoir exactement un appel à `defineApplication`. Il déclare :
* **Identité** — identifiant universel, nom d'affichage, description.
* **Autorisations** — le rôle sous lequel s'exécutent ses fonctions logiques et ses composants front-end.
* **Variables** *(facultatif)* — paires clé–valeur exposées à votre code en tant que variables d'environnement.
* **Hooks de pré-installation / post-installation / désinstallation** *(facultatif)* — voir [Fonctions logiques](/l/fr/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,
},
},
});
```
Notes :
* Les champs `universalIdentifier` sont des identifiants déterministes que vous contrôlez. Générez-les une fois et conservez-les stables entre les synchronisations.
* `applicationVariables` deviennent des variables d'environnement pour vos fonctions et vos composants front-end. Dans les fonctions logiques (côté serveur), elles sont disponibles sous `process.env.VARIABLE_NAME`. Dans les composants front-end, utilisez `getApplicationVariable('VARIABLE_NAME')` depuis `twenty-sdk/front-component`. Les variables marquées avec `isSecret: true` sont uniquement injectées dans les fonctions logiques. Les composants front-end ne reçoivent que des variables non secrètes.
* Le rôle par défaut est détecté automatiquement à partir du fichier de rôle marqué avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) — vous n’avez pas besoin d’y faire référence depuis `defineApplication()`.
* Les fonctions de pré-installation, de post-installation et de désinstallation sont détectées automatiquement lors de la construction du manifeste — vous n'avez pas besoin de les référencer dans `defineApplication()`.
* Le passage explicite de `defaultRoleUniversalIdentifier` est toujours pris en charge pour des raisons de rétrocompatibilité, mais il est obsolète au profit de `defineApplicationRole()`.
* `serverVariables` sont des configurations et des secrets au niveau de l’instance (par exemple des clés d’API). Contrairement à `applicationVariables`, ils ne déclarent aucune valeur dans le manifeste — l’opérateur de l’espace de travail les renseigne dans les paramètres de l’application, et ils sont injectés dans les fonctions logiques uniquement une fois définis.
* Les deux types de variable acceptent `isDeprecated: true`. Utilisez-la pour mettre une variable hors service plutôt que de la supprimer : garder la clé déclarée préserve la valeur stockée (la supprimer détruit la valeur saisie par l’utilisateur), et la variable est toujours injectée, de sorte que votre code peut s’y rabattre — `process.env.NEW_API_KEY ?? process.env.API_KEY`. Une variable dépréciée disparaît des paramètres de l'application lorsqu'elle n'a plus de valeur et n'est jamais prise en compte dans la vérification de la configuration de l'application, donc `isDeprecated` l'emporte sur `isRequired`.
* Pour afficher une interface utilisateur de configuration personnalisée dans l’onglet **Settings** de l’application (à la place de la section de configuration des variables par défaut), déclarez un composant frontal avec [`defineSettingsFrontComponent()`](/l/fr/developers/extend/apps/layout/front-components#custom-settings-component) dans son propre fichier. Un seul est autorisé par application. Les sections gérées par le système (mise à niveau automatique, App URL, connexions) restent toujours visibles.
## Types de variables
`applicationVariables` et `serverVariables` acceptent tous deux un `type` optionnel (et, pour `SELECT` / `MULTI_SELECT`, une liste `options`). Types pris en charge : `TEXT` (par défaut), `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',
},
},
});
```
Le `type` affecte uniquement **la présentation et la validation** — il sélectionne le champ de saisie correspondant dans l’interface des paramètres de l’espace de travail (un bouton bascule, un champ numérique, une liste déroulante, un sélecteur de date, un éditeur JSON, …) et permet au build de valider votre configuration (par exemple, `SELECT` / `MULTI_SELECT` doivent déclarer des `options` non vides). Il ne change **pas** la façon dont la valeur atteint votre code.
Les valeurs sont **toujours injectées sous forme de chaînes de caractères** — cela est inhérent aux variables d’environnement (`process.env.*` est uniquement composé de chaînes). Lorsque votre fonction logique s’exécute, l’exécuteur sérialise chaque valeur selon son `type` déclaré lors de la construction de `process.env`, de sorte que le format de chaîne soit cohérent, quelle que soit la manière dont la valeur a été définie (valeur par défaut du manifeste, interface des paramètres ou version précédente) :
| Type | chaîne `process.env` |
| ------------------------------------- | ---------------------------------------- |
| `TEXT`, `SELECT`, `DATE`, `DATE_TIME` | la valeur brute (`"eu"`, `"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`, `NUMERIC` | chaîne décimale (`"10"`, `"2.5"`) |
| `MULTI_SELECT`, `ARRAY` | tableau JSON (`'["email","postcard"]'`) |
| `RAW_JSON`, `RICH_TEXT` | objet JSON (`'{"retries":3}'`) |
Analysez la chaîne pour la convertir dans le type attendu :
```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 }
```
Il en va de même pour les composants front qui lisent les valeurs via `getApplicationVariable('VARIABLE_NAME')` — la valeur renvoyée est une chaîne ; analysez-la selon vos besoins.
## Rôle de fonction par défaut
Le rôle déclaré avec [`defineApplicationRole()`](/l/fr/developers/extend/apps/config/roles) contrôle ce à quoi les fonctions logiques de l’application et les composants front peuvent accéder :
* Les jetons d'exécution injectés dans vos fonctions logiques sont dérivés de ce rôle. Un appel agissant en tant que personne est davantage limité par ce que cette personne peut faire, de sorte qu'il ne peut jamais dépasser l'un ou l'autre. Voir [Les accès qu'un appel utilise](/l/fr/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses).
* Le client d'API typé est limité aux autorisations accordées à ce rôle.
* Appliquez le principe du moindre privilège : déclarez uniquement les autorisations dont vos fonctions ont besoin.
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut à `src/roles/default-role.ts`. Voir [Rôles et autorisations](/l/fr/developers/extend/apps/config/roles) pour la référence complète.
## Métadonnées de la marketplace
Si vous prévoyez de [publier votre application](/l/fr/developers/extend/apps/operations/publishing), ces champs optionnels contrôlent la façon dont elle apparaît dans la marketplace :
| Champ | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `author` | Nom de l'auteur ou de l'entreprise |
| `category` | Catégorie de l'application pour le filtrage dans la marketplace |
| `logo` | Chemin vers le logo de votre application empaqueté dans `public/` (p. ex., `public/logo.png`) |
| `galleryImages` | Tableau de chemins d’images de galerie empaquetées dans `public/` (p. ex., `public/screenshot-1.png`) |
| `aboutDescription` | Description markdown plus longue pour l'onglet "À propos". S'il est omis, la marketplace utilise le `README.md` du package sur npm |
| `websiteUrl` | Lien vers votre site web |
| `termsUrl` | Lien vers les conditions d'utilisation |
| `emailSupport` | Adresse e-mail du support |
| `issueReportUrl` | Lien vers le système de suivi des problèmes |
<Note>
`logoUrl` et `screenshots` sont des alias obsolètes de `logo` et `galleryImages`. Les URL absolues externes (`http://` ou `https://`) ne sont pas prises en charge pour ces champs : elles sont ignorées avec un avertissement au moment de la compilation. Placez plutôt les images dans le dossier `public/` de votre application.
</Note>

View File

@@ -0,0 +1,188 @@
---
title: Hooks d'installation
description: Exécutez de la logique pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation — préremplissez des données, sauvegardez des enregistrements, validez la mise à niveau, nettoyez les ressources externes.
icon: wrench
---
Les hooks d'installation sont des fonctions logiques spéciales qui s'exécutent pendant le cycle de vie d'installation, de mise à niveau ou de désinstallation. Ils partagent le même environnement d'exécution que les [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) classiques, mais ils sont déclarés avec leurs propres fonctions de définition et ne relèvent pas du modèle de déclencheur habituel (HTTP, cron, événements de base de données). Les hooks d'installation reçoivent un `InstallPayload` (`{ previousVersion?: string; newVersion: string }` — `previousVersion` est `undefined` lors d'une nouvelle installation) ; le hook de désinstallation reçoit un `UninstallPayload` (`{ version?: string }` — la version supprimée).
Chaque application peut définir **au maximum un seul** hook de chaque type (pré-installation, post-installation, désinstallation). La génération du manifeste renvoie une erreur si plus d'un hook de n'importe quel type est détecté.
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## En un coup d'œil
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Exécutions | Avant la migration des métadonnées — le schéma et les données **précédents** sont toujours intacts | Après la migration et la génération du SDK — le **nouveau** schéma est en place |
| Exécution | Toujours synchrone ; bloque l'installation | Asynchrone par défaut (mis en file d'attente, 3 nouvelles tentatives) ; mode synchrone en option via `shouldRunSynchronously: true` |
| En cas d'échec | L'installation est **abandonnée** avant toute modification du schéma | Asynchrone : jusqu'à 3 réessais. Synchrone : l'appelant reçoit `POST_INSTALL_ERROR` (les modifications de schéma **ne** sont pas annulées) |
| Utilisation typique | Sauvegarder ou corriger les données qu'une migration ferait perdre ; refuser une mise à niveau risquée en levant une exception | Initialiser des données par défaut, configurer l'espace de travail, enregistrer des ressources externes |
**Règle empirique :** privilégiez post-install par défaut. Ne recourez à la pré-installation que lorsque la migration elle-même est destructive et que vous devez intercepter l'état précédent avant qu'il ne disparaisse.
| Vous souhaitez... | Utiliser |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Initialiser des données, configurer l'espace de travail, enregistrer des ressources externes | `post-install` |
| Travail de longue durée qui ne doit pas bloquer la réponse d'installation | `post-install` (mode asynchrone par défaut, avec nouvelles tentatives du worker) |
| Configuration rapide dont l'appelant dépend immédiatement après le retour de l'installation | `post-install` avec `shouldRunSynchronously: true` |
| Lire ou sauvegarder des données que la migration à venir ferait perdre | `pre-install` |
| Rejeter une mise à niveau qui corromprait des données existantes | `pre-install` (lancer une exception depuis le gestionnaire) |
| Réconciliation à chaque mise à niveau | N'importe quel hook avec `shouldRunOnVersionUpgrade: true` |
## Comportement partagé par les deux hooks
* La configuration est une configuration `defineLogicFunction` moins les paramètres de déclencheur, plus `shouldRunOnVersionUpgrade`.
* **Quand il s'exécute** : uniquement lors des nouvelles installations, par défaut. Définissez `shouldRunOnVersionUpgrade: true` pour qu'il s'exécute également lors des mises à niveau. Utilisez `previousVersion` / `newVersion` pour bifurquer selon le chemin de mise à niveau.
* **L'idempotence est importante** : le post-install asynchrone peut être relancé, et chaque hook est réexécuté lors des mises à niveau lorsque `shouldRunOnVersionUpgrade` est activé.
* L'environnement habituel des fonctions logiques (`APPLICATION_ID`, `APP_ACCESS_TOKEN`, `API_URL`) est injecté, ce qui vous permet d'appeler l'API Twenty avec le jeton de votre application.
* Le hook est rattaché automatiquement au manifeste de l'application au moment de la compilation (`preInstallLogicFunction` / `postInstallLogicFunction`) — rien à référencer dans [`defineApplication()`](/l/fr/developers/extend/apps/config/application).
* La valeur par défaut de `timeoutSeconds` est 300 pour permettre des tâches de configuration plus longues comme l'initialisation des données.
* **Non exécuté en mode dev** : `yarn twenty dev` ignore le flux d'installation et synchronise directement les fichiers, donc les hooks ne s'exécutent jamais dans ce cas. Déclenchez-les manuellement à la place :
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="S'exécute après l'application de la migration des métadonnées de l'espace de travail">
S'exécute une fois que votre application a terminé son installation : métadonnées synchronisées, client SDK généré, nouveau schéma interrogeable. Exemple — initialiser un enregistrement par défaut lors des nouvelles installations :
```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,
});
```
Le drapeau `shouldRunSynchronously` contrôle le modèle d'exécution :
* `false` *(par défaut)* — mis en file d'attente dans la file de messages (`retryLimit: 3`) et exécuté par un worker. La réponse d'installation est renvoyée dès que la tâche est mise en file d'attente. **À utiliser pour les travaux de longue durée** — initialisation de grands ensembles de données, API tierces lentes.
* `true` — exécuté en ligne pendant le flux d'installation. La requête d'installation est bloquée jusqu'à ce que le gestionnaire ait terminé ; une erreur levée apparaît comme `POST_INSTALL_ERROR` pour l'appelant (aucune nouvelle tentative). **À utiliser pour les travaux rapides qui doivent être terminés avant la réponse.** La migration a déjà été appliquée à ce stade, donc un échec n'annule pas les modifications du schéma — il ne fait que remonter l'erreur.
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="S'exécute avant l'application de la migration des métadonnées de l'espace de travail">
S'exécute avant la migration des métadonnées, sur le schéma **précédent** — l'endroit idéal pour sauvegarder des données qu'une migration ferait perdre, ou pour refuser une mise à niveau risquée. Avant l'exécution, le serveur effectue une « synchronisation réduite » purement additive qui enregistre uniquement la fonction de pré-installation de la nouvelle version ; tout le reste — les objets, champs et données de la version précédente — reste intact lorsque votre gestionnaire s'exécute.
La pré-installation est toujours **synchrone** et bloque l'installation. Si le gestionnaire lève une exception, l'installation est abandonnée avant toute modification du schéma — l'espace de travail reste sur la version précédente dans un état cohérent. C'est intentionnel : la pré-installation est votre dernière chance de refuser une mise à niveau risquée.
Exemple — copier les valeurs d'un champ hérité avant que la migration ne le supprime :
```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>
## Hook de désinstallation
`defineUninstallLogicFunction` déclare un hook qui s'exécute lorsqu'un utilisateur désinstalle votre application. Il s'exécute **avant** que les métadonnées, les données et le code de l'application ne soient supprimés — une fois que la migration de suppression est exécutée, il ne reste plus rien à exécuter — de sorte que votre gestionnaire peut encore interroger les objets et enregistrements de l'application. Utilisez-le pour nettoyer les ressources externes : déprovisionner les ressources d'API, supprimer les bots restants, révoquer les webhooks.
Notes :
* Le hook fonctionne selon le principe du "best effort" : il s'exécute de manière synchrone, mais un échec est consigné dans les journaux et **ne bloque jamais la désinstallation** — le nettoyage ne doit pas rendre une application impossible à supprimer.
* Il reçoit `UninstallPayload` (`{ version?: string }` — la version supprimée).
* Il **ne** s'exécute **pas** lorsqu'une nouvelle installation ayant échoué est annulée — l'application n'a jamais terminé son installation.
* Le hook ne peut pas s'exécuter après la suppression de l'application, donc le nettoyage externe qui dépend des données de l'application (par exemple des identifiants de bots stockés dans des enregistrements) doit être effectué ici, et non dans une tâche planifiée externe.
* Comme les hooks d'installation, il **n'est pas exécuté en mode dev** — déclenchez-le manuellement à la place :
```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: Vue d'ensemble
description: Configurer l'application elle-même — son identité, ses autorisations par défaut et ce qui s'exécute au moment de l'installation.
icon: screwdriver-wrench
---
La **couche de configuration** d'une application Twenty est ce qui décrit l'application *à la plateforme* — son identité, les autorisations qu'elle détient et le code qui s'exécute lors de l'installation ou de la mise à niveau. Ces déclarations n'ajoutent pas de nouvelles structures de données ni de comportement à l'exécution ; elles indiquent à Twenty *qui est l'application* et *comment la configurer*.
```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
└──────────────────────────────────┘
```
## Dans cette section
<CardGroup cols={2}>
<Card title="Configuration de l'application" icon="rocket" href="/l/fr/developers/extend/apps/config/application">
`defineApplication` — identité, rôle par défaut, variables, métadonnées de la marketplace.
</Card>
<Card title="Rôles et autorisations" icon="shield-halved" href="/l/fr/developers/extend/apps/config/roles">
`defineRole` — déclarez ce que les fonctions logiques de votre application peuvent lire et écrire.
</Card>
<Card title="Hooks d'installation" icon="wrench" href="/l/fr/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`, `definePostInstallLogicFunction` et `defineUninstallLogicFunction` — sauvegarder les données, préremplir les valeurs par défaut, valider les mises à niveau, nettoyer lors de la suppression.
</Card>
</CardGroup>
## Comment les éléments s'articulent
* L'**application** est le point d'entrée. Chaque application comporte exactement un appel à `defineApplication()` qui pointe vers un **Rôle** par défaut.
* Le **Rôle** contrôle ce que les fonctions logiques et les composants frontaux de l'application peuvent lire et écrire. Suivez le principe du moindre privilège : accordez uniquement les autorisations dont votre code a réellement besoin.
* Les **hooks d'installation** s'exécutent lors de l'installation ou de la mise à niveau — la pré-installation avant la migration des métadonnées (ce qui permet de refuser une mise à niveau risquée), la post-installation après la migration (ce qui permet de préremplir les données par défaut selon le nouveau schéma). Le hook de désinstallation s’exécute juste avant que l’application ne soit supprimée, ce qui lui permet de nettoyer les ressources externes tant que les données de l’application sont encore lisibles.
<Note>
Les hooks d'installation partagent l'environnement d'exécution des [fonctions logiques](/l/fr/developers/extend/apps/logic/logic-functions) — même signature de handler, mêmes variables d'environnement, même client d'API typé — mais ils sont déclarés avec leurs propres fonctions `define` et vivent en dehors du modèle de déclencheurs habituel (HTTP, cron, événements de base de données).
</Note>

View File

@@ -0,0 +1,67 @@
---
title: Ressources publiques
description: Distribuez des fichiers statiques — images, icônes, polices — avec votre application via le dossier public/.
icon: folder-open
---
Le dossier `public/` à la racine de votre application contient des fichiers statiques — images, icônes, polices ou toute autre ressource dont votre application a besoin à l'exécution. Ces fichiers sont automatiquement inclus dans les builds, synchronisés pendant le mode de développement et téléversés vers le serveur.
Les fichiers placés dans `public/` sont :
* **Accessibles publiquement** — une fois synchronisés avec le serveur, les fichiers sont servis à une URL publique. Aucune authentification n'est nécessaire pour y accéder.
* **Disponibles dans les composants frontaux** — utilisez les URL des ressources pour afficher des images, des icônes ou tout média dans vos composants React.
* **Disponibles dans les fonctions logiques** — référencez des URL de ressources dans des e-mails, des réponses d'API ou toute logique côté serveur.
* **Utilisés pour les métadonnées de marketplace** — les champs `logo` et `galleryImages` dans `defineApplication()` référencent des fichiers de ce dossier (p. ex., `public/logo.png`). Celles-ci sont affichées dans la marketplace lorsque votre application est publiée. Les URL absolues externes sont ignorées pour ces champs — intégrez plutôt les images dans `public/`. Chaque image ne doit pas dépasser 10 Mo.
* **Synchronisés automatiquement en mode dev** — lorsque vous ajoutez, mettez à jour ou supprimez un fichier dans `public/`, il est automatiquement synchronisé avec le serveur. Aucun redémarrage nécessaire.
* **Inclus dans les builds** — `yarn twenty dev:build` regroupe toutes les ressources publiques dans la sortie de distribution.
## Accéder aux ressources publiques avec `getPublicAssetUrl`
Utilisez l'utilitaire `getPublicAssetUrl` de `twenty-sdk` pour obtenir l'URL complète d'un fichier dans votre répertoire `public/`. Cela fonctionne à la fois dans les **fonctions logiques** et les **composants frontaux**.
**Dans une fonction logique :**
```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,
});
```
**Dans un composant frontal :**
```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,
});
```
L'argument `path` est relatif au dossier `public/` de votre application. Les deux `getPublicAssetUrl('logo.png')` et `getPublicAssetUrl('public/logo.png')` aboutissent à la même URL — le préfixe `public/` est supprimé automatiquement s'il est présent.

View File

@@ -0,0 +1,201 @@
---
title: Rôles et autorisations
description: Déclarez quels objets et champs les fonctions de logique et les composants frontaux de votre application peuvent lire et écrire.
icon: shield-halved
---
Un **rôle** est un ensemble d’autorisations : quels objets une application peut lire ou écrire, quels champs elle peut voir et quelles fonctionnalités de plateforme elle peut utiliser. Les fonctions de logique et les composants frontaux de chaque application héritent des autorisations du rôle marqué avec `defineApplicationRole()` (voir [Le rôle de fonction par défaut](#the-default-function-role) ci-dessous).
```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],
});
```
## Sécurité au niveau des lignes
Les autorisations d’objet et de champ déterminent à *quels objets et champs* un rôle peut accéder. Les **prédicats d’autorisation au niveau des lignes** vont plus loin et déterminent *quels enregistrements* un rôle peut voir et sur lesquels il peut agir — par exemple, un rôle en libre-service où chaque utilisateur externe ne voit que ses propres enregistrements.
Déclarez les prédicats avec `rowLevelPermissionPredicates` sur le rôle. Comme le reste du manifeste, chaque prédicat porte son propre `universalIdentifier` et fait référence à un objet et à un champ par leur `universalIdentifier`, un `operand` et (facultativement) un champ workspaceMember dont la valeur est injectée au moment de la requête — vous pouvez ainsi exprimer : "la relation de propriétaire de l’enregistrement **est** le membre de l’espace de travail actuel" :
```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,
},
],
});
```
Comme les prédicats sont livrés dans le manifeste, ils sont créés, mis à jour et supprimés en même temps que le rôle à chaque installation et mise à niveau — il n’y a aucune étape distincte après l’installation à garder synchronisée.
### Combiner des prédicats avec des groupes
Par défaut, les prédicats d’un rôle sont combinés avec `AND`. Pour en combiner certains avec `OR` (ou pour imbriquer la logique), déclarez une entrée `rowLevelPermissionPredicateGroups` et faites pointer chaque prédicat vers celle-ci via `predicateGroupUniversalIdentifier`. Ce rôle permet à un partenaire de voir une opportunité qu’il **possède** ou pour laquelle il est le **point de contact** :
```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',
},
],
});
```
Notes :
* Donnez à chaque prédicat et groupe un `universalIdentifier` stable (n’importe quel uuid) — il sert de clé à l’entité lors des mises à niveau, et les prédicats référencent les groupes via celui-ci.
* Les prédicats peuvent référencer des objets et des champs appartenant à votre application ou aux objets standard de Twenty.
* La sécurité au niveau des lignes est appliquée pour les espaces de travail sur les offres qui l’incluent ; les prédicats sont tout de même synchronisés sur les autres offres, ils ne sont simplement pas appliqués.
## Le rôle de fonction par défaut
Lorsque vous générez une nouvelle application, la CLI crée un fichier de rôle par défaut déclaré avec `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()` est un léger wrapper autour de `defineRole()` qui marque le rôle utilisé comme rôle par défaut de votre application au moment de l’installation. La validation est identique à `defineRole`, mais le pipeline de build connecte automatiquement son `universalIdentifier` au champ `defaultRoleUniversalIdentifier` du manifeste de l’application — vous n’avez donc pas besoin de le référencer vous-même depuis [`defineApplication`](/l/fr/developers/extend/apps/config/application).
Notes :
* Exactement **un** `defineApplicationRole(...)` est autorisé par application — la génération du manifeste échouera si elle en trouve plus d’un.
* Utilisez `defineRole()` (et non `defineApplicationRole()`) pour tous les rôles **supplémentaires** fournis par votre application.
* Le fait de définir explicitement `defaultRoleUniversalIdentifier` dans `defineApplication()` est toujours pris en charge pour assurer la rétrocompatibilité, mais c’est obsolète et remplacé par `defineApplicationRole()`.
## Meilleures pratiques
* Partez du rôle généré par le gabarit, puis restreignez-le progressivement — par défaut, il accorde un large accès en lecture, ce qui correspond rarement à ce que vous voulez en production.
* Remplacez `objectPermissions` et `fieldPermissions` par les objets et champs dont vos fonctions ont réellement besoin.
* `permissionFlagUniversalIdentifiers` contrôlent l’accès aux capacités au niveau de la plateforme. Maintenez-les au strict minimum.
* Voir un exemple fonctionnel : [`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).