Compare commits

..
Author SHA1 Message Date
sonarly-bot f7fc629f56 fix(messaging): cap Gmail message import concurrency
https://sonarly.com/issue/39364?type=bug

Message import jobs for Gmail can hit per-user API quota and fail the channel after 5 retries. This stops mailbox sync for affected channels until a relaunch/reset path runs.

Fix: Implemented the production fix in the Gmail import layer (right layer for the quota burst):

1) Capped Gmail message fetch concurrency
- In `GmailGetMessagesService`, replaced unbounded `Promise.all(messageIds.map(...users.messages.get...))` with `p-limit` capped execution.
- Added explicit constants:
  - `GMAIL_MESSAGES_GET_CONCURRENCY = 10`
  - `GMAIL_THREADS_GET_CONCURRENCY = 10`
- Applied the same cap to thread expansion (`users.threads.get`) to prevent a second unbounded fan-out path from hitting the same per-user quota envelope.

2) Added test coverage for the fix
- New spec validates that `getMessages()` never exceeds concurrency 10 while processing 30 message IDs.

Pre-fix checklist summary:
- Best-engineer/user-protection: yes, directly prevents quota burst and channel failure loop.
- Right layer: yes (Gmail driver import fan-out).
- Intentional-revert check (`git blame`): no intentional behavior reverted; unbounded fan-out came from original implementation.
- Blast radius check: limited to Gmail import fetch path and local caller behavior.
- No autogenerated files edited.
- No partial/shared-interface unsafe refactor.

Authored by Sonarly by autonomous analysis (run 44831).
2026-05-21 10:35:20 +00:00
16ad50ea46 Set website default port to 3002 (#20795)
## Summary

- Set `twenty-website`'s `dev` script to run Next.js on port `3002` by
default.
- Set `twenty-website`'s `start` script to use the same default port.

## Why

`twenty-website` previously inherited Next.js' default port `3000`,
which is also Twenty's backend/server default. The main Twenty frontend
already defaults to `3001`, so using `3002` for the website avoids local
port collisions when running the website next to the app server and
frontend.

This keeps the local convention sequential:

- `3000`: Twenty backend/server
- `3001`: Twenty frontend app
- `3002`: Twenty website

## Validation

- Parsed `packages/twenty-website/package.json` successfully with Node.
- Ran `git diff --check` for the changed package file.
- Verified Next.js supports the `--port` option for `next dev`.

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-05-21 09:34:56 +00:00
martmullandGitHub 74fa26f9e5 Fix must wait 3 days to create app in twenty-apps (#20794)
as title
2026-05-21 09:29:47 +00:00
7ca2efeb96 i18n - translations (#20796)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-21 11:42:46 +02:00
WeikoandGitHub 224376233b fix(front): focus new Field widget and open side panel on add (#20777)
## Context
In the record-page edit mode, the "Add widget" section has three menu
items:
`Fields group`, `Field`, and `More widgets`. `Fields group` creates the
widget, focuses it, and opens its settings side panel. `Field` only
added the widget to the draft — no focus, no panel — which left the user
without visible confirmation or a way to immediately edit the new
widget.

## Change
Updated `useCreateRecordPageFieldWidget` to mirror
`useCreateRecordPageFieldsWidget`:
After appending the new widget to the draft, it now sets
`pageLayoutEditingWidgetIdComponentState` to the new widget's id and
navigates the side panel to `SidePanelPages.RecordPageFieldSettings`
(with `focusTitleInput: true`, `resetNavigationStack: true`).
This matches the behavior of clicking an existing field widget in
`useOpenWidgetSettingsInSidePanel` (`WidgetType.FIELD` branch).
2026-05-21 09:22:21 +00:00
Paul RastoinandGitHub a3c92311e3 Application file storage service (#20793)
# Introduction
Fix unsafe resource path join with expected prefix at file storage
directly
Add early paths transversal detections in metadata validators
2026-05-21 09:21:44 +00:00
1ed347bc17 chore: sync AI model catalog from models.dev (#20791)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-05-21 09:14:02 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b792f7654b chore(deps): bump tinyglobby from 0.2.15 to 0.2.16 (#20788)
Bumps [tinyglobby](https://github.com/SuperchupuDev/tinyglobby) from
0.2.15 to 0.2.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/SuperchupuDev/tinyglobby/releases">tinyglobby's
releases</a>.</em></p>
<blockquote>
<h2>0.2.16</h2>
<h2>Fixed</h2>
<ul>
<li>Upgraded <code>picomatch</code> to 4.0.4, mitigating any potential
exposure to <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-c2c7-rcm5-vvqj">CVE-2026-33671</a>
and <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-3v7f-55p6-f55p">CVE-2026-33672</a></li>
</ul>
<h2>Changed</h2>
<ul>
<li>Overhauled and optimized most internals by <a
href="https://github.com/Torathion"><code>@​Torathion</code></a></li>
<li>Ignore patterns are no longer compiled twice by <a
href="https://github.com/webpro"><code>@​webpro</code></a></li>
</ul>
<p>Consider <a
href="https://github.com/sponsors/SuperchupuDev">sponsoring</a> if you'd
like to support the development of this project and the goal of reaching
a lighter and faster ecosystem</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/SuperchupuDev/tinyglobby/blob/main/CHANGELOG.md">tinyglobby's
changelog</a>.</em></p>
<blockquote>
<h3><a
href="https://github.com/SuperchupuDev/tinyglobby/compare/0.2.15...0.2.16">0.2.16</a></h3>
<h4>Fixed</h4>
<ul>
<li>Upgraded <code>picomatch</code> to 4.0.4, mitigating any potential
exposure to
<a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-c2c7-rcm5-vvqj">CVE-2026-33671</a>
and <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-3v7f-55p6-f55p">CVE-2026-33672</a></li>
</ul>
<h4>Changed</h4>
<ul>
<li>Overhauled and optimized most internals by <a
href="https://github.com/Torathion">Torathion</a></li>
<li>Ignore patterns are no longer compiled twice by <a
href="https://github.com/webpro">webpro</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/577920259c91f5603fab3dbfa599a83bbb14a27a"><code>5779202</code></a>
release 0.2.16</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/071954f97f4d2b573ecd92aecb48220867b3c776"><code>071954f</code></a>
bump deps once more</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/e541dde000158e69b44e8b5a03789d136d88ae0d"><code>e541dde</code></a>
do not import the whole <code>fs</code> module</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/2381b766d3447e078b3112e533ada07621f60526"><code>2381b76</code></a>
fix root being too broad</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/0addeb9a78cab5fd93ac6dad217f75930cfbfda2"><code>0addeb9</code></a>
chore(deps): update all non-major dependencies (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/191">#191</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/91ac26cc3bda60378d188565ce4f05e884aa4f31"><code>91ac26c</code></a>
chore(deps): update pnpm/action-setup action to v5 (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/192">#192</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/c50558e944bf71dbfeb392f8693fada87f520a70"><code>c50558e</code></a>
upgrade picomatch (and everything else)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/618517544e95f8167f1722902558dd78dcd34fe6"><code>6185175</code></a>
chore(deps): update dependency picomatch to v4.0.4 [security] (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/193">#193</a>)</li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/49c2b9356c4977f1f531de651f90b420a06c64f6"><code>49c2b93</code></a>
enable pnpm <code>trustPolicy</code></li>
<li><a
href="https://github.com/SuperchupuDev/tinyglobby/commit/bc825c476a5a429e58e06d11364cd68707b4cdd1"><code>bc825c4</code></a>
chore(deps): update all non-major dependencies (<a
href="https://redirect.github.com/SuperchupuDev/tinyglobby/issues/181">#181</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/SuperchupuDev/tinyglobby/compare/0.2.15...0.2.16">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tinyglobby&package-manager=npm_and_yarn&previous-version=0.2.15&new-version=0.2.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:47:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
42ad3cbd1a chore(deps): bump linkify-react from 4.3.2 to 4.3.3 (#20789)
Bumps
[linkify-react](https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkify-react)
from 4.3.2 to 4.3.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/nfrasser/linkifyjs/releases">linkify-react's
releases</a>.</em></p>
<blockquote>
<h2>v4.3.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix parsing bugs with some special encoded URLs</li>
<li>Parsed emails should not include port numbers</li>
<li>Exact version requirement for interfaces and plugins to avoid
incompatibility issues with older versions of linkify core</li>
<li>Support for jQuery 4</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/nfrasser/linkifyjs/compare/v4.3.2...v4.3.3">https://github.com/nfrasser/linkifyjs/compare/v4.3.2...v4.3.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nfrasser/linkifyjs/blob/main/CHANGELOG.md">linkify-react's
changelog</a>.</em></p>
<blockquote>
<h2>v4.3.3</h2>
<ul>
<li>Fix parsing bugs with some special encoded URLs</li>
<li>Parsed emails should not include port numbers</li>
<li>Exact version requirement for interfaces and plugins to avoid
incompatibility issues with older versions of linkify core</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nfrasser/linkifyjs/commit/7fffcc6b48f7dbf8e98fca493e1c997a659fe651"><code>7fffcc6</code></a>
v4.3.3</li>
<li><a
href="https://github.com/nfrasser/linkifyjs/commit/2cb8352d78c7449cd8c7ee489a647d3422640a25"><code>2cb8352</code></a>
Update dependencies (<a
href="https://github.com/nfrasser/linkifyjs/tree/HEAD/packages/linkify-react/issues/529">#529</a>)</li>
<li>See full diff in <a
href="https://github.com/nfrasser/linkifyjs/commits/v4.3.3/packages/linkify-react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for linkify-react since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=linkify-react&package-manager=npm_and_yarn&previous-version=4.3.2&new-version=4.3.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:47:35 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e1378dd4cf chore(deps): bump @azure/msal-node from 3.8.4 to 3.8.10 (#20787)
Bumps
[@azure/msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js)
from 3.8.4 to 3.8.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/releases">@​azure/msal-node's
releases</a>.</em></p>
<blockquote>
<h2><code>@​azure/msal-node</code> v3.8.10</h2>
<h2>3.8.10</h2>
<p>Wed, 18 Mar 2026 20:48:29 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.17.0 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.9</h2>
<h2>3.8.9</h2>
<p>Fri, 13 Mar 2026 04:32:07 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.16.1 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.8</h2>
<h2>3.8.8</h2>
<p>Mon, 23 Feb 2026 16:28:24 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.15.0 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
<h2><code>@​azure/msal-node</code> v3.8.7</h2>
<h2>3.8.7</h2>
<p>Tue, 10 Feb 2026 22:19:29 GMT</p>
<h3>Patches</h3>
<ul>
<li>Bump <code>@​azure/msal-common</code> to v15.14.2 (beachball)</li>
<li>Bump eslint-config-msal to v0.0.0 (beachball)</li>
<li>Bump rollup-msal to v0.0.0 (beachball)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/74c792ec34cd83a3470c4d878b403af0fa2884f0"><code>74c792e</code></a>
[v4] Add missing client capabilities in platform broker flows (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8429">#8429</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/e096fc85e22fcb941646b3f5d9f264b90dffd0ad"><code>e096fc8</code></a>
[v4] Post-release PR (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8425">#8425</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/23cd31c1fbcfa569dd9135830f5168ec71678a19"><code>23cd31c</code></a>
[v4] Add support for client data telemetry with CLI_DATA parameter (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8378">#8378</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/eb893e565a4f7de5b9c957b37bb4f975302f1713"><code>eb893e5</code></a>
Track online/offline status change (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8410">#8410</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/d221f4e6dbc439e771a90f1648c8f5db09b4d88b"><code>d221f4e</code></a>
[v4] Respect claims of the brokered application (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8409">#8409</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/73df97e5c505010f6b9681749df28a328e4e3894"><code>73df97e</code></a>
Common partial release resolution (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8397">#8397</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/ee5e7abd5d9a6cfb6ada9b3ac36e15a81f7ba3a8"><code>ee5e7ab</code></a>
monitor_window_timeout telemetry (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8380">#8380</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/2be7888051cc12e7447487e84c826159dad3ed42"><code>2be7888</code></a>
Rename dev to v4-lts changes (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8364">#8364</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/4486b7e4bfedc414f7f5f1a7786be30484318206"><code>4486b7e</code></a>
Fix JSON object conversion in PlatformDOMRequest v4 (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8350">#8350</a>)</li>
<li><a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/3e5d58b71e232b5978edcf0bd4c05f5948b8a747"><code>3e5d58b</code></a>
Post-release PR (<a
href="https://redirect.github.com/AzureAD/microsoft-authentication-library-for-js/issues/8354">#8354</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/AzureAD/microsoft-authentication-library-for-js/compare/msal-node-v3.8.4...msal-node-v3.8.10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@azure/msal-node&package-manager=npm_and_yarn&previous-version=3.8.4&new-version=3.8.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:06:59 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5e1b23a28b chore(deps): bump @recallai/desktop-sdk from 2.0.8 to 2.0.15 (#20785)
Bumps @recallai/desktop-sdk from 2.0.8 to 2.0.15.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@recallai/desktop-sdk&package-manager=npm_and_yarn&previous-version=2.0.8&new-version=2.0.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 05:04:13 +00:00
7aef406696 i18n - translations (#20782)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-20 21:33:20 +02:00
923c3beead fix(auth): clarify error when joining a non-active workspace (#20769)
## Summary

When an existing user accepts an invite into a workspace whose
`activationStatus` is not `ACTIVE` (e.g. `SUSPENDED`, `INACTIVE`,
`PENDING_CREATION`), the throw in
`throwIfWorkspaceIsNotReadyForSignInUp` returns:

> User is not part of the workspace

The message describes the symptom (they aren't a member yet) instead of
the cause (the workspace can't accept new members), which makes invitees
assume their invite is broken when the real issue is the target
workspace's state.

The sibling branch a few lines above — for brand-new users hitting the
same non-ACTIVE workspace — already returns `"Workspace is not ready to
welcome new members"`. This PR reuses the same message in the
existing-user branch so both paths give a consistent, accurate
explanation.

Single file, two string literals.

## Test plan

- [ ] Sign in via Google with an existing Twenty account, accepting an
invite to a `SUSPENDED` workspace → confirm the new message is shown
instead of "User is not part of the workspace".
- [ ] Confirm the happy path (sign-in to an `ACTIVE` workspace via
invite) is unchanged — early-return on `ACTIVE` is untouched.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-20 19:16:11 +00:00
3095 changed files with 49153 additions and 60119 deletions
-26
View File
@@ -1,26 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/.next/**",
"**/coverage/**",
"**/generated/**",
"**/generated-admin/**",
"**/generated-metadata/**",
"**/.cache/**",
"**/node_modules/**",
"**/*.min.js",
"**/*.snap",
"**/*.md",
"**/*.mdx",
"**/seed-project/**/*.mjs",
"packages/twenty-zapier/build/**"
]
}
+4
View File
@@ -10,4 +10,8 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
npmPreapprovedPackages:
- "twenty-sdk"
- "twenty-client-sdk"
yarnPath: .yarn/releases/yarn-4.13.0.cjs
+13 -9
View File
@@ -44,12 +44,12 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
},
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
@@ -59,12 +59,12 @@
"cache": false,
"dependsOn": ["twenty-oxlint-rules:build"],
"options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},
@@ -73,14 +73,18 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxfmt --check {args.files} {args.write}",
"files": ".",
"write": ""
"command": "prettier {args.files} --check --cache {args.cache} --cache-location {args.cacheLocation} --write {args.write} --cache-strategy {args.cacheStrategy}",
"cache": true,
"cacheLocation": "../../.cache/prettier/{projectRoot}",
"cacheStrategy": "metadata",
"write": false
},
"configurations": {
"ci": {},
"ci": {
"cacheStrategy": "content"
},
"fix": {
"command": "npx oxfmt {args.files}"
"write": true
}
},
"dependsOn": ["^build"]
-1
View File
@@ -13,7 +13,6 @@
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"nx": "22.5.4",
"oxfmt": "0.50.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1"
},
-17
View File
@@ -1,17 +0,0 @@
{
"$schema": "../../node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"dist/**",
"build/**",
".next/**",
"coverage/**",
"generated/**",
".cache/**",
"node_modules/**",
],
}
+14 -26
View File
@@ -4,10 +4,7 @@
"ignorePatterns": ["node_modules", ".next", "storybook-static"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
@@ -16,31 +13,22 @@
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-object-type": ["error", {
"allowInterfaces": "with-single-extends"
}],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
"typescript/no-unused-vars": ["warn", {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}]
}
}
-8
View File
@@ -3,21 +3,18 @@
## What Was Migrated
### Documentation Files
- **69 MDX files** copied from twenty-website to twenty-docs
- **45 User Guide articles**
- **22 Developer documentation articles**
- **2 Getting Started guides** (existing)
### Images & Assets
- **81 images** copied to `public/images/`
- User guide screenshots
- Developer documentation images
- Logo and branding assets
### Navigation Structure
- Complete `mint.json` configuration with tabs and nested navigation
- User Guide tab with 11 sections
- Developers tab with 6 sections
@@ -25,15 +22,12 @@
## Components Converted
### Custom Components → Mintlify Equivalents
- `<ArticleWarning>``<Warning>`
- `<ArticleLink href="...">text</ArticleLink>``[text](...)`
- `<ArticleEditContent>` → Removed (not needed)
### Still Need Manual Review
Some components may need additional conversion:
- `<ArticleTabs>` - Mintlify uses `<Tabs>` component
- Embedded iframes/videos - May need adjustment
- Custom styled elements - Review for Mintlify compatibility
@@ -69,7 +63,6 @@ packages/twenty-docs/
## Testing
Start the local Mintlify dev server:
```bash
npx nx run twenty-docs:dev
```
@@ -79,7 +72,6 @@ Open http://localhost:3000 to preview all migrated documentation.
## Deployment
To deploy to Mintlify:
1. Push changes to GitHub
2. Connect the repo in Mintlify dashboard
3. Set subdirectory to `packages/twenty-docs`
-2
View File
@@ -9,7 +9,6 @@ Visit the documentation at [docs.twenty.com](https://docs.twenty.com)
## 📚 Content
This repository contains:
- **User Guide** (46 pages) - Complete guide for Twenty users
- **Developers** (24 pages) - Technical documentation for developers
- **Twenty UI** (25 pages) - UI component library documentation
@@ -58,7 +57,6 @@ Your content here...
1. Place images in the `/images/` directory
2. Reference them in MDX: `![Alt text](/images/your-image.png)`
3. Or use Mintlify Frame component:
```mdx
<Frame>
<img src="/images/your-image.png" alt="Description" />
+6 -6
View File
@@ -12,8 +12,8 @@
opacity: 0.85 !important;
}
:is(.dark, [data-theme='dark']) #topbar-cta-button a,
:is(.dark, [data-theme='dark']) #topbar-cta-button a span {
:is(.dark, [data-theme="dark"]) #topbar-cta-button a,
:is(.dark, [data-theme="dark"]) #topbar-cta-button a span {
background-color: #ffffff !important;
color: #141414 !important;
}
@@ -35,14 +35,14 @@
* We use :has() to scope the rule to only the sidebar group that contains
* the developers/introduction link, so other tabs are unaffected.
*/
div:has(> .sidebar-group a[href='/developers/introduction']),
div:has(> .sidebar-group a[href='/user-guide/introduction']) {
div:has(> .sidebar-group a[href="/developers/introduction"]),
div:has(> .sidebar-group a[href="/user-guide/introduction"]) {
display: none;
}
/* Remove the top margin on the group that follows the hidden overview group,
so it sits flush at the top of the sidebar without a gap. */
div:has(> .sidebar-group a[href='/developers/introduction']) + div,
div:has(> .sidebar-group a[href='/user-guide/introduction']) + div {
div:has(> .sidebar-group a[href="/developers/introduction"]) + div,
div:has(> .sidebar-group a[href="/user-guide/introduction"]) + div {
margin-top: 0 !important;
}
@@ -2,6 +2,7 @@
title: Best Practices
---
This document outlines the best practices you should follow when working on the backend.
## Follow a modular approach
@@ -20,3 +21,4 @@ You should also expose services that you want to use in other modules. Exposing
When you declare a variable as `any`, TypeScript's type checker doesn't perform any type checking, making it possible to assign any type of values to the variable. TypeScript uses type inference to determine the type of variable based on the value. By declaring it as `any`, TypeScript can no longer infer the type. This makes it hard to catch type-related errors during development, leading to runtime errors and makes the code less maintainable, less reliable, and harder to understand for others.
This is why everything should have a type. So if you create a new object with a first name and last name, you should create an interface or type that contains a first name and last name that defines the shape of the object you are manipulating.
@@ -2,22 +2,21 @@
title: Custom Objects
---
Objects are structures that allow you to store data (records, attributes, and values) specific to an organization. Twenty provides both standard and custom objects.
Standard objects are in-built objects with a set of attributes available for all users. Examples of standard objects in Twenty include Company and Person. Standard objects have standard fields that are also available for all Twenty users, like Company.displayName.
Custom objects are objects that you can create to store information that is unique to your organization. They are not built-in; members of your workspace can create and customize custom objects to hold information that standard objects aren't suitable for.
## High-level schema
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/custom-object-schema.png"
alt="High level schema"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="High level schema" />
</div>
<br />
<br/>
## How it works
@@ -28,21 +27,15 @@ Custom objects come from metadata tables that determine the shape, name, and typ
- **Field**: Outlines an Object's fields and connects to the Object.
To add a custom object, the workspaceMember will query the /metadata API. This updates the metadata accordingly and computes a GraphQL schema based on the metadata, storing it in a GQL cache for later use.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/add-custom-objects.jpeg"
alt="Query the /metadata API to add custom objects"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Query the /metadata API to add custom objects" />
</div>
<br />
<br/>
To fetch data, the process involves making queries through the /graphql endpoint and passing them through the Query Resolver.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/custom-object-schema.png"
alt="Query the /graphql endpoint to fetch data"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Query the /graphql endpoint to fetch data" />
</div>
@@ -2,6 +2,7 @@
title: Feature Flags
---
Feature flags are used to hide experimental features. For Twenty, they are set on workspace level and not on a user level.
## Adding a new feature flag
@@ -23,6 +24,8 @@ enum FeatureFlagKeys {
}
```
To apply a feature flag on a **backend** feature use:
```ts
@@ -37,10 +40,12 @@ To apply a feature flag on a **frontend** feature use:
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
```
## Configure feature flags for the deployment
Change the corresponding record in the Table `core.featureFlag`:
| id | key | workspaceId | value |
| ------ | ------------------------ | ----------- | ------ |
| Random | `IS_FEATURENAME_ENABLED` | WorkspaceID | `true` |
| id | key | workspaceId | value |
|----------|--------------------------|---------------|--------|
| Random | `IS_FEATURENAME_ENABLED` | WorkspaceID | `true` |
@@ -3,6 +3,7 @@ title: Folder Architecture
info: A detailed look into our server folder architecture
---
The backend directory structure is as follows:
```
@@ -83,6 +84,7 @@ workspace
└───workspace.factory.ts
```
The root of the workspace directory includes the `workspace.factory.ts`, a file containing the `createGraphQLSchema` function. This function generates workspace-specific schema by using the metadata to tailor a schema for individual workspaces. By separating the schema and resolver construction, we use the `makeExecutableSchema` function, which combines these discrete elements.
This strategy is not just about organization, but also helps with optimization, such as caching generated type definitions to enhance performance and scalability.
@@ -94,7 +96,6 @@ Generates the GraphQL schema, and includes:
#### Factories:
Specialised constructors to generate GraphQL-related constructs.
- The type.factory translates field metadata into GraphQL types using `TypeMapperService`.
- The type-definition.factory creates GraphQL input or output objects derived from `objectMetadata`.
@@ -123,3 +124,4 @@ Each factory in this directory is responsible for producing a distinct resolver
### Workspace Query Runner
Runs the generated queries on the database and parses the result.
@@ -2,6 +2,7 @@
title: Message Queue
---
Queues facilitate async operations to be performed. They can be used for performing background tasks such as sending a welcome email on register.
Each use case will have its own queue class extended from `MessageQueueServiceBase`.
@@ -15,12 +16,9 @@ Currently, we only support `bull-mq`[bull-mq](https://bullmq.io/) as the queue d
4. Add worker class with token based injection just like producer.
### Example usage
```ts
class Resolver {
constructor(
@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService,
) {}
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
async onSomeAction() {
//business logic
@@ -30,9 +28,7 @@ class Resolver {
//async worker
class CustomWorker {
constructor(
@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService,
) {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
this.initWorker();
}
@@ -43,3 +39,5 @@ class CustomWorker {
}
}
```
@@ -1,8 +1,9 @@
---
title: Backend Commands
icon: 'terminal'
icon: "terminal"
---
## Useful commands
These commands should be executed from packages/twenty-server folder.
@@ -32,7 +33,6 @@ npx nx run twenty-server:lint # pass --fix to fix lint errors
npx nx run twenty-server:test:unit # run unit tests
npx nx run twenty-server:test:integration # run integration tests
```
Note: you can run `npx nx run twenty-server:test:integration:with-db-reset` in case you need to reset the database before running the integration tests.
### Resetting the database
@@ -59,29 +59,26 @@ Prisma was the first ORM we used. But in order to allow users to create custom f
Here's what the tech stack now looks like.
**Core**
**Core**
- [NestJS](https://nestjs.com/)
- [TypeORM](https://typeorm.io/)
- [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
**Database**
- [Postgres](https://www.postgresql.org/)
**Third-party integrations**
- [Sentry](https://sentry.io/welcome/) for tracking bugs
**Testing**
- [Jest](https://jestjs.io/)
**Tooling**
- [Yarn](https://yarnpkg.com/)
- [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
**Development**
- [AWS EKS](https://aws.amazon.com/eks/)
@@ -2,6 +2,7 @@
title: Zapier App
---
Effortlessly sync Twenty with 3000+ apps using [Zapier](https://zapier.com/). Automate tasks, boost productivity, and supercharge your customer relationships!
## About Zapier
@@ -35,7 +36,6 @@ From the `packages/twenty-zapier` folder, run:
```bash
cp .env.example .env
```
Run the application locally, go to [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks), and generate an API key.
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
@@ -49,37 +49,28 @@ Make sure to run `yarn build` before any `zapier` command.
</Warning>
### Test
```bash
yarn test
```
### Lint
```bash
yarn format
```
### Watch and compile as you edit code
```bash
yarn watch
```
### Validate your Zapier app
```bash
yarn validate
```
### Deploy your Zapier app
```bash
yarn deploy
```
### List all Zapier CLI commands
```bash
zapier
```
@@ -1,9 +1,10 @@
---
title: Bugs, Requests & Pull Requests
icon: 'bug'
icon: "bug"
info: Report issues, request features, and contribute code
---
## Reporting Bugs
To report a bug, please [create an issue on GitHub](https://github.com/twentyhq/twenty/issues/new).
@@ -28,14 +29,12 @@ Contributing code to Twenty starts with a pull request (PR).
1. Fork the repository on GitHub
2. Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/twenty.git
cd twenty
```
3. Add upstream remote:
```bash
git remote add upstream https://github.com/twentyhq/twenty.git
```
@@ -47,7 +46,6 @@ git checkout -b feature/your-feature-name
```
Use descriptive branch names:
- `feature/add-export-button`
- `fix/login-redirect-issue`
- `docs/update-api-guide`
@@ -62,7 +60,6 @@ Use descriptive branch names:
### Submit Your PR
1. Push your branch:
```bash
git push origin feature/your-feature-name
```
@@ -77,3 +74,4 @@ git push origin feature/your-feature-name
- [ ] Tests pass locally
- [ ] Documentation is updated
- [ ] PR description explains the changes
@@ -1,8 +1,9 @@
---
title: Best Practices
icon: 'star'
icon: "star"
---
This document outlines the best practices you should follow when working on the frontend.
## State management
@@ -33,10 +34,13 @@ export const MyComponent = () => {
return (
<div>
<input value={myAtom} onChange={(e) => setMyAtom(e.target.value)} />
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
};
}
```
### Do not use `useRef` to store state
@@ -87,7 +91,7 @@ export const PageComponent = () => {
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if (someDependency !== data) {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
@@ -95,7 +99,9 @@ export const PageComponent = () => {
return <div>{data}</div>;
};
export const App = () => <PageComponent />;
export const App = () => (
<PageComponent />
);
```
```tsx
@@ -112,7 +118,7 @@ export const PageData = () => {
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if (someDependency !== data) {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
@@ -163,7 +169,6 @@ Make sure you remove all `console.logs` before pushing the code to production.
Variable names ought to precisely depict the purpose or function of the variable.
#### The issue with generic names
Generic names in programming are not ideal because they lack specificity, leading to ambiguity and reduced code readability. Such names fail to convey the variable or function's purpose, making it challenging for developers to understand the code's intent without deeper investigation. This can result in increased debugging time, higher susceptibility to errors, and difficulties in maintenance and collaboration. Meanwhile, descriptive naming makes the code self-explanatory and easier to navigate, enhancing code quality and developer productivity.
```tsx
@@ -179,7 +184,7 @@ const [email, setEmail] = useState('');
#### Some words to avoid in variable names
- dummy
- dummy
### Event handlers
@@ -282,11 +287,14 @@ When importing, opt for the designated aliases rather than specifying complete o
```
**Usage**
```tsx
// ❌ Bad, specifies the entire relative path
import { CatalogDecorator } from '../../../../../testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '../../../../../testing/decorators/ComponentDecorator';
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
@@ -315,6 +323,9 @@ const validationSchema = z
type Form = z.infer<typeof validationSchema>;
```
## Breaking Changes
Always perform thorough manual testing before proceeding to guarantee that modifications havent caused disruptions elsewhere, given that tests have not yet been extensively integrated.
@@ -1,9 +1,10 @@
---
title: Folder Architecture
icon: 'folder-tree'
icon: "folder-tree"
info: A detailed look into our folder architecture
---
In this guide, you will explore the details of the project directory structure and how it contributes to the organization and maintainability of Twenty.
By following this folder architecture convention, it's easier to find the files related to specific features and ensure that the application is scalable and maintainable.
@@ -93,6 +94,7 @@ React's built-in state management still handles state within a component.
Should just contain reusable pure functions. Otherwise, create custom hooks in the `hooks` folder.
## UI
Contains all the reusable UI components used in the application.
@@ -108,3 +110,4 @@ You can import other module code from any module except for the `ui` folder. Thi
### Internal
Each part (hooks, states, ...) of a module can have an `internal` folder, which contains parts that are just used within the module.
@@ -1,8 +1,9 @@
---
title: Frontend Commands
icon: 'terminal'
icon: "terminal"
---
## Useful commands
### Starting the app
@@ -16,9 +17,7 @@ npx nx start twenty-front
```bash
npx nx run twenty-front:graphql:generate --configuration=metadata
```
OR
```bash
npx nx run twenty-front:graphql:generate
```
@@ -89,3 +88,5 @@ See [best practices](/developers/contribute/capabilities/frontend-development/be
Jest is mainly for testing utility functions, and not components themselves.
Storybook is for testing the behavior of isolated components, as well as displaying the design system.
@@ -2,6 +2,7 @@
title: Hotkeys
---
## Introduction
When you need to listen to a hotkey, you would normally use the `onKeyDown` event listener.
@@ -19,7 +20,6 @@ You place it in a component, and it will listen to the hotkeys only when the com
## How to listen for hotkeys in practice?
There are two steps involved in setting up hotkey listening :
1. Set the [hotkey scope](#what-is-a-hotkey-scope-) that will listen to hotkeys
2. Use the `useScopedHotkeys` hook to listen to hotkeys
@@ -28,7 +28,6 @@ Setting up hotkey scopes is required even in simple pages, because other UI elem
## Use cases for hotkeys
In general, you'll have two use cases that require hotkeys :
1. In a page or a component mounted in a page
2. In a modal-type component that takes the focus due to a user action
@@ -89,7 +88,9 @@ const ExamplePageWithModal = () => {
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(ExampleHotkeyScopes.ExampleModal);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
@@ -98,13 +99,11 @@ const ExamplePageWithModal = () => {
goBackToPreviousHotkeyScope();
};
return (
<div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>
);
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
@@ -118,7 +117,7 @@ const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
useScopedHotkeys(
Key.Escape,
() => {
onClose();
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
@@ -6,3 +6,4 @@ description: Browse Twenty's UI component library
View our complete component library and documentation in Storybook.
[Open Storybook →](https://storybook.twenty.com)
@@ -1,8 +1,9 @@
---
title: Style Guide
icon: 'paintbrush'
icon: "paintbrush"
---
This document includes the rules to follow when writing code.
The goal here is to have a consistent codebase, which is easy to read and easy to maintain.
@@ -32,7 +33,7 @@ export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
}
};
```
### Props
@@ -50,9 +51,7 @@ type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => (
<div>Hello {name}</div>
);
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### Refrain from using `React.FC` or `React.FunctionComponent` to define prop types
@@ -93,7 +92,7 @@ Avoid using single variable prop spreading in JSX elements, like `{...props}`. T
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
};
}
```
```tsx
@@ -106,7 +105,6 @@ const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
```
Rationale:
- At a glance, it's clearer which props the code passes down, making it easier to understand and maintain.
- It helps to prevent tight coupling between components via their props.
- Linting tools make it easier to identify misspelled or unused props when you list props explicitly.
@@ -160,9 +158,9 @@ You can see why TypeScript recommends avoiding enums [here](https://www.typescri
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue',
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
@@ -171,7 +169,7 @@ let color = Color.Red;
```tsx
// ✅ Good, utilizes a string literal
let color: 'red' | 'green' | 'blue' = 'red';
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL and internal libraries
@@ -239,6 +237,7 @@ Avoid using `px` or `rem` values directly within the styled components. The nece
Refrain from introducing new colors; instead, use the existing palette from the theme. Should there be a situation where the palette does not align, please leave a comment so that the team can rectify it.
```tsx
// ❌ Bad, directly specifies style values without utilizing the theme
const StyledButton = styled.button`
@@ -257,10 +256,9 @@ const StyledButton = styled.button`
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
## Enforcing No-Type Imports
Avoid type imports. To enforce this standard, an Oxlint rule checks for and reports any type imports. This helps maintain consistency and readability in the TypeScript code.
@@ -3,6 +3,7 @@ title: Work with Figma
info: Learn how you can collaborate with Twenty's Figma
---
Figma is a collaborative interface design tool that aids in bridging the communication barrier between designers and developers.
This guide explains how you can collaborate with Figma.
@@ -10,7 +11,7 @@ This guide explains how you can collaborate with Figma.
1. **Access the shared link:** You can access the project's Figma file [here](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
2. **Sign in:** If you're not already signed in, Figma will prompt you to do so.
Key features are only available to logged-in users, such as the developer mode and the ability to select a dedicated frame.
Key features are only available to logged-in users, such as the developer mode and the ability to select a dedicated frame.
<Warning>
@@ -18,12 +19,13 @@ You will not be able to collaborate effectively without an account.
</Warning>
## Figma structure
On the left sidebar, you can access the different pages of Twenty's Figma. This is how they're organized:
- **Components page:** This is the first page. The designer uses it to create and organize the reusable design elements used throughout the design file. For example, buttons, icons, symbols, or any other reusable components. It serves to maintain consistency across the design.
- **Main page:** The second page is the main page, which shows the complete user interface of the project. You can press **_Play_** to use the full app prototype.
- **Main page:** The second page is the main page, which shows the complete user interface of the project. You can press ***Play*** to use the full app prototype.
- **Features pages:** The other pages are typically dedicated to features in progress. They contain the design of specific features or modules of the application or website. They are typically still in progress.
## Useful Tips
@@ -59,3 +61,4 @@ It's part of our recommended extensions.
1. **Using Comments:** You are welcome to use the comment feature by clicking on the bubble icon in the left part of the toolbar.
2. **Cursor chat:** A nice feature of Figma is the Cursor chat. Just press `;` on Mac and `/` on Windows to send a message if you see someone else using Figma as the same time as you.
@@ -1,25 +1,24 @@
---
title: Local Setup
icon: 'laptop-code'
description: 'The guide for contributors (or curious developers) who want to run Twenty locally.'
icon: "laptop-code"
description: "The guide for contributors (or curious developers) who want to run Twenty locally."
---
## Prerequisites
<Tabs>
<Tab title="Linux and macOS">
Before you can install and use Twenty, make sure you install the following on your computer:
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [Node v24.5.0](https://nodejs.org/en/download)
- [yarn v4](https://yarnpkg.com/getting-started/install)
- [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
`npm` won't work, you should use `yarn` instead. Yarn is now shipped with
Node.js, so you don't need to install it separately. You only have to run
`corepack enable` to enable Yarn if you haven't done it yet.
`npm` won't work, you should use `yarn` instead. Yarn is now shipped with Node.js, so you don't need to install it separately.
You only have to run `corepack enable` to enable Yarn if you haven't done it yet.
</Warning>
</Tab>
@@ -27,12 +26,10 @@ Before you can install and use Twenty, make sure you install the following on yo
<Tab title="Windows (WSL)">
1. Install WSL
Open PowerShell as Administrator and run:
Open PowerShell as Administrator and run:
```powershell
wsl --install
```
You should now see a prompt to restart your computer. If not, restart it manually.
Upon restart, a PowerShell window will open and install Ubuntu. This may take up some time.
@@ -51,8 +48,7 @@ git config --global user.email "youremail@domain.com"
3. Install nvm, node.js and yarn
<Warning>
Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all
contributors use the same version.
Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version.
</Warning>
```bash
@@ -60,7 +56,6 @@ sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
Close and reopen your terminal to use nvm. Then run the following commands.
```bash
@@ -120,7 +115,6 @@ You should run all commands in the following steps from the root of the project.
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (preferred):** To provision your database locally with `brew`:
@@ -177,7 +171,6 @@ You should run all commands in the following steps from the root of the project.
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
All the following steps are to be run in the WSL terminal (within your virtual machine)
@@ -195,7 +188,6 @@ You should run all commands in the following steps from the root of the project.
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
@@ -204,7 +196,6 @@ You can now access the database at `localhost:5432`.
If you used the Docker option above, the default credentials are user `postgres` and password `postgres`. For native PostgreSQL installations, use the credentials and roles configured on your machine.
## Step 4: Set up a Redis Database (cache)
Twenty requires a Redis cache to provide the best performance.
<Tabs>
@@ -216,7 +207,6 @@ Twenty requires a Redis cache to provide the best performance.
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**Option 1 (preferred):** To provision your Redis locally with `brew`:
@@ -232,7 +222,6 @@ Twenty requires a Redis cache to provide the best performance.
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
**Option 1:** To provision your Redis locally:
@@ -242,7 +231,6 @@ Twenty requires a Redis cache to provide the best performance.
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
@@ -253,61 +241,48 @@ If you need a client GUI, we recommend [Redis Insight](https://redis.io/insight/
Use environment variables or `.env` files to configure your project. More info [here](/developers/self-host/capabilities/setup).
Copy the `.env.example` files in `/front` and `/server`:
```bash
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**Multi-Workspace Mode:** By default, Twenty runs in single-workspace mode
where only one workspace can be created. To enable multi-workspace support
(useful for testing subdomain-based features), set
`IS_MULTIWORKSPACE_ENABLED=true` in your server `.env` file. See
[Multi-Workspace
Mode](/developers/self-host/capabilities/setup#multi-workspace-mode) for
details.
**Multi-Workspace Mode:** By default, Twenty runs in single-workspace mode where only one workspace can be created. To enable multi-workspace support (useful for testing subdomain-based features), set `IS_MULTIWORKSPACE_ENABLED=true` in your server `.env` file. See [Multi-Workspace Mode](/developers/self-host/capabilities/setup#multi-workspace-mode) for details.
</Info>
## Step 6: Installing dependencies
To build Twenty server and seed some data into your database, run the following command:
```bash
yarn
```
Note that `npm` or `pnpm` won't work
## Step 7: Running the project
<Tabs>
<Tab title="Linux">
Depending on your Linux distribution, Redis server might be started
automatically. If not, check the [Redis installation
guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)
for your distro.
Depending on your Linux distribution, Redis server might be started automatically.
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
</Tab>
<Tab title="Mac OS">
Redis should already be running. If not, run: ```bash brew services start
redis ```
Redis should already be running. If not, run:
```bash
brew services start redis
```
</Tab>
<Tab title="Windows (WSL)">
Depending on your Linux distribution, Redis server might be started
automatically. If not, check the [Redis installation
guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)
for your distro.
Depending on your Linux distribution, Redis server might be started automatically.
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
</Tab>
</Tabs>
Set up your database with the following command:
Set up your database with the following command:
```bash
npx nx database:reset twenty-server
```
Start the server, the worker and the frontend services:
```bash
npx nx start twenty-server
npx nx worker twenty-server
@@ -315,7 +290,6 @@ npx nx start twenty-front
```
Alternatively, you can start all services at once:
```bash
npx nx start
```
@@ -333,6 +307,8 @@ You can log in using the default demo account: `tim@apple.dev` (password: `tim@a
- The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql)
- The REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest)
## Troubleshooting
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
@@ -1,6 +1,6 @@
---
title: Commands
icon: 'terminal'
icon: "terminal"
description: Useful commands for developing Twenty.
---
@@ -2,7 +2,6 @@
title: Contribute
description: Contribute to Twenty's open-source development.
---
<Frame>
<img src="/images/user-guide/github/github-header.png" alt="AI" />
</Frame>
@@ -22,18 +21,11 @@ Twenty is open-source and welcomes contributions from the community. Whether you
## Getting Started
<CardGroup cols={2}>
<Card
title="Bug Reports & Requests"
icon="bug"
href="/developers/contribute/capabilities/bug-and-requests"
>
<Card title="Bug Reports & Requests" icon="bug" href="/developers/contribute/capabilities/bug-and-requests">
Report issues or request features
</Card>
<Card
title="Frontend Development"
icon="browser"
href="/developers/contribute/capabilities/frontend-development"
>
<Card title="Frontend Development" icon="browser" href="/developers/contribute/capabilities/frontend-development">
Contribute to the UI
</Card>
</CardGroup>
@@ -1,6 +1,6 @@
---
title: Style Guide
icon: 'paintbrush'
icon: "paintbrush"
description: Code conventions and best practices for contributing to Twenty.
---
@@ -20,7 +20,7 @@ export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
}
};
```
### Props
@@ -32,9 +32,7 @@ type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => (
<div>Hello {name}</div>
);
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### No single-variable prop spreading
@@ -44,9 +42,7 @@ export const MyComponent = ({ name }: MyComponentProps) => (
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => (
<Other {...{ prop1, prop2 }} />
);
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## State Management
@@ -79,9 +75,7 @@ export const myAtomState = createAtomState<string>({
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => {
setData(dep);
}, [dep]);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
@@ -89,9 +83,7 @@ export const Page = () => {
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => {
setData(dep);
}, [dep]);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
@@ -1,6 +1,6 @@
---
title: APIs
icon: 'plug'
icon: "plug"
description: REST and GraphQL APIs generated from your workspace schema.
---
@@ -26,10 +26,10 @@ Both are available as REST and GraphQL. GraphQL adds batch upserts and the abili
## Base URLs
| Environment | Base URL |
| ----------- | ------------------------- |
| Cloud | `https://api.twenty.com/` |
| Self-Hosted | `https://{your-domain}/` |
| Environment | Base URL |
|-------------|----------|
| Cloud | `https://api.twenty.com/` |
| Self-Hosted | `https://{your-domain}/` |
## Authentication
@@ -49,7 +49,7 @@ Both REST and GraphQL support batching up to 60 records per request — create,
## Rate limits
| Limit | Value |
| ---------- | ------------------- |
| Requests | 100 per minute |
| Limit | Value |
|-------|-------|
| Requests | 100 per minute |
| Batch size | 60 records per call |
@@ -1,15 +1,15 @@
---
title: Application Config
description: Declare your app's identity, default role, variables, and marketplace metadata with defineApplication.
icon: 'rocket'
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)_ — keyvalue pairs exposed to your code as environment variables.
- **Pre-install / post-install hooks** _(optional)_ — see [Logic Functions](/developers/extend/apps/logic/logic-functions).
- **Variables** *(optional)* — keyvalue pairs exposed to your code as environment variables.
- **Pre-install / post-install hooks** *(optional)* — see [Logic Functions](/developers/extend/apps/logic/logic-functions).
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
@@ -30,7 +30,6 @@ export default defineApplication({
```
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. In logic functions (server-side), they are available as `process.env.VARIABLE_NAME`. In front components, use `getApplicationVariable('VARIABLE_NAME')` from `twenty-sdk/front-component`. Variables marked with `isSecret: true` are only injected into logic functions. Front components receive only non-secret variables.
- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`.
@@ -51,14 +50,14 @@ When you scaffold a new app, the CLI creates a starter role file at `src/roles/d
If you plan to [publish your app](/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`) |
| 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 |
| `websiteUrl` | Link to your website |
| `termsUrl` | Link to terms of service |
| `emailSupport` | Support email address |
| `issueReportUrl` | Link to issue tracker |
@@ -1,7 +1,7 @@
---
title: Install Hooks
description: Run logic before or after the install — seed data, back up records, validate the upgrade.
icon: 'wrench'
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](/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).
@@ -25,16 +25,10 @@ Each app may define **at most one pre-install** and **at most one post-install**
A post-install function runs automatically once your app has finished installing on a workspace. The server executes it **after** the app's metadata has been synchronized and the SDK client has been generated, so the workspace is fully ready to use and the new schema is in place. Typical use cases include seeding default data, creating initial records, configuring workspace settings, or provisioning resources on third-party services.
```ts src/logic-functions/post-install.ts
import {
definePostInstallLogicFunction,
type InstallPayload,
} from 'twenty-sdk/define';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log(
'Post install logic function executed successfully!',
payload.previousVersion,
);
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
@@ -55,12 +49,11 @@ yarn twenty dev:function:exec --postInstall
```
Key points:
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls _how_ post-install is executed.
- `shouldRunSynchronously: false` _(default)_ — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
- `shouldRunSynchronously: false` *(default)* — the hook is **enqueued on the message queue** with `retryLimit: 3` and runs asynchronously in a worker. The install response returns as soon as the job is enqueued, so a slow or failing handler does not block the caller. The worker will retry up to three times. **Use this for long-running jobs** — seeding large datasets, calling slow third-party APIs, provisioning external resources, anything that might exceed a reasonable HTTP response window.
- `shouldRunSynchronously: true` — the hook is executed **inline during the install flow** (same executor as pre-install). The install request blocks until the handler finishes, and if it throws, the install caller receives a `POST_INSTALL_ERROR`. No automatic retries. **Use this for fast, must-complete-before-response work** — for example, emitting a validation error to the user, or quick setup that the client will rely on immediately after the install call returns. Keep in mind the metadata migration has already been applied by the time post-install runs, so a sync-mode failure does **not** roll back the schema changes — it only surfaces the error.
- Make sure your handler is idempotent. In async mode the queue may retry up to three times; in either mode the hook may run again on upgrades when `shouldRunOnVersionUpgrade: true`.
- The environment variables `APPLICATION_ID`, `APP_ACCESS_TOKEN`, and `API_URL` are available inside the handler (same as any other logic function), so you can call the Twenty API with an application access token scoped to your app.
@@ -75,16 +68,10 @@ Key points:
A pre-install function runs automatically during installation, **before the workspace metadata migration is applied**. It shares the same payload shape as post-install (`InstallPayload`), but it is positioned earlier in the install flow so it can prepare state that the upcoming migration depends on — typical uses include backing up data, validating compatibility with the new schema, or archiving records that are about to be restructured or dropped.
```ts src/logic-functions/pre-install.ts
import {
definePreInstallLogicFunction,
type InstallPayload,
} from 'twenty-sdk/define';
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
const handler = async (payload: InstallPayload): Promise<void> => {
console.log(
'Pre install logic function executed successfully!',
payload.previousVersion,
);
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
@@ -104,7 +91,6 @@ yarn twenty dev:function:exec --preInstall
```
Key points:
- Pre-install functions use `definePreInstallLogicFunction()` — same specialized config as post-install, just attached to a different lifecycle slot.
- Both pre- and post-install handlers receive the same `InstallPayload` type: `{ previousVersion?: string; newVersion: string }`. Import it once and reuse it for both hooks.
- **When the hook runs**: positioned just before the workspace metadata migration (`synchronizeFromManifest`). Before executing, the server runs a purely additive "pared-down sync" that registers the **new** version's pre-install function in the workspace metadata — nothing else is touched — and then executes it. Because this sync is additive-only, the previous version's objects, fields, and data are still intact when your handler runs: you can safely read and back up pre-migration state.
@@ -129,10 +115,7 @@ Pre-install is always **synchronous** (it blocks the install and can abort it).
Example — seed a default `PostCard` record after install:
```ts src/logic-functions/post-install.ts
import {
definePostInstallLogicFunction,
type InstallPayload,
} from 'twenty-sdk/define';
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
@@ -154,7 +137,7 @@ export default definePostInstallLogicFunction({
});
```
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the _previous_ schema and its failure rolls back the upgrade, it is the right place for anything risky:
**Use `pre-install` when a migration would otherwise destroy or corrupt existing data.** Because pre-install runs against the *previous* schema and its failure rolls back the upgrade, it is the right place for anything risky:
- **Backing up data that is about to be dropped or restructured** — e.g. you are removing a field in v2 and need to copy its values into another field or export them to storage before the migration runs.
- **Archiving records that a new constraint would invalidate** — e.g. a field is becoming `NOT NULL` and you need to delete or fix rows with null values first.
@@ -164,16 +147,10 @@ export default definePostInstallLogicFunction({
Example — archive records before a destructive migration:
```ts src/logic-functions/pre-install.ts
import {
definePreInstallLogicFunction,
type InstallPayload,
} from 'twenty-sdk/define';
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { createClient } from './generated/client';
const handler = async ({
previousVersion,
newVersion,
}: InstallPayload): Promise<void> => {
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;
@@ -202,8 +179,7 @@ const handler = async ({
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description:
'Backs up legacy notes into description before the v2 migration.',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
@@ -212,20 +188,18 @@ export default definePreInstallLogicFunction({
**Rule of thumb:**
| You want to... | Use |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Seed default data, configure the workspace, register external resources | `post-install` |
| You want to... | Use |
|---|---|
| Seed default data, configure the workspace, register external resources | `post-install` |
| Run long-running seeding or third-party calls that shouldn't block the install response | `post-install` (default — `shouldRunSynchronously: false`, with worker retries) |
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
| Read or back up data that the upcoming migration would lose | `pre-install` |
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
| Run fast setup that the caller will rely on immediately after the install call returns | `post-install` with `shouldRunSynchronously: true` |
| Read or back up data that the upcoming migration would lose | `pre-install` |
| Reject an upgrade that would corrupt existing data | `pre-install` (throw from the handler) |
| Run reconciliation on every upgrade | `post-install` with `shouldRunOnVersionUpgrade: true` |
| Do one-off setup on the first install only | `post-install` with `shouldRunOnVersionUpgrade: false` (default) |
<Note>
If in doubt, default to **post-install**. Only reach for pre-install when the
migration itself is destructive and you need to intercept the previous state
before it is gone.
If in doubt, default to **post-install**. Only reach for pre-install when the migration itself is destructive and you need to intercept the previous state before it is gone.
</Note>
</Accordion>
@@ -1,10 +1,10 @@
---
title: Overview
description: Configure the app itself — its identity, default permissions, and what runs at install time.
icon: 'screwdriver-wrench'
icon: "screwdriver-wrench"
---
A Twenty app's **config layer** is what describes the app _to the platform_ — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty _who the app is_ and _how to set it up_.
A Twenty app's **config layer** is what describes the app *to the platform* — its identity, the permissions it holds, and the code that runs during install or upgrade. These declarations don't add new data shapes or runtime behavior; they tell Twenty *who the app is* and *how to set it up*.
```text
┌────────────────────────────────────────────────────────┐
@@ -29,28 +29,14 @@ A Twenty app's **config layer** is what describes the app _to the platform_ —
## In this section
<CardGroup cols={2}>
<Card
title="Application Config"
icon="rocket"
href="/developers/extend/apps/config/application"
>
`defineApplication` — identity, default role, variables, marketplace
metadata.
<Card title="Application Config" icon="rocket" href="/developers/extend/apps/config/application">
`defineApplication` — identity, default role, variables, marketplace metadata.
</Card>
<Card
title="Roles & Permissions"
icon="shield-halved"
href="/developers/extend/apps/config/roles"
>
<Card title="Roles & Permissions" icon="shield-halved" href="/developers/extend/apps/config/roles">
`defineRole` — declare what your app's logic functions can read and write.
</Card>
<Card
title="Install Hooks"
icon="wrench"
href="/developers/extend/apps/config/install-hooks"
>
`definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back
up data, seed defaults, validate upgrades.
<Card title="Install Hooks" icon="wrench" href="/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction` and `definePostInstallLogicFunction` — back up data, seed defaults, validate upgrades.
</Card>
</CardGroup>
@@ -61,9 +47,5 @@ A Twenty app's **config layer** is what describes the app _to the platform_ —
- **Install Hooks** run during install or upgrade — pre-install before the metadata migration (so it can refuse a risky upgrade), post-install after the migration (so it can seed default data against the new schema).
<Note>
Install hooks share the [logic
function](/developers/extend/apps/logic/logic-functions) runtime — same
handler signature, same environment variables, same typed API client — but
they're declared with their own define functions and live outside the regular
trigger model (HTTP, cron, database events).
Install hooks share the [logic function](/developers/extend/apps/logic/logic-functions) runtime — same handler signature, same environment variables, same typed API client — but they're declared with their own define functions and live outside the regular trigger model (HTTP, cron, database events).
</Note>
@@ -1,7 +1,7 @@
---
title: Public Assets
description: Ship static files — images, icons, fonts — alongside your app via the public/ folder.
icon: 'folder-open'
icon: "folder-open"
---
The `public/` folder at the root of your app holds static files — images, icons, fonts, or any other assets your app needs at runtime. These files are automatically included in builds, synced during dev mode, and uploaded to the server.
@@ -1,7 +1,7 @@
---
title: Extending Objects
description: Add fields to standard Twenty objects (Person, Company, …) or to objects from other apps using defineField.
icon: 'wand-magic-sparkles'
icon: "wand-magic-sparkles"
---
Use `defineField()` to add a field to an object you don't own — a standard Twenty object like Person or Company, or an object shipped by another installed app. Unlike inline fields declared inside [`defineObject`](/developers/extend/apps/data/objects), standalone fields require an `objectUniversalIdentifier` to specify which object they extend.
@@ -1,7 +1,7 @@
---
title: Objects
description: Declare new record types — custom tables with their own fields — using defineObject.
icon: 'table'
icon: "table"
---
Custom **objects** are new record types your app adds to a workspace — Post Card, Invoice, Subscription, anything specific to your domain. Each object declares its schema (fields, relations, default values) and a stable universal identifier that survives across syncs and deploys.
@@ -55,30 +55,10 @@ export default defineObject({
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
@@ -103,12 +83,7 @@ export default defineObject({
- You can scaffold new objects with `yarn twenty dev:add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding).
<Note>
**Base fields are added automatically.** When you define a custom object,
Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`,
`createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare
them in your `fields` array — only your custom fields. You can override a
default field by declaring one with the same name, but this is rarely a good
idea.
**Base fields are added automatically.** When you define a custom object, Twenty creates standard fields like `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and `deletedAt` for you. You don't need to declare them in your `fields` array — only your custom fields. You can override a default field by declaring one with the same name, but this is rarely a good idea.
</Note>
## What's next
@@ -1,10 +1,10 @@
---
title: Overview
description: Shape the data your app adds to a workspace — objects, fields, and relations.
icon: 'database'
icon: "database"
---
A Twenty app's **data layer** is the data your app _adds_ to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other.
A Twenty app's **data layer** is the data your app *adds* to a workspace — the new record types it declares, the columns it adds to existing objects, and how those records connect to each other.
```text
┌──────────────────────────────────────────────────┐
@@ -26,43 +26,27 @@ A Twenty app's **data layer** is the data your app _adds_ to a workspace — the
## In this section
<CardGroup cols={2}>
<Card
title="Objects"
icon="table"
href="/developers/extend/apps/data/objects"
>
<Card title="Objects" icon="table" href="/developers/extend/apps/data/objects">
`defineObject` — declare new record types with their own fields.
</Card>
<Card
title="Extending Objects"
icon="wand-magic-sparkles"
href="/developers/extend/apps/data/extending-objects"
>
<Card title="Extending Objects" icon="wand-magic-sparkles" href="/developers/extend/apps/data/extending-objects">
`defineField` — add fields to standard or other apps' objects.
</Card>
<Card
title="Relations"
icon="diagram-project"
href="/developers/extend/apps/data/relations"
>
<Card title="Relations" icon="diagram-project" href="/developers/extend/apps/data/relations">
Bidirectional `MANY_TO_ONE` / `ONE_TO_MANY` connections between objects.
</Card>
</CardGroup>
## Entities at a glance
| Entity | Purpose | Defined with |
| ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` |
| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` |
| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` |
| Entity | Purpose | Defined with |
|--------|---------|--------------|
| **Object** | A new custom record type (e.g. PostCard, Invoice) with its own fields | `defineObject()` |
| **Field** | A column on an object. Standalone fields can extend objects you didn't create (e.g. add `loyaltyTier` to Company) | `defineField()` |
| **Relation** | A bidirectional link between two objects — both sides declared as fields | `defineField()` with `FieldType.RELATION` |
The SDK detects these via AST analysis at build time, so file organization is up to you — the convention is `src/objects/` and `src/fields/`. Stable `universalIdentifier` UUIDs tie everything together across deploys.
<Note>
Looking for **Application Config** or **Roles & Permissions**? Those describe
the app itself rather than the data it adds — they live under
[Config](/developers/extend/apps/config/overview). Looking for **Connections**
(Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions
and live under [Logic](/developers/extend/apps/logic/connections).
Looking for **Application Config** or **Roles & Permissions**? Those describe the app itself rather than the data it adds — they live under [Config](/developers/extend/apps/config/overview). Looking for **Connections** (Linear, GitHub, Slack OAuth)? Those exist to be called *from* logic functions and live under [Logic](/developers/extend/apps/logic/connections).
</Note>
@@ -1,15 +1,15 @@
---
title: Relations
description: Connect objects together with bidirectional MANY_TO_ONE / ONE_TO_MANY relations.
icon: 'diagram-project'
icon: "diagram-project"
---
Relations connect two objects together. In Twenty, relations are always **bidirectional** — every relation has two sides, and each side is declared as a field that references the other.
| Relation type | Description | Has foreign key? |
| ------------- | ------------------------------------------------------------- | ---------------------- |
| Relation type | Description | Has foreign key? |
|---------------|-------------|------------------|
| `MANY_TO_ONE` | Many records of this object point to one record of the target | Yes (`joinColumnName`) |
| `ONE_TO_MANY` | One record of this object has many records of the target | No (the inverse side) |
| `ONE_TO_MANY` | One record of this object has many records of the target | No (the inverse side) |
## How relations work
@@ -32,8 +32,7 @@ import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID =
'a1111111-1111-1111-1111-111111111111';
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
@@ -44,8 +43,7 @@ export default defineField({
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
@@ -56,12 +54,7 @@ export default defineField({
**Step 2: Define the MANY_TO_ONE side on PostCardRecipient** (the "many" side — holds the foreign key):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
} from 'twenty-sdk/define';
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
@@ -77,8 +70,7 @@ export default defineField({
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
@@ -89,10 +81,7 @@ export default defineField({
```
<Note>
**Circular imports:** both relation fields reference each other's
`universalIdentifier`. To avoid circular import issues, export your field IDs
as named constants from each file and import them in the other. The build
system resolves these at compile time.
**Circular imports:** both relation fields reference each other's `universalIdentifier`. To avoid circular import issues, export your field IDs as named constants from each file and import them in the other. The build system resolves these at compile time.
</Note>
## Relating to standard objects
@@ -110,8 +99,7 @@ import {
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID =
'd4444444-4444-4444-4444-444444444444';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
@@ -123,8 +111,7 @@ export default defineField({
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
SELF_HOSTING_USER_REVERSE_FIELD_ID,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
@@ -135,14 +122,14 @@ export default defineField({
## Relation field properties
| Property | Required | Description |
| ------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------- |
| `type` | Yes | Must be `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object |
| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object |
| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) |
| Property | Required | Description |
|----------|----------|-------------|
| `type` | Yes | Must be `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the target object |
| `relationTargetFieldMetadataUniversalIdentifier` | Yes | The `universalIdentifier` of the matching field on the target object |
| `universalSettings.relationType` | Yes | `RelationType.MANY_TO_ONE` or `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | MANY_TO_ONE only | What happens when the referenced record is deleted: `CASCADE`, `SET_NULL`, `RESTRICT`, or `NO_ACTION` |
| `universalSettings.joinColumnName` | MANY_TO_ONE only | Database column name for the foreign key (e.g., `postCardId`) |
## Inline relation fields
@@ -159,10 +146,8 @@ export default defineObject({
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_ID,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
@@ -1,7 +1,7 @@
---
title: Concepts
description: How Twenty apps work — entity model, sandboxing, and the install lifecycle.
icon: 'sitemap'
icon: "sitemap"
---
Twenty apps are TypeScript packages that extend your workspace with custom objects, logic, UI components, and AI capabilities. They run on the Twenty platform with full sandboxing and permission controls.
@@ -29,29 +29,27 @@ your-app/
```
<Note>
**File organization is up to you.** Entity detection is AST-based — the SDK
finds `export default defineEntity(...)` calls regardless of where the file
lives. The folder structure above is a convention, not a requirement.
**File organization is up to you.** Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. The folder structure above is a convention, not a requirement.
</Note>
## Entity types
| Entity | Purpose | Docs |
| ------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- |
| **Application** | App identity, default role, variables | [Application Config](/developers/extend/apps/config/application) |
| **Role** | Permission sets on objects and fields | [Roles & Permissions](/developers/extend/apps/config/roles) |
| **Object** | Custom record types with fields | [Objects](/developers/extend/apps/data/objects) |
| **Field** | Add fields to objects from other apps | [Extending Objects](/developers/extend/apps/data/extending-objects) |
| **Relation** | Bidirectional links between objects | [Relations](/developers/extend/apps/data/relations) |
| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/developers/extend/apps/logic/logic-functions) |
| **Skill** | Reusable AI agent instructions | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | AI assistants with custom prompts | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) |
| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/developers/extend/apps/logic/connections) |
| **View** | Pre-configured record list views | [Views](/developers/extend/apps/layout/views) |
| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) |
| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/developers/extend/apps/layout/page-layouts) |
| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/developers/extend/apps/layout/front-components) |
| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/developers/extend/apps/layout/command-menu-items) |
| Entity | Purpose | Docs |
|--------|---------|------|
| **Application** | App identity, default role, variables | [Application Config](/developers/extend/apps/config/application) |
| **Role** | Permission sets on objects and fields | [Roles & Permissions](/developers/extend/apps/config/roles) |
| **Object** | Custom record types with fields | [Objects](/developers/extend/apps/data/objects) |
| **Field** | Add fields to objects from other apps | [Extending Objects](/developers/extend/apps/data/extending-objects) |
| **Relation** | Bidirectional links between objects | [Relations](/developers/extend/apps/data/relations) |
| **Logic Function** | Server-side TypeScript with triggers | [Logic Functions](/developers/extend/apps/logic/logic-functions) |
| **Skill** | Reusable AI agent instructions | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) |
| **Agent** | AI assistants with custom prompts | [Skills & Agents](/developers/extend/apps/logic/skills-and-agents) |
| **Connection Provider** | OAuth credentials for third-party APIs | [Connections](/developers/extend/apps/logic/connections) |
| **View** | Pre-configured record list views | [Views](/developers/extend/apps/layout/views) |
| **Navigation Menu Item** | Custom sidebar entries | [Navigation Menu Items](/developers/extend/apps/layout/navigation-menu-items) |
| **Page Layout** | Tabs and widgets on a record's detail page | [Page Layouts](/developers/extend/apps/layout/page-layouts) |
| **Front Component** | Sandboxed React UI inside Twenty | [Front Components](/developers/extend/apps/layout/front-components) |
| **Command Menu Item** | Quick actions and Cmd+K entries | [Command Menu Items](/developers/extend/apps/layout/command-menu-items) |
## Sandboxing
@@ -85,35 +83,19 @@ your-app/
## Next steps
<CardGroup cols={2}>
<Card
title="Config"
icon="screwdriver-wrench"
href="/developers/extend/apps/config/overview"
>
<Card title="Config" icon="screwdriver-wrench" href="/developers/extend/apps/config/overview">
Application identity, default role, and install hooks.
</Card>
<Card
title="Data"
icon="database"
href="/developers/extend/apps/data/overview"
>
<Card title="Data" icon="database" href="/developers/extend/apps/data/overview">
Objects, fields, and bidirectional relations.
</Card>
<Card title="Logic" icon="bolt" href="/developers/extend/apps/logic/overview">
Logic functions, skills, agents, and OAuth connections.
</Card>
<Card
title="Layout"
icon="table-columns"
href="/developers/extend/apps/layout/overview"
>
<Card title="Layout" icon="table-columns" href="/developers/extend/apps/layout/overview">
Views, navigation, page layouts, front components.
</Card>
<Card
title="Operations"
icon="rocket"
href="/developers/extend/apps/operations/overview"
>
<Card title="Operations" icon="rocket" href="/developers/extend/apps/operations/overview">
CLI, testing, remotes, CI, and publishing your app.
</Card>
</CardGroup>
@@ -1,23 +1,23 @@
---
title: Local Server
description: Manage the local Twenty Docker server — start, stop, upgrade, parallel test instance, and manual SDK setup.
icon: 'server'
icon: "server"
---
## Managing the local server
Use `yarn twenty docker:*` to control the local Twenty container:
| Command | What it does |
| -------------------------------------- | -------------------------------------------- |
| `yarn twenty docker:start` | Start the server (pulls the image if needed) |
| `yarn twenty docker:start --port 3030` | Start on a custom port |
| `yarn twenty docker:stop` | Stop the server (preserves data) |
| `yarn twenty docker:status` | Show URL, version, and login credentials |
| `yarn twenty docker:logs` | Stream server logs |
| `yarn twenty docker:reset` | Wipe data and start fresh |
| `yarn twenty docker:upgrade` | Pull the latest `twenty-app-dev` image |
| `yarn twenty docker:upgrade 2.2.0` | Upgrade to a specific version |
| Command | What it does |
|---------|--------------|
| `yarn twenty docker:start` | Start the server (pulls the image if needed) |
| `yarn twenty docker:start --port 3030` | Start on a custom port |
| `yarn twenty docker:stop` | Stop the server (preserves data) |
| `yarn twenty docker:status` | Show URL, version, and login credentials |
| `yarn twenty docker:logs` | Stream server logs |
| `yarn twenty docker:reset` | Wipe data and start fresh |
| `yarn twenty docker:upgrade` | Pull the latest `twenty-app-dev` image |
| `yarn twenty docker:upgrade 2.2.0` | Upgrade to a specific version |
Data persists across restarts in two Docker volumes (`twenty-app-dev-data` for PostgreSQL, `twenty-app-dev-storage` for files). Use `reset` to wipe everything.
@@ -36,14 +36,14 @@ Verify the running version with `yarn twenty docker:status` (it shows the `APP_V
Pass `--test` to any `docker:*` command to manage a second, fully isolated instance — useful for integration tests or experiments without touching your main dev data:
| Command | What it does |
| ----------------------------------- | ----------------------------------------------- |
| `yarn twenty docker:start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty docker:stop --test` | Stop it |
| `yarn twenty docker:status --test` | Show its status |
| `yarn twenty docker:logs --test` | Stream its logs |
| `yarn twenty docker:reset --test` | Wipe its data |
| `yarn twenty docker:upgrade --test` | Upgrade its image |
| Command | What it does |
|---------|--------------|
| `yarn twenty docker:start --test` | Start the test instance (defaults to port 2021) |
| `yarn twenty docker:stop --test` | Stop it |
| `yarn twenty docker:status --test` | Show its status |
| `yarn twenty docker:logs --test` | Stream its logs |
| `yarn twenty docker:reset --test` | Wipe its data |
| `yarn twenty docker:upgrade --test` | Upgrade its image |
The test instance has its own container (`twenty-app-dev-test`), volumes (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`), and config — it runs alongside your main instance without conflicts. Combine `--test` with `--port` to override 2021.
@@ -68,6 +68,5 @@ Add the script to `package.json`:
You can now run `yarn twenty dev`, `yarn twenty docker:start`, and the rest.
<Note>
Don't install `twenty-sdk` globally — pin it per project so each app uses its
own version.
Don't install `twenty-sdk` globally — pin it per project so each app uses its own version.
</Note>
@@ -1,7 +1,7 @@
---
title: Project Structure
description: What's inside a scaffolded Twenty app — files, folders, and what each one does.
icon: 'folder-tree'
icon: "folder-tree"
---
A new app generated by `npx create-twenty-app` looks like this:
@@ -27,16 +27,14 @@ my-twenty-app/
## Key files
| File / Folder | Purpose |
| ---------------------------------------- | -------------------------------------------------------------- |
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
| `src/default-role.ts` | Default role controlling what your logic functions can access. |
| File / Folder | Purpose |
|---|---|
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
| `src/default-role.ts` | Default role controlling what your logic functions can access. |
| `src/constants/universal-identifiers.ts` | Auto-generated UUIDs and metadata (display name, description). |
| `src/__tests__/` | Integration tests (setup + example test). |
| `public/` | Static assets (images, fonts) served with your app. |
| `src/__tests__/` | Integration tests (setup + example test). |
| `public/` | Static assets (images, fonts) served with your app. |
<Note>
**File organization is up to you.** The folders above are conventions — the
SDK detects entities via AST analysis on `export default defineEntity(...)`
calls regardless of where the file lives.
**File organization is up to you.** The folders above are conventions — the SDK detects entities via AST analysis on `export default defineEntity(...)` calls regardless of where the file lives.
</Note>
@@ -1,6 +1,6 @@
---
title: Quick Start
icon: 'rocket'
icon: "rocket"
description: Create your first Twenty app in minutes.
---
@@ -12,11 +12,11 @@ description: Create your first Twenty app in minutes.
Building a Twenty app has three phases. The scaffolder collapses them into one happy-path command, but each phase is a separate concept — when something fails, knowing which phase you're in tells you what to fix.
| Phase | What you do | Tool | Result |
| ------------------- | ---------------------------------- | ----------------------------- | ----------------------------- |
| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI |
| Phase | What you do | Tool | Result |
|---|---|---|---|
| **1. Scaffold** | Generate the app's source code | `npx create-twenty-app` | A TypeScript project on disk |
| **2. Run a server** | Start a Twenty server to sync into | Docker + `yarn twenty server` | A running Twenty instance |
| **3. Sync** | Live-sync your code to the server | `yarn twenty dev` | Your changes appear in the UI |
---
@@ -45,11 +45,8 @@ The scaffolder offers to start one for you:
- **Yes (recommended)** — pulls the `twentycrm/twenty-app-dev` Docker image and starts it on port `2020`. Make sure Docker is running first.
- **No** — choose this if you already have a Twenty server you want to connect to. You can wire it up later with `yarn twenty remote:add`.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/start-instance.png"
alt="Should start local instance?"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
</div>
Once the server is up, a browser opens for sign-in. Use the pre-seeded demo account:
@@ -57,37 +54,26 @@ Once the server is up, a browser opens for sign-in. Use the pre-seeded demo acco
- **Email:** `tim@apple.dev`
- **Password:** `tim@apple.dev`
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/login.png"
alt="Twenty login screen"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty login screen" />
</div>
Click **Authorize** on the next screen — this gives the CLI access to your workspace.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/authorize.png"
alt="Twenty CLI authorization screen"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI authorization screen" />
</div>
Your terminal will confirm everything is set up.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/scaffolded.png"
alt="App scaffolded successfully"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App scaffolded successfully" />
</div>
**After this phase:** you have a running Twenty server at [http://localhost:2020](http://localhost:2020) with your CLI authorized to sync to it.
<Note>
If Docker isn't installed or running, the scaffolder will tell you the right
start command for your OS. Once Docker is up, you can resume with `yarn twenty
docker:start` — no need to re-scaffold.
If Docker isn't installed or running, the scaffolder will tell you the right start command for your OS. Once Docker is up, you can resume with `yarn twenty docker:start` — no need to re-scaffold.
</Note>
---
@@ -105,38 +91,26 @@ This watches `src/`, rebuilds on every change, and syncs the result to the serve
For more detailed output (build logs, sync requests, error traces), add `--verbose`.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/dev.png"
alt="Dev mode terminal output"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.png" alt="Dev mode terminal output" />
</div>
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer). You should see your app under **Your Apps**.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-1.png"
alt="Your Apps list showing My twenty app"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
</div>
Click **My twenty app** to see its **application registration** — a server-level record describing your app (name, identifier, OAuth credentials, source). One registration can be installed across multiple workspaces on the same server.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-2.png"
alt="Application registration details"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
</div>
Click **View installed app** to see the workspace install. The **About** tab shows version and management options.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-3.png"
alt="Installed app"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app" />
</div>
**After this phase:** you have a live development loop. Edit any file in `src/` and it appears in the UI.
@@ -149,68 +123,52 @@ Pass `--once` to run a single build + sync and exit — same pipeline, no watche
yarn twenty dev --once
```
| Command | Behavior | When to use |
| ------------------------ | ------------------------------------------------------------- | ---------------------------------------------------- |
| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. |
| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. |
| Command | Behavior | When to use |
|---------|----------|-------------|
| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. |
| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. |
Both modes need an authenticated remote.
### Dev mode options
| Flag | Description |
| ----------------------- | --------------------------------------------------------------------- |
| `--once` | Build and sync once, then exit. |
| `--debounceMs <ms>` | Set the file-change debounce delay in milliseconds (default: `2000`). |
| `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. |
| Flag | Description |
|------|-------------|
| `--once` | Build and sync once, then exit. |
| `--debounceMs <ms>` | Set the file-change debounce delay in milliseconds (default: `2000`). |
| `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. |
## What you can build
Apps are composed of **entities** — each defined as a TypeScript file with a single `export default`:
| Entity | What it does |
| ---------------------- | ----------------------------------------------------------------------------------- |
| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields |
| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) |
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
| **Page layouts** | Custom record detail pages with tabs and widgets |
| Entity | What it does |
|--------|-------------|
| **Objects & Fields** | Custom data models (Post Card, Invoice, etc.) with typed fields |
| **Logic functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
| **Front components** | React components that render inside Twenty's UI (side panel, widgets, command menu) |
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
| **Page layouts** | Custom record detail pages with tabs and widgets |
Full reference: [Concepts](/developers/extend/apps/getting-started/concepts).
## Next steps
<CardGroup cols={2}>
<Card
title="Config"
icon="screwdriver-wrench"
href="/developers/extend/apps/config/overview"
>
<Card title="Config" icon="screwdriver-wrench" href="/developers/extend/apps/config/overview">
Application identity, default role, install hooks, public assets.
</Card>
<Card
title="Data"
icon="database"
href="/developers/extend/apps/data/overview"
>
<Card title="Data" icon="database" href="/developers/extend/apps/data/overview">
Objects, fields, and bidirectional relations.
</Card>
<Card title="Logic" icon="bolt" href="/developers/extend/apps/logic/overview">
Logic functions, skills, agents, and OAuth connections.
</Card>
<Card
title="Layout"
icon="table-columns"
href="/developers/extend/apps/layout/overview"
>
<Card title="Layout" icon="table-columns" href="/developers/extend/apps/layout/overview">
Views, navigation, page layouts, front components.
</Card>
<Card
title="Operations"
icon="rocket"
href="/developers/extend/apps/operations/overview"
>
<Card title="Operations" icon="rocket" href="/developers/extend/apps/operations/overview">
CLI, testing, remotes, CI, and publishing your app.
</Card>
</CardGroup>
@@ -1,7 +1,7 @@
---
title: Scaffolding
description: Generate entity files interactively with yarn twenty dev:add — objects, fields, views, logic functions, and more.
icon: 'wand-magic-sparkles'
icon: "wand-magic-sparkles"
---
Instead of creating entity files by hand, use the interactive scaffolder:
@@ -22,18 +22,18 @@ yarn twenty dev:add frontComponent
## Available entity types
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Entity type | Command | Generated file |
|-------------|---------|----------------|
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
## What the scaffolder generates
@@ -1,7 +1,7 @@
---
title: Troubleshooting
description: Common first-run issues — Docker, Node version, Yarn, dependencies.
icon: 'wrench'
icon: "wrench"
---
- **Docker errors** — Make sure Docker Desktop (or the daemon) is running before `yarn twenty docker:start`. The error message will show the right start command for your OS.
@@ -1,7 +1,7 @@
---
title: Command Menu Items
description: Surface front components as quick actions and command menu (Cmd+K) entries with defineCommandMenuItem.
icon: 'terminal'
icon: "terminal"
---
A **command menu item** is the bridge between the user and a [front component](/developers/extend/apps/layout/front-components). It registers the component in Twenty's command menu (Cmd+K) and, optionally, as a pinned quick-action button in the top-right corner of the page.
@@ -22,17 +22,17 @@ export default defineCommandMenuItem({
## Configuration fields
| Field | Required | Description |
| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `universalIdentifier` | Yes | Stable unique ID for the command |
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens |
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) |
| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) |
| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) |
| Field | Required | Description |
|-------|----------|-------------|
| `universalIdentifier` | Yes | Stable unique ID for the command |
| `label` | Yes | Full label shown in the command menu (Cmd+K) |
| `frontComponentUniversalIdentifier` | Yes | The `universalIdentifier` of the front component this command opens |
| `shortLabel` | No | Shorter label displayed on the pinned quick-action button |
| `icon` | No | Icon name displayed next to the label (e.g. `'IconBolt'`, `'IconSend'`) |
| `isPinned` | No | When `true`, shows the command as a quick-action button in the top-right corner of the page |
| `availabilityType` | No | Controls where the command appears: `'GLOBAL'` (always available), `'RECORD_SELECTION'` (only when records are selected), or `'FALLBACK'` (shown when no other commands match) |
| `availabilityObjectUniversalIdentifier` | No | Restrict the command to pages of a specific object type (e.g. only on Company records) |
| `conditionalAvailabilityExpression` | No | A boolean expression that dynamically controls visibility (see below) |
## Headless commands
@@ -85,7 +85,10 @@ The `conditionalAvailabilityExpression` field lets you control when a command is
```ts src/command-menu-items/bulk-update.command-menu-item.ts
import { defineCommandMenuItem } from 'twenty-sdk/define';
import { objectPermissions, everyEquals } from 'twenty-sdk/front-component';
import {
objectPermissions,
everyEquals,
} from 'twenty-sdk/front-component';
export default defineCommandMenuItem({
universalIdentifier: '...',
@@ -104,38 +107,38 @@ export default defineCommandMenuItem({
These represent the current state of the page:
| Variable | Type | Description |
| ------------------------------ | --------- | ---------------------------------------------------------------- |
| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel |
| `numberOfSelectedRecords` | `number` | Number of currently selected records |
| `isSelectAll` | `boolean` | Whether "select all" is active |
| `selectedRecords` | `array` | The selected record objects |
| `favoriteRecordIds` | `array` | IDs of favorited records |
| `objectPermissions` | `object` | Permissions for the current object type |
| `targetObjectReadPermissions` | `object` | Read permissions for the target object |
| `targetObjectWritePermissions` | `object` | Write permissions for the target object |
| `featureFlags` | `object` | Active feature flags |
| `objectMetadataItem` | `object` | Metadata of the current object type |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter |
| Variable | Type | Description |
|----------|------|-------------|
| `pageType` | `string` | Current page type (e.g. `'RecordIndexPage'`, `'RecordShowPage'`) |
| `isInSidePanel` | `boolean` | Whether the component is rendered in a side panel |
| `numberOfSelectedRecords` | `number` | Number of currently selected records |
| `isSelectAll` | `boolean` | Whether "select all" is active |
| `selectedRecords` | `array` | The selected record objects |
| `favoriteRecordIds` | `array` | IDs of favorited records |
| `objectPermissions` | `object` | Permissions for the current object type |
| `targetObjectReadPermissions` | `object` | Read permissions for the target object |
| `targetObjectWritePermissions` | `object` | Write permissions for the target object |
| `featureFlags` | `object` | Active feature flags |
| `objectMetadataItem` | `object` | Metadata of the current object type |
| `hasAnySoftDeleteFilterOnView` | `boolean` | Whether the current view has a soft-delete filter |
### Operators
Combine variables into boolean expressions:
| Operator | Description |
| ----------------------------------- | ----------------------------------------------------------------- |
| `isDefined(value)` | `true` if the value is not null/undefined |
| `isNonEmptyString(value)` | `true` if the value is a non-empty string |
| `includes(array, value)` | `true` if the array contains the value |
| `includesEvery(array, prop, value)` | `true` if every item's property includes the value |
| `every(array, prop)` | `true` if the property is truthy on every item |
| `everyDefined(array, prop)` | `true` if the property is defined on every item |
| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item |
| `some(array, prop)` | `true` if the property is truthy on at least one item |
| `someDefined(array, prop)` | `true` if the property is defined on at least one item |
| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item |
| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item |
| `none(array, prop)` | `true` if the property is falsy on every item |
| `noneDefined(array, prop)` | `true` if the property is undefined on every item |
| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item |
| Operator | Description |
|----------|-------------|
| `isDefined(value)` | `true` if the value is not null/undefined |
| `isNonEmptyString(value)` | `true` if the value is a non-empty string |
| `includes(array, value)` | `true` if the array contains the value |
| `includesEvery(array, prop, value)` | `true` if every item's property includes the value |
| `every(array, prop)` | `true` if the property is truthy on every item |
| `everyDefined(array, prop)` | `true` if the property is defined on every item |
| `everyEquals(array, prop, value)` | `true` if the property equals the value on every item |
| `some(array, prop)` | `true` if the property is truthy on at least one item |
| `someDefined(array, prop)` | `true` if the property is defined on at least one item |
| `someEquals(array, prop, value)` | `true` if the property equals the value on at least one item |
| `someNonEmptyString(array, prop)` | `true` if the property is a non-empty string on at least one item |
| `none(array, prop)` | `true` if the property is falsy on every item |
| `noneDefined(array, prop)` | `true` if the property is undefined on every item |
| `noneEquals(array, prop, value)` | `true` if the property does not equal the value on any item |
@@ -1,7 +1,7 @@
---
title: Front Components
description: Build React components that render inside Twenty's UI with sandboxed isolation.
icon: 'window-maximize'
icon: "window-maximize"
---
Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe.
@@ -13,7 +13,7 @@ Front components can render in two locations within Twenty:
- **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
- **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget.
A front component on its own isn't reachable from the UI — you need to _surface_ it. The two ways to do that are:
A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are:
- **Pair it with a [command menu item](/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action.
- **Embed it as a widget in a [page layout](/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard.
@@ -58,24 +58,21 @@ export default defineCommandMenuItem({
After syncing with `yarn twenty dev` (or running a one-shot `yarn twenty dev --once`), the quick action appears in the top-right corner of the page:
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/quick-action.png"
alt="Quick action button in the top-right corner"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
</div>
Click it to render the component inline.
## Configuration fields
| Field | Required | Description |
| --------------------- | -------- | ------------------------------------------------------------ |
| `universalIdentifier` | Yes | Stable unique ID for this component |
| `component` | Yes | A React component function |
| `name` | No | Display name |
| `description` | No | Description of what the component does |
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
| Field | Required | Description |
|-------|----------|-------------|
| `universalIdentifier` | Yes | Stable unique ID for this component |
| `component` | Yes | A React component function |
| `name` | No | Display name |
| `description` | No | Description of what the component does |
| `isHeadless` | No | Set to `true` if the component has no visible UI (see below) |
## Placing a front component on a page
@@ -98,10 +95,7 @@ const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({
message: `Tracking record ${recordId}`,
variant: 'info',
});
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
@@ -237,13 +231,13 @@ export default defineFrontComponent({
Available hooks:
| Hook | Returns | Description |
| --------------------------------------------- | ------------------ | ---------------------------------------------------------- |
| `useUserId()` | `string` or `null` | The current user's ID |
| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) |
| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead |
| `useFrontComponentId()` | `string` | This component instance's ID |
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
| Hook | Returns | Description |
|------|---------|-------------|
| `useUserId()` | `string` or `null` | The current user's ID |
| `useSelectedRecordIds()` | `string[]` | All selected record IDs (empty array if none selected) |
| `useRecordId()` | `string` or `null` | **Deprecated.** Use `useSelectedRecordIds()` instead |
| `useFrontComponentId()` | `string` | This component instance's ID |
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
## Application variables
@@ -254,8 +248,7 @@ import { defineFrontComponent } from 'twenty-sdk/define';
import { getApplicationVariable } from 'twenty-sdk/front-component';
const Greeting = () => {
const recipientName =
getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
return <p>Hello, {recipientName}!</p>;
};
@@ -268,33 +261,29 @@ export default defineFrontComponent({
```
<Warning>
Secret variables (`isSecret: true`) are **not** exposed to front components.
They are only available in [logic
functions](/developers/extend/apps/logic/logic-functions), which run
server-side. This prevents sensitive values like API keys from being sent to
the browser.
Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser.
</Warning>
The following system variables are always available via `process.env`:
| Variable | Description |
| ------------------------- | ------------------------------------------- |
| `TWENTY_API_URL` | Base URL of the Twenty API |
| Variable | Description |
|----------|-------------|
| `TWENTY_API_URL` | Base URL of the Twenty API |
| `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role |
## Host communication API
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
| Function | Description |
| ----------------------------------------------- | ----------------------------- |
| Function | Description |
|----------|-------------|
| `navigate(to, params?, queryParams?, options?)` | Navigate to a page in the app |
| `openSidePanelPage(params)` | Open a side panel |
| `closeSidePanel()` | Close the side panel |
| `openCommandConfirmationModal(params)` | Show a confirmation dialog |
| `enqueueSnackbar(params)` | Show a toast notification |
| `unmountFrontComponent()` | Unmount the component |
| `updateProgress(progress)` | Update a progress indicator |
| `openSidePanelPage(params)` | Open a side panel |
| `closeSidePanel()` | Close the side panel |
| `openCommandConfirmationModal(params)` | Show a confirmation dialog |
| `enqueueSnackbar(params)` | Show a toast notification |
| `unmountFrontComponent()` | Unmount the component |
| `updateProgress(progress)` | Update a progress indicator |
Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
@@ -347,10 +336,7 @@ Use `useSelectedRecordIds()` to handle multiple selected records. This is useful
```tsx src/front-components/bulk-export.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import {
useSelectedRecordIds,
numberOfSelectedRecords,
} from 'twenty-sdk/front-component';
import { useSelectedRecordIds, numberOfSelectedRecords } from 'twenty-sdk/front-component';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
import { CoreApiClient } from 'twenty-sdk/clients';
@@ -1,16 +1,13 @@
---
title: Navigation Menu Items
description: Add custom entries to the workspace sidebar — links to saved views or external URLs.
icon: 'bars'
icon: "bars"
---
A **navigation menu item** is an entry in the left sidebar. Use `defineNavigationMenuItem()` to ship custom sidebar links — typically one per [view](/developers/extend/apps/layout/views) you ship — or to point at external URLs.
```ts src/navigation-menu-items/example-navigation-menu-item.ts
import {
defineNavigationMenuItem,
NavigationMenuItemType,
} from 'twenty-sdk/define';
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from '../views/example-view';
export default defineNavigationMenuItem({
@@ -28,21 +25,18 @@ export default defineNavigationMenuItem({
- `type` determines what the menu item links to. Each type pairs with a specific identifier field:
| Type | What it does | Required field |
| ------------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------- |
| `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Opens an external URL | `link` |
| `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` |
| Type | What it does | Required field |
|------|--------------|----------------|
| `NavigationMenuItemType.VIEW` | Opens a saved view | `viewUniversalIdentifier` |
| `NavigationMenuItemType.LINK` | Opens an external URL | `link` |
| `NavigationMenuItemType.FOLDER` | Groups nested items under a label | `name` (and child items reference the folder via `folderUniversalIdentifier`) |
| `NavigationMenuItemType.OBJECT` | Opens an object's default index page | `targetObjectUniversalIdentifier` |
| `NavigationMenuItemType.PAGE_LAYOUT` | Opens a standalone page layout | `pageLayoutUniversalIdentifier` |
- `position` controls ordering in the sidebar.
- `icon` and `color` are optional and customize how the entry looks.
- `folderUniversalIdentifier` is also available on any item to nest it inside a `FOLDER`-type parent.
<Note>
**Common pitfall:** creating an object without an associated view + navigation
menu item makes that object invisible to users. Unless it's a
technical/internal object, every custom object should have a default view
*and* a sidebar entry pointing at it.
**Common pitfall:** creating an object without an associated view + navigation menu item makes that object invisible to users. Unless it's a technical/internal object, every custom object should have a default view *and* a sidebar entry pointing at it.
</Note>
@@ -1,7 +1,7 @@
---
title: Overview
description: Place your app inside Twenty's UI — sidebar entries, saved views, record page tabs, and sandboxed React components.
icon: 'table-columns'
icon: "table-columns"
---
A Twenty app's **layout layer** is everything the user sees: where the app surfaces in the sidebar, which list views it ships, how its record detail pages are arranged, and which custom React components render inside those pages.
@@ -29,48 +29,28 @@ A Twenty app's **layout layer** is everything the user sees: where the app surfa
<Card title="Views" icon="list" href="/developers/extend/apps/layout/views">
`defineView` — saved list configurations: visible columns, filters, groups.
</Card>
<Card
title="Navigation Menu Items"
icon="bars"
href="/developers/extend/apps/layout/navigation-menu-items"
>
`defineNavigationMenuItem` — sidebar entries pointing at views or external
URLs.
<Card title="Navigation Menu Items" icon="bars" href="/developers/extend/apps/layout/navigation-menu-items">
`defineNavigationMenuItem` — sidebar entries pointing at views or external URLs.
</Card>
<Card
title="Page Layouts"
icon="table-columns"
href="/developers/extend/apps/layout/page-layouts"
>
`definePageLayout` and `definePageLayoutTab` — tabs and widgets on a
record's detail page.
<Card title="Page Layouts" icon="table-columns" href="/developers/extend/apps/layout/page-layouts">
`definePageLayout` and `definePageLayoutTab` — tabs and widgets on a record's detail page.
</Card>
<Card
title="Front Components"
icon="window-maximize"
href="/developers/extend/apps/layout/front-components"
>
`defineFrontComponent` — sandboxed React components that render inside
Twenty.
<Card title="Front Components" icon="window-maximize" href="/developers/extend/apps/layout/front-components">
`defineFrontComponent` — sandboxed React components that render inside Twenty.
</Card>
<Card
title="Command Menu Items"
icon="terminal"
href="/developers/extend/apps/layout/command-menu-items"
>
`defineCommandMenuItem` — register front components as Cmd+K entries and
quick actions.
<Card title="Command Menu Items" icon="terminal" href="/developers/extend/apps/layout/command-menu-items">
`defineCommandMenuItem` — register front components as Cmd+K entries and quick actions.
</Card>
</CardGroup>
## Where the app surfaces
| Surface | What it controls | Entity |
| --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- |
| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` |
| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` |
| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` |
| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` |
| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` |
| Surface | What it controls | Entity |
|---------|------------------|--------|
| **Sidebar** | A custom entry linking to a saved view or external URL | `defineNavigationMenuItem` |
| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` |
| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` |
| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` |
| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` |
Front components run inside an isolated Web Worker using Remote DOM — they render _natively_ in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API.
Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API.
@@ -1,14 +1,14 @@
---
title: Page Layouts
description: Customize record detail pages — tabs, widgets, and where front components render — using definePageLayout and definePageLayoutTab.
icon: 'table-columns'
icon: "table-columns"
---
A **page layout** controls how a record's detail page is arranged: which tabs appear and what widgets they contain. Use `definePageLayout()` to declare a layout for an object you own, or `definePageLayoutTab()` to add a single tab to a layout that already exists (yours or a standard Twenty one).
| Use case | Entity |
| ---------------------------------------------------------------------- | --------------------- |
| Define the entire layout for a record page on an object you own | `definePageLayout` |
| Use case | Entity |
|----------|--------|
| Define the entire layout for a record page on an object you own | `definePageLayout` |
| Add one tab to an existing layout (your own object, or a standard one) | `definePageLayoutTab` |
## definePageLayout
@@ -1,7 +1,7 @@
---
title: Views
description: Ship pre-configured saved views — column order, filters, groups — for objects in your app.
icon: 'list'
icon: "list"
---
A **view** is a saved configuration for how records of an object are displayed: which fields appear, their order, whether they're visible, and any filters or groups applied. Use `defineView()` to ship pre-configured views with your app — typically a default index view for each custom object you create.
@@ -1,7 +1,7 @@
---
title: Logic Functions
description: Define server-side TypeScript functions with HTTP, cron, and database event triggers.
icon: 'bolt'
icon: "bolt"
---
Logic functions are server-side TypeScript functions that run on the Twenty platform. They can be triggered by HTTP requests, cron schedules, or database events — and can also be exposed as tools for AI agents.
@@ -51,12 +51,11 @@ export default defineLogicFunction({
```
Available trigger types:
- **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
- **cron**: Runs your function on a schedule using a CRON expression.
- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function.
> e.g. `person.updated`, `*.created`, `company.*`
> e.g. `person.updated`, `*.created`, `company.*`
<Note>
You can also manually execute a function using the CLI:
@@ -74,7 +73,6 @@ You can watch logs with:
```bash filename="Terminal"
yarn twenty dev:function:logs
```
</Note>
#### Route trigger payload
@@ -96,16 +94,17 @@ const handler = async (event: RoutePayload) => {
The `RoutePayload` type has the following structure:
| Property | Type | Description | Example |
| ---------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below |
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | |
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Raw request path | |
| Property | Type | Description | Example |
|----------|------|-------------|---------|
| `headers` | `Record<string, string \| undefined>` | HTTP headers (only those listed in `forwardedRequestHeaders`) | see section below |
| `queryStringParameters` | `Record<string, string \| undefined>` | Query string parameters (multiple values joined with commas) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }`|
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
| `body` | `object \| null` | Parsed request body (JSON) | `{ id: 1 }` -> `{ id: 1 }` |
| `rawBody` | `string \| undefined` | Original UTF-8 request body, before JSON parsing. Useful for verifying HMAC-style webhook signatures (e.g. GitHub's `X-Hub-Signature-256`, Stripe). `undefined` when the runtime did not preserve it. | |
| `isBase64Encoded` | `boolean` | Whether the body is base64 encoded | |
| `requestContext.http.method` | `string` | HTTP method (GET, POST, PUT, PATCH, DELETE) | |
| `requestContext.http.path` | `string` | Raw request path | |
#### forwardedRequestHeaders
@@ -139,8 +138,7 @@ const handler = async (event: RoutePayload) => {
```
<Note>
Header names are normalized to lowercase. Access them using lowercase keys
(e.g., `event.headers['content-type']`).
Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
</Note>
#### Database event trigger payload
@@ -163,34 +161,35 @@ type Person = {
The payload includes:
| Property | Description |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `name` | Event name, such as `person.updated`. |
| `workspaceId` | Workspace where the event happened. |
| `objectMetadata` | Metadata for the object that changed. |
| `recordId` | Id of the changed record. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Actor fields when the event was caused by a workspace user. |
| `properties` | Record data for the event, with `before`, `after`, `diff`, and `updatedFields` depending on the operation. |
| Property | Description |
|----------|-------------|
| `name` | Event name, such as `person.updated`. |
| `workspaceId` | Workspace where the event happened. |
| `objectMetadata` | Metadata for the object that changed. |
| `recordId` | Id of the changed record. |
| `userId`, `userWorkspaceId`, `workspaceMemberId` | Actor fields when the event was caused by a workspace user. |
| `properties` | Record data for the event, with `before`, `after`, `diff`, and `updatedFields` depending on the operation. |
| Event | Record data |
| ------------------ | -------------------------------------------------------------------------------------------------------------- |
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
| Event | Record data |
|-------|-------------|
| `person.created` | `event.properties.after` |
| `person.updated` | `event.properties.before`, `event.properties.after`, `event.properties.diff`, `event.properties.updatedFields` |
| `person.destroyed` | `event.properties.before` |
For soft deletes, `.deleted` follows the update-style shape because the record's `deletedAt` field changes.
For permanent deletes, use `.destroyed`.
<Note>
`databaseEventTriggerSettings.updatedFields` filters which update events
trigger the function. `event.properties.updatedFields` tells you which fields
actually changed on the current event.
`databaseEventTriggerSettings.updatedFields` filters which update events trigger the function.
`event.properties.updatedFields` tells you which fields actually changed on the current event.
</Note>
Created event example:
```ts
type PersonCreatedEvent = DatabaseEventPayload<ObjectRecordCreateEvent<Person>>;
type PersonCreatedEvent = DatabaseEventPayload<
ObjectRecordCreateEvent<Person>
>;
const handler = async (event: PersonCreatedEvent) => {
const person = event.properties.after;
@@ -205,7 +204,9 @@ const handler = async (event: PersonCreatedEvent) => {
Updated event example:
```ts
type PersonUpdatedEvent = DatabaseEventPayload<ObjectRecordUpdateEvent<Person>>;
type PersonUpdatedEvent = DatabaseEventPayload<
ObjectRecordUpdateEvent<Person>
>;
const handler = async (event: PersonUpdatedEvent) => {
const { before, after, diff, updatedFields } = event.properties;
@@ -318,29 +319,24 @@ export default defineLogicFunction({
```
<Note>
**Write a good `description`.** AI agents rely on the function's `description`
field to decide when to use the tool. Be specific about what the tool does and
when it should be called.
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
</Note>
</Accordion>
</AccordionGroup>
<Note>
**Install hooks** — pre-install and post-install handlers — share this runtime
but are declared with their own define functions and don't take trigger
settings. See [Install Hooks](/developers/extend/apps/config/install-hooks)
for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`.
**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`.
</Note>
## Typed API clients (twenty-client-sdk)
The `twenty-client-sdk` package provides two typed GraphQL clients for interacting with the Twenty API from your logic functions and front components.
| Client | Import | Endpoint | Generated? |
| ------------------- | ---------------------------- | ---------------------------------------------- | ---------------------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built |
| Client | Import | Endpoint | Generated? |
|--------|--------|----------|------------|
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql` — workspace data (records, objects) | Yes, at dev/build time |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata` — workspace config, file uploads | No, ships pre-built |
<AccordionGroup>
<Accordion title="CoreApiClient" description="Query and mutate workspace data (records, objects)">
@@ -385,10 +381,7 @@ const { createCompany } = await client.mutation({
The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
<Note>
**CoreApiClient is generated at dev/build time.** If you use it without
running `yarn twenty dev` or `yarn twenty dev:build` first, it throws an
error. The generation happens automatically — the CLI introspects your
workspace's GraphQL schema and generates a typed client using `@genql/cli`.
**CoreApiClient is generated at dev/build time.** If you use it without running `yarn twenty dev` or `yarn twenty dev:build` first, it throws an error. The generation happens automatically — the CLI introspects your workspace's GraphQL schema and generates a typed client using `@genql/cli`.
</Note>
#### Using CoreSchema for type annotations
@@ -457,37 +450,36 @@ const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| Parameter | Type | Description |
| ---------------------------------- | -------- | --------------------------------------------------------------- |
| `fileBuffer` | `Buffer` | The raw file contents |
| `filename` | `string` | The name of the file (used for storage and display) |
| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) |
| Parameter | Type | Description |
|-----------|------|-------------|
| `fileBuffer` | `Buffer` | The raw file contents |
| `filename` | `string` | The name of the file (used for storage and display) |
| `contentType` | `string` | MIME type (defaults to `application/octet-stream` if omitted) |
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
Key points:
- Uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed.
- The returned `url` is a signed URL you can use to access the uploaded file.
</Accordion>
</AccordionGroup>
<Note>
When your code runs on Twenty (logic functions or front components), the platform injects credentials as environment variables:
- `TWENTY_API_URL` — Base URL of the Twenty API
- `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role
You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role declared with `defineApplicationRole()` (or referenced via `defaultRoleUniversalIdentifier` in `application-config.ts`).
- `TWENTY_API_URL` — Base URL of the Twenty API
- `TWENTY_APP_ACCESS_TOKEN` — Short-lived key scoped to your application's default function role
You do **not** need to pass these to the clients — they read from `process.env` automatically. The API key's permissions are determined by the role declared with `defineApplicationRole()` (or referenced via `defaultRoleUniversalIdentifier` in `application-config.ts`).
</Note>
@@ -1,10 +1,10 @@
---
title: Overview
description: Server-side TypeScript that runs inside Twenty — triggered by HTTP routes, cron schedules, database events, AI tools, or workflow actions.
icon: 'bolt'
icon: "bolt"
---
A Twenty app's **logic layer** is the code that _runs_ — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services.
A Twenty app's **logic layer** is the code that *runs* — server-side TypeScript handlers reacting to HTTP requests, cron schedules, and record changes; AI skills and agents that live inside the workspace; and OAuth connections that let your functions act on a user's behalf in third-party services.
```text
┌─ HTTP route ──┐
@@ -25,27 +25,14 @@ A Twenty app's **logic layer** is the code that _runs_ — server-side TypeScrip
## In this section
<CardGroup cols={2}>
<Card
title="Logic Functions"
icon="bolt"
href="/developers/extend/apps/logic/logic-functions"
>
<Card title="Logic Functions" icon="bolt" href="/developers/extend/apps/logic/logic-functions">
The core building block — trigger types, payloads, and the typed API client.
</Card>
<Card
title="Skills & Agents"
icon="robot"
href="/developers/extend/apps/logic/skills-and-agents"
>
<Card title="Skills & Agents" icon="robot" href="/developers/extend/apps/logic/skills-and-agents">
Reusable AI agent instructions and assistants with custom system prompts.
</Card>
<Card
title="Connections"
icon="plug"
href="/developers/extend/apps/logic/connections"
>
OAuth credentials your app holds for third-party services — Linear, GitHub,
Slack, and more.
<Card title="Connections" icon="plug" href="/developers/extend/apps/logic/connections">
OAuth credentials your app holds for third-party services — Linear, GitHub, Slack, and more.
</Card>
</CardGroup>
@@ -53,18 +40,16 @@ A Twenty app's **logic layer** is the code that _runs_ — server-side TypeScrip
A logic function picks one or more triggers — every entry below is a separate field on `defineLogicFunction()`:
| Trigger | When it runs | Setting |
| ------------------- | -------------------------------------------------- | ------------------------------- |
| **HTTP route** | A request hits your `/s/<path>` endpoint | `httpRouteTriggerSettings` |
| **Cron** | A CRON expression matches | `cronTriggerSettings` |
| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` |
| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` |
| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` |
| Trigger | When it runs | Setting |
|---------|--------------|---------|
| **HTTP route** | A request hits your `/s/<path>` endpoint | `httpRouteTriggerSettings` |
| **Cron** | A CRON expression matches | `cronTriggerSettings` |
| **Database event** | A workspace record is created, updated, or deleted | `databaseEventTriggerSettings` |
| **AI tool** | A Twenty AI feature decides to call your function | `toolTriggerSettings` |
| **Workflow action** | A workflow step invokes your function | `workflowActionTriggerSettings` |
Functions run sandboxed in isolated Node.js processes and access the workspace through a typed API client scoped to the role declared on [`defineApplication()`](/developers/extend/apps/config/application).
<Note>
**Install-time hooks** — code that runs before or after the install — share
this runtime but use their own define functions and live under [Config →
Install Hooks](/developers/extend/apps/config/install-hooks).
**Install-time hooks** — code that runs before or after the install — share this runtime but use their own define functions and live under [Config → Install Hooks](/developers/extend/apps/config/install-hooks).
</Note>
@@ -1,12 +1,11 @@
---
title: Skills & Agents
description: Define AI skills and agents for your app.
icon: 'robot'
icon: "robot"
---
<Warning>
Skills and agents are currently in alpha. The feature works but is still
evolving.
Skills and agents are currently in alpha. The feature works but is still evolving.
</Warning>
Apps can define AI capabilities that live inside the workspace — reusable skill instructions and agents with custom system prompts.
@@ -23,8 +22,7 @@ export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description:
'Guides the AI agent through a structured sales outreach process',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
@@ -35,7 +33,6 @@ export default defineSkill({
```
Key points:
- `name` is a unique identifier string for the skill (kebab-case recommended).
- `label` is the human-readable display name shown in the UI.
- `content` contains the skill instructions — this is the text the AI agent uses.
@@ -54,16 +51,13 @@ export default defineAgent({
universalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
name: 'sales-assistant',
label: 'Sales Assistant',
description:
'Helps the sales team draft outreach emails and research prospects',
description: 'Helps the sales team draft outreach emails and research prospects',
icon: 'IconRobot',
prompt:
'You are a helpful sales assistant. Help users with their questions and tasks.',
prompt: 'You are a helpful sales assistant. Help users with their questions and tasks.',
});
```
Key points:
- `name` is the unique identifier string for the agent (kebab-case recommended).
- `label` is the display name shown in the UI.
- `prompt` is the system prompt that defines the agent's behavior.
@@ -1,7 +1,7 @@
---
title: CLI
description: yarn twenty commands for executing functions, streaming logs, managing app installations, and switching remotes.
icon: 'terminal'
icon: "terminal"
---
Beyond `dev`, `dev:build`, `dev:add`, and `dev:typecheck`, the `yarn twenty` CLI provides commands for executing functions, viewing logs, and managing app installations.
@@ -40,9 +40,7 @@ yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
This is different from `yarn twenty docker:logs`, which shows the Docker
container logs. `yarn twenty dev:function:logs` shows your app's function
execution logs from the Twenty server.
This is different from `yarn twenty docker:logs`, which shows the Docker container logs. `yarn twenty dev:function:logs` shows your app's function execution logs from the Twenty server.
</Note>
## Uninstalling an app (`yarn twenty app:uninstall`)
@@ -1,10 +1,10 @@
---
title: Overview
description: Build, test, and ship your app — CLI commands, integration tests, CI, and publishing to a server or to npm.
icon: 'rocket'
icon: "rocket"
---
The **operations layer** is everything you do _to_ your app rather than _with_ it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace.
The **operations layer** is everything you do *to* your app rather than *with* it: invoking CLI commands, running integration tests against a real Twenty server, configuring CI, and shipping releases — either as a tarball deployed to a single server or as an npm package listed in the marketplace.
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
@@ -17,25 +17,13 @@ The **operations layer** is everything you do _to_ your app rather than _with_ i
## In this section
<CardGroup cols={2}>
<Card
title="CLI"
icon="terminal"
href="/developers/extend/apps/operations/cli"
>
<Card title="CLI" icon="terminal" href="/developers/extend/apps/operations/cli">
`yarn twenty` reference — exec, logs, uninstall, remotes.
</Card>
<Card
title="Testing"
icon="flask"
href="/developers/extend/apps/operations/testing"
>
<Card title="Testing" icon="flask" href="/developers/extend/apps/operations/testing">
Vitest setup, integration tests, type checking, CI workflow.
</Card>
<Card
title="Publishing"
icon="upload"
href="/developers/extend/apps/operations/publishing"
>
<Card title="Publishing" icon="upload" href="/developers/extend/apps/operations/publishing">
Build, deploy a tarball, publish to npm, install.
</Card>
</CardGroup>
@@ -1,6 +1,6 @@
---
title: Publishing
icon: 'upload'
icon: "upload"
description: Distribute your Twenty app to the marketplace or deploy it internally.
---
@@ -72,10 +72,7 @@ To release an update:
3. Workspaces that have the app installed will see the upgrade available in their settings
<Note>
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is
allowed, and a final release like `1.0.0` is correctly recognized as higher
than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver
string.
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
</Note>
{/* TODO: add screenshot of the Upgrade button */}
@@ -97,11 +94,11 @@ If your app uses a feature introduced in a specific Twenty server version (for e
The value is a standard [semver range](https://github.com/npm/node-semver#ranges). Common patterns:
| Range | Meaning |
| ---------------- | ---------------------------------------- |
| `>=2.3.0` | Any server from 2.3.0 onward |
| Range | Meaning |
|-------|---------|
| `>=2.3.0` | Any server from 2.3.0 onward |
| `>=2.3.0 <3.0.0` | 2.3.0 or later, but below the next major |
| `^2.3.0` | Same as `>=2.3.0 <3.0.0` |
| `^2.3.0` | Same as `>=2.3.0 <3.0.0` |
**What happens at deploy and install time:**
@@ -110,9 +107,7 @@ The value is a standard [semver range](https://github.com/npm/node-semver#ranges
- If the server has no `APP_VERSION` configured, the check is skipped.
<Note>
The server is the authoritative check — it validates `engines.twenty` on both
tarball upload and workspace install. If you deploy a tarball out-of-band or
install from the marketplace, the server still enforces compatibility.
The server is the authoritative check — it validates `engines.twenty` on both tarball upload and workspace install. If you deploy a tarball out-of-band or install from the marketplace, the server still enforces compatibility.
</Note>
## Automated CI/CD (scaffolded workflows)
@@ -149,16 +144,13 @@ Deploys your app to a configured Twenty server on every push to `main`, and opti
**Required configuration:**
| Setting | Where | Purpose |
| ----------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. |
| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. |
| Setting | Where | Purpose |
|---------|-------|---------|
| `TWENTY_DEPLOY_URL` | `env` in `cd.yml` (defaults to `http://localhost:3000`) | The Twenty server to deploy to. Change this to your real server URL before first use. |
| `TWENTY_DEPLOY_API_KEY` | GitHub repo **Settings → Secrets and variables → Actions** | API key with deploy permission on the target server. |
<Note>
The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder —
it will not reach anything from a GitHub-hosted runner. Update it to your
server's public URL (or use a self-hosted runner with network access) before
enabling CD.
The default `TWENTY_DEPLOY_URL` of `http://localhost:3000` is a placeholder — it will not reach anything from a GitHub-hosted runner. Update it to your server's public URL (or use a self-hosted runner with network access) before enabling CD.
</Note>
**Triggering a preview deploy from a PR:**
@@ -196,7 +188,10 @@ export default defineApplication({
displayName: 'My App',
description: 'A great app',
logoUrl: 'public/logo.png',
screenshots: ['public/screenshot-1.png', 'public/screenshot-2.png'],
screenshots: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
@@ -207,9 +202,7 @@ See the [defineApplication accordion](/developers/extend/apps/config/application
The marketplace renders `screenshots` in a fixed `8:5` container (for example, `1600×1000 px`).
<Note>
Screenshots of any aspect ratio are displayed in full and are never cropped,
but anything significantly taller or narrower than `8:5` will show empty bands
on the sides.
Screenshots of any aspect ratio are displayed in full and are never cropped, but anything significantly taller or narrower than `8:5` will show empty bands on the sides.
</Note>
### Publish
@@ -239,11 +232,7 @@ yarn twenty dev:catalog-sync
The metadata shown in the marketplace comes from your `defineApplication()` config — fields like `displayName`, `description`, `author`, `category`, `logoUrl`, `screenshots`, `aboutDescription`, `websiteUrl`, and `termsUrl`.
<Note>
If your app does not define an `aboutDescription` in `defineApplication()`,
the marketplace will automatically use your package's `README.md` from npm as
the about page content. This means you can maintain a single README for both
npm and the Twenty marketplace. If you want a different description in the
marketplace, explicitly set `aboutDescription`.
If your app does not define an `aboutDescription` in `defineApplication()`, the marketplace will automatically use your package's `README.md` from npm as the about page content. This means you can maintain a single README for both npm and the Twenty marketplace. If you want a different description in the marketplace, explicitly set `aboutDescription`.
</Note>
### CI publishing
@@ -267,7 +256,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
node-version: "24"
registry-url: https://registry.npmjs.org
- run: yarn install --immutable
- run: npx twenty dev:build
@@ -278,11 +267,7 @@ jobs:
For other CI systems (GitLab CI, CircleCI, etc.), the same three commands apply: `yarn install`, `yarn twenty dev:build`, then `npm publish` from `.twenty/output`.
<Note>
**npm provenance** is optional but recommended. Publishing with `--provenance`
adds a trust badge to your npm listing, letting users verify the package was
built from a specific commit in a public CI pipeline. See the [npm provenance
docs](https://docs.npmjs.com/generating-provenance-statements) for setup
instructions.
**npm provenance** is optional but recommended. Publishing with `--provenance` adds a trust badge to your npm listing, letting users verify the package was built from a specific commit in a public CI pipeline. See the [npm provenance docs](https://docs.npmjs.com/generating-provenance-statements) for setup instructions.
</Note>
## Installing apps
@@ -306,5 +291,4 @@ The server enforces semver versioning on install, mirroring the rules on deploy:
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
To install a newer version, deploy or publish it first, then re-run `yarn twenty app:install`.
</Note>
@@ -1,7 +1,7 @@
---
title: Testing
description: Vitest setup, integration tests against a real Twenty server, type checking, and CI with GitHub Actions.
icon: 'flask'
icon: "flask"
---
The SDK provides programmatic APIs that let you build, deploy, install, and uninstall your app from test code. Combined with [Vitest](https://vitest.dev/) and the typed API clients, you can write integration tests that verify your app works end-to-end against a real Twenty server.
@@ -125,19 +125,15 @@ beforeAll(async () => {
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,
},
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
defaultRemote: 'local',
},
null,
2,
),
defaultRemote: 'local',
}, null, 2),
);
});
```
@@ -146,11 +142,11 @@ beforeAll(async () => {
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 |
| 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`.
@@ -10,36 +10,29 @@ Twenty was built to be developer-friendly, offering powerful APIs that adapt to
## Developer-First Approach
Twenty generates APIs specifically for your data model:
- **No long IDs required**: Use your object and field names directly in endpoints
- **Standard and custom objects treated equally**: Your custom objects get the same API treatment as built-in ones
- **Dedicated endpoints**: Each object and field gets its own API endpoint
- **Custom documentation**: Generated specifically for your workspace's data model
<Note>
Your personalized API documentation is available under **Settings → API &
Webhooks** after creating an API key. Since Twenty generates APIs that match
your custom data model, the documentation is unique to your workspace.
Your personalized API documentation is available under **Settings → API & Webhooks** after creating an API key. Since Twenty generates APIs that match your custom data model, the documentation is unique to your workspace.
</Note>
## The Two API Types
### Core API
Accessed on `/rest/` or `/graphql/`
Work with your actual **records** (the data):
- Create, read, update, delete People, Companies, Opportunities, etc.
- Query and filter data
- Manage record relationships
### Metadata API
Accessed on `/rest/metadata/` or `/metadata/`
Manage your **workspace and data model**:
- Create, modify, or delete objects and fields
- Configure workspace settings
- Define relationships between objects
@@ -48,19 +41,19 @@ Manage your **workspace and data model**:
Both Core and Metadata APIs are available in REST and GraphQL formats:
| Format | Available Operations |
| ----------- | ---------------------------------------------------------- |
| **REST** | CRUD, batch operations, upserts |
| Format | Available Operations |
|--------|---------------------|
| **REST** | CRUD, batch operations, upserts |
| **GraphQL** | Same + **batch upserts**, relationship queries in one call |
Choose based on your needs — both formats access the same data.
## API Endpoints
| Environment | Base URL |
| --------------- | ------------------------- |
| **Cloud** | `https://api.twenty.com/` |
| **Self-Hosted** | `https://{your-domain}/` |
| Environment | Base URL |
|-------------|----------|
| **Cloud** | `https://api.twenty.com/` |
| **Self-Hosted** | `https://{your-domain}/` |
## Authentication
@@ -83,8 +76,7 @@ Authorization: Bearer YOUR_API_KEY
<VimeoEmbed videoId="928786722" title="Creating API key" />
<Warning>
Your API key grants access to sensitive data. Don't share it with untrusted
services. If compromised, disable it immediately and generate a new one.
Your API key grants access to sensitive data. Don't share it with untrusted services. If compromised, disable it immediately and generate a new one.
</Warning>
### Assign a Role to an API Key
@@ -127,12 +119,10 @@ The playground reflects your custom objects and fields, so documentation is alwa
## Batch Operations
Both REST and GraphQL support batch operations:
- **Batch size**: Up to 60 records per request
- **Operations**: Create, update, delete multiple records
**GraphQL-only features:**
- **Batch Upsert**: Create or update in one call
- Use plural object names (e.g., `CreateCompanies` instead of `CreateCompany`)
@@ -140,12 +130,12 @@ Both REST and GraphQL support batch operations:
API requests are throttled to ensure platform stability:
| Limit | Value |
| -------------- | -------------------- |
| **Requests** | 100 calls per minute |
| **Batch size** | 60 records per call |
| Limit | Value |
|-------|-------|
| **Requests** | 100 calls per minute |
| **Batch size** | 60 records per call |
<Tip>
Use batch operations to maximize throughput — process up to 60 records in a
single API call instead of making individual requests.
Use batch operations to maximize throughput — process up to 60 records in a single API call instead of making individual requests.
</Tip>
@@ -5,6 +5,7 @@ description: Receive real-time notifications when events occur in your CRM.
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Webhooks push data to your systems in real-time when events occur in Twenty — no polling required. Use them to keep external systems in sync, trigger automations, or send alerts.
## Create a Webhook
@@ -28,11 +29,11 @@ The webhook activates immediately and starts sending notifications.
Twenty sends webhooks for these event types:
| Event | Example |
| ------------------ | ---------------------------------------------------------- |
| **Record Created** | `person.created`, `company.created`, `note.created` |
| Event | Example |
|-------|---------|
| **Record Created** | `person.created`, `company.created`, `note.created` |
| **Record Updated** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Record Deleted** | `person.deleted`, `company.deleted` |
| **Record Deleted** | `person.deleted`, `company.deleted` |
All event types are sent to your webhook URL. Event filtering may be added in future releases.
@@ -55,15 +56,14 @@ Each webhook sends an HTTP POST with a JSON body:
}
```
| Field | Description |
| ----------- | ------------------------------------------------ |
| `event` | What happened (e.g., `person.created`) |
| `data` | The full record that was created/updated/deleted |
| `timestamp` | When the event occurred (UTC) |
| Field | Description |
|-------|-------------|
| `event` | What happened (e.g., `person.created`) |
| `data` | The full record that was created/updated/deleted |
| `timestamp` | When the event occurred (UTC) |
<Note>
Respond with a **2xx HTTP status** (200-299) to acknowledge receipt. Non-2xx
responses are logged as delivery failures.
Respond with a **2xx HTTP status** (200-299) to acknowledge receipt. Non-2xx responses are logged as delivery failures.
</Note>
## Webhook Validation
@@ -72,10 +72,10 @@ Twenty signs each webhook request for security. Validate signatures to ensure re
### Headers
| Header | Description |
| ---------------------------- | --------------------- |
| Header | Description |
|--------|-------------|
| `X-Twenty-Webhook-Signature` | HMAC SHA256 signature |
| `X-Twenty-Webhook-Timestamp` | Request timestamp |
| `X-Twenty-Webhook-Timestamp` | Request timestamp |
### Validation Steps
@@ -87,27 +87,27 @@ Twenty signs each webhook request for security. Validate signatures to ensure re
### Example (Node.js)
```javascript
const crypto = require('crypto');
const crypto = require("crypto");
const timestamp = req.headers['x-twenty-webhook-timestamp'];
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = 'your-webhook-secret';
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.createHmac("sha256", secret)
.update(stringToSign)
.digest('hex');
.digest("hex");
const isValid = expectedSignature === req.headers['x-twenty-webhook-signature'];
const isValid = expectedSignature === req.headers["x-twenty-webhook-signature"];
```
## Webhooks vs Workflows
| Method | Direction | Use Case |
| ---------------------------- | --------- | ---------------------------------------------------------- |
| **Webhooks** | OUT | Automatically notify external systems of any record change |
| **Workflow + HTTP Request** | OUT | Send data out with custom logic (filters, transformations) |
| **Workflow Webhook Trigger** | IN | Receive data into Twenty from external systems |
| Method | Direction | Use Case |
|--------|-----------|----------|
| **Webhooks** | OUT | Automatically notify external systems of any record change |
| **Workflow + HTTP Request** | OUT | Send data out with custom logic (filters, transformations) |
| **Workflow Webhook Trigger** | IN | Receive data into Twenty from external systems |
For receiving external data, see [Set Up a Webhook Trigger](/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).
@@ -2,7 +2,6 @@
title: Extend
description: Extend Twenty's functionality with APIs, webhooks, and custom apps.
---
<Frame>
<img src="/images/user-guide/integrations/plug.png" alt="AI" />
</Frame>
@@ -26,11 +25,7 @@ Twenty is designed to be extensible. Use our APIs, webhooks, and app framework t
<Card title="Webhooks" icon="bell" href="/developers/extend/webhooks">
Get notified of events in real-time
</Card>
<Card
title="Apps"
icon="puzzle-piece"
href="/developers/extend/apps/getting-started"
>
<Card title="Apps" icon="puzzle-piece" href="/developers/extend/apps/getting-started">
Build customizations as code
</Card>
</CardGroup>
@@ -1,6 +1,6 @@
---
title: OAuth
icon: 'key'
icon: "key"
description: Authorization code flow with PKCE and client credentials for server-to-server access.
---
@@ -8,12 +8,12 @@ Twenty implements OAuth 2.0 with authorization code + PKCE for user-facing apps
## When to Use OAuth
| Scenario | Auth Method |
| --------------------------------------- | -------------------------------------------------------------------------------- |
| Internal scripts, automation | [API Key](/developers/extend/api#authentication) |
| External app acting on behalf of a user | **OAuth — Authorization Code** |
| Server-to-server, no user context | **OAuth — Client Credentials** |
| Twenty App with UI extensions | [Apps](/developers/extend/apps/getting-started) (OAuth is handled automatically) |
| Scenario | Auth Method |
|----------|-------------|
| Internal scripts, automation | [API Key](/developers/extend/api#authentication) |
| External app acting on behalf of a user | **OAuth — Authorization Code** |
| Server-to-server, no user context | **OAuth — Client Credentials** |
| Twenty App with UI extensions | [Apps](/developers/extend/apps/getting-started) (OAuth is handled automatically) |
## Register a Client
@@ -43,15 +43,15 @@ Content-Type: application/json
```
<Warning>
Store the `client_secret` securely — it cannot be retrieved later.
Store the `client_secret` securely — it cannot be retrieved later.
</Warning>
## Scopes
| Scope | Access |
| --------- | ---------------------------------------------------- |
| `api` | Full read/write access to the Core and Metadata APIs |
| `profile` | Read the authenticated user's profile information |
| Scope | Access |
|-------|--------|
| `api` | Full read/write access to the Core and Metadata APIs |
| `profile` | Read the authenticated user's profile information |
Request scopes as a space-separated string: `scope=api profile`
@@ -72,15 +72,15 @@ GET /oauth/authorize?
code_challenge_method=S256
```
| Parameter | Required | Description |
| ----------------------- | ----------- | ------------------------------------------------------------ |
| `client_id` | Yes | Your registered client ID |
| `response_type` | Yes | Must be `code` |
| `redirect_uri` | Yes | Must match a registered redirect URI |
| `scope` | No | Space-separated scopes (defaults to `api`) |
| `state` | Recommended | Random string to prevent CSRF attacks |
| `code_challenge` | Recommended | PKCE challenge (SHA-256 hash of verifier, base64url-encoded) |
| `code_challenge_method` | Recommended | Must be `S256` when using PKCE |
| Parameter | Required | Description |
|-----------|----------|-------------|
| `client_id` | Yes | Your registered client ID |
| `response_type` | Yes | Must be `code` |
| `redirect_uri` | Yes | Must match a registered redirect URI |
| `scope` | No | Space-separated scopes (defaults to `api`) |
| `state` | Recommended | Random string to prevent CSRF attacks |
| `code_challenge` | Recommended | PKCE challenge (SHA-256 hash of verifier, base64url-encoded) |
| `code_challenge_method` | Recommended | Must be `S256` when using PKCE |
The user sees a consent screen and approves or denies access.
@@ -166,24 +166,24 @@ This returns all endpoints, supported grant types, scopes, and capabilities —
## API Endpoints Summary
| Endpoint | Purpose |
| ----------------------------------------- | --------------------------- |
| `/.well-known/oauth-authorization-server` | Server metadata discovery |
| `/oauth/register` | Dynamic client registration |
| `/oauth/authorize` | User authorization |
| `/oauth/token` | Token exchange and refresh |
| Endpoint | Purpose |
|----------|---------|
| `/.well-known/oauth-authorization-server` | Server metadata discovery |
| `/oauth/register` | Dynamic client registration |
| `/oauth/authorize` | User authorization |
| `/oauth/token` | Token exchange and refresh |
| Environment | Base URL |
| --------------- | ------------------------ |
| **Cloud** | `https://api.twenty.com` |
| **Self-Hosted** | `https://{your-domain}` |
| Environment | Base URL |
|-------------|----------|
| **Cloud** | `https://api.twenty.com` |
| **Self-Hosted** | `https://{your-domain}` |
## OAuth vs API Keys
| | API Keys | OAuth |
| ------------------ | ----------------------- | -------------------------------------- |
| **Setup** | Generate in Settings | Register a client, implement flow |
| **User context** | None (workspace-level) | Specific user's permissions |
| **Best for** | Scripts, internal tools | External apps, multi-user integrations |
| **Token rotation** | Manual | Automatic via refresh tokens |
| **Scoped access** | Full API access | Granular via scopes |
| | API Keys | OAuth |
|-|----------|-------|
| **Setup** | Generate in Settings | Register a client, implement flow |
| **User context** | None (workspace-level) | Specific user's permissions |
| **Best for** | Scripts, internal tools | External apps, multi-user integrations |
| **Token rotation** | Manual | Automatic via refresh tokens |
| **Scoped access** | Full API access | Granular via scopes |
@@ -1,6 +1,6 @@
---
title: Webhooks
icon: 'satellite-dish'
icon: "satellite-dish"
description: Get notified when records change — HTTP POST to your endpoint on every create, update, or delete.
---
@@ -29,11 +29,11 @@ The webhook activates immediately and starts sending notifications.
Twenty sends webhooks for these event types:
| Event | Example |
| ------------------ | ---------------------------------------------------------- |
| **Record Created** | `person.created`, `company.created`, `note.created` |
| Event | Example |
|-------|---------|
| **Record Created** | `person.created`, `company.created`, `note.created` |
| **Record Updated** | `person.updated`, `company.updated`, `opportunity.updated` |
| **Record Deleted** | `person.deleted`, `company.deleted` |
| **Record Deleted** | `person.deleted`, `company.deleted` |
All event types are sent to your webhook URL. Event filtering may be added in future releases.
@@ -56,15 +56,14 @@ Each webhook sends an HTTP POST with a JSON body:
}
```
| Field | Description |
| ----------- | ------------------------------------------------ |
| `event` | What happened (e.g., `person.created`) |
| `data` | The full record that was created/updated/deleted |
| `timestamp` | When the event occurred (UTC) |
| Field | Description |
|-------|-------------|
| `event` | What happened (e.g., `person.created`) |
| `data` | The full record that was created/updated/deleted |
| `timestamp` | When the event occurred (UTC) |
<Note>
Respond with a **2xx HTTP status** (200-299) to acknowledge receipt. Non-2xx
responses are logged as delivery failures.
Respond with a **2xx HTTP status** (200-299) to acknowledge receipt. Non-2xx responses are logged as delivery failures.
</Note>
## Webhook Validation
@@ -73,10 +72,10 @@ Twenty signs each webhook request for security. Validate signatures to ensure re
### Headers
| Header | Description |
| ---------------------------- | --------------------- |
| Header | Description |
|--------|-------------|
| `X-Twenty-Webhook-Signature` | HMAC SHA256 signature |
| `X-Twenty-Webhook-Timestamp` | Request timestamp |
| `X-Twenty-Webhook-Timestamp` | Request timestamp |
### Validation Steps
@@ -88,31 +87,31 @@ Twenty signs each webhook request for security. Validate signatures to ensure re
### Example (Node.js)
```javascript
const crypto = require('crypto');
const crypto = require("crypto");
const timestamp = req.headers['x-twenty-webhook-timestamp'];
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const secret = 'your-webhook-secret';
const secret = "your-webhook-secret";
const stringToSign = `${timestamp}:${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.createHmac("sha256", secret)
.update(stringToSign)
.digest('hex');
.digest("hex");
const receivedSignature = req.headers['x-twenty-webhook-signature'];
const receivedSignature = req.headers["x-twenty-webhook-signature"];
const isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
```
## Webhooks vs Workflows
| Method | Direction | Use Case |
| ---------------------------- | --------- | ---------------------------------------------------------- |
| **Webhooks** | OUT | Automatically notify external systems of any record change |
| **Workflow + HTTP Request** | OUT | Send data out with custom logic (filters, transformations) |
| **Workflow Webhook Trigger** | IN | Receive data into Twenty from external systems |
| Method | Direction | Use Case |
|--------|-----------|----------|
| **Webhooks** | OUT | Automatically notify external systems of any record change |
| **Workflow + HTTP Request** | OUT | Send data out with custom logic (filters, transformations) |
| **Workflow Webhook Trigger** | IN | Receive data into Twenty from external systems |
For receiving external data, see [Set Up a Webhook Trigger](/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger).
@@ -3,7 +3,7 @@ title: Developers
description: Build apps, use the API, self-host, or contribute to the codebase.
---
import { CardTitle } from '/snippets/card-title.mdx';
import { CardTitle } from "/snippets/card-title.mdx"
<CardGroup cols={3}>
<Card href="/developers/extend/apps/getting-started" img="/images/user-guide/halftone/dev-apps.png">
@@ -11,21 +11,15 @@ import { CardTitle } from '/snippets/card-title.mdx';
Extend Twenty with custom objects, server-side logic, UI components, and AI agents — all as TypeScript packages.
</Card>
<Card
href="/developers/extend/api"
img="/images/user-guide/halftone/dev-api.png"
>
<CardTitle>API</CardTitle>
REST and GraphQL APIs, webhooks, and OAuth.
</Card>
<Card href="/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API</CardTitle>
REST and GraphQL APIs, webhooks, and OAuth.
</Card>
<Card
href="/developers/self-host/capabilities/docker-compose"
img="/images/user-guide/halftone/dev-self-host.png"
>
<CardTitle>Self-Host</CardTitle>
Run Twenty on your own infrastructure.
</Card>
<Card href="/developers/self-host/capabilities/docker-compose" img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>Self-Host</CardTitle>
Run Twenty on your own infrastructure.
</Card>
<Card href="/developers/contribute/capabilities/local-setup" img="/images/user-guide/halftone/dev-contribute.png">
<CardTitle>Contribute</CardTitle>
@@ -1,6 +1,6 @@
---
title: Other methods
icon: 'cloud'
icon: "cloud"
---
<Warning>
@@ -17,6 +17,7 @@ Deploy Twenty on servers using Coolify. (official image on Coolify will be avail
[Coolify documentation](https://coolify.io/docs/get-started/introduction)
### EasyPanel
Deploy Twenty on EasyPanel with the community maintained template below.
@@ -44,3 +45,5 @@ Deploy Twenty on Sealos with the community maintained template below.
## Others
Please feel free to Open a PR to add more Cloud Provider options.
@@ -1,12 +1,11 @@
---
title: Docker Compose
icon: 'docker'
icon: "docker"
---
<Warning>
Docker containers are for production hosting or self-hosting. For
contributing, please check the [Local
Setup](/developers/contribute/capabilities/local-setup).
Docker containers are for production hosting or self-hosting. For contributing, please check the [Local Setup](/developers/contribute/capabilities/local-setup).
</Warning>
## Overview
@@ -25,61 +24,55 @@ See [Setup Environment Variables](/developers/self-host/capabilities/setup) for
## Option 1: One-line script
Install the latest stable version of Twenty with a single command:
```bash
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
To install a specific version or branch:
```bash
VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```
- Replace x.y.z with the desired version number.
- Replace branch-name with the name of the branch you want to install.
## Option 2: Manual steps
Follow these steps for a manual setup.
### Step 1: Set Up the Environment File
1. **Create the .env File**
Copy the example environment file to a new .env file in your working directory:
```bash
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
Copy the example environment file to a new .env file in your working directory:
```bash
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
```
2. **Generate an Encryption Key**
Run the following command to generate a unique random string:
Run the following command to generate a unique random string:
```bash
openssl rand -base64 32
```
**Important:** Keep this value secret / do not share it. Losing `ENCRYPTION_KEY` means losing access to every secret stored in the database (OAuth tokens, application variables, TOTP secrets, etc.).
```bash
openssl rand -base64 32
```
**Important:** Keep this value secret / do not share it. Losing `ENCRYPTION_KEY` means losing access to every secret stored in the database (OAuth tokens, application variables, TOTP secrets, etc.).
3. **Update the `.env`**
Replace the placeholder value in your .env file with the generated token:
Replace the placeholder value in your .env file with the generated token:
```ini
ENCRYPTION_KEY=random_string
```
```ini
ENCRYPTION_KEY=random_string
```
See the [Key rotation guide](/developers/self-host/capabilities/key-rotation) for instructions on rotating it without downtime.
See the [Key rotation guide](/developers/self-host/capabilities/key-rotation) for instructions on rotating it without downtime.
4. **Set the Postgres Password**
Update the `PG_DATABASE_PASSWORD` value in the .env file with a strong password without special characters.
Update the `PG_DATABASE_PASSWORD` value in the .env file with a strong password without special characters.
```ini
PG_DATABASE_PASSWORD=my_strong_password
```
```ini
PG_DATABASE_PASSWORD=my_strong_password
```
### Step 2: Obtain the Docker Compose File
@@ -92,7 +85,6 @@ curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/ref
### Step 3: Launch the Application
Start the Docker containers:
```bash
docker compose up -d
```
@@ -102,7 +94,6 @@ docker compose up -d
If you host twentyCRM on your own computer, open your browser and navigate to [http://localhost:3000](http://localhost:3000).
If you host it on a server, check that the server is running and that everything is ok with
```bash
curl http://localhost:3000
```
@@ -116,8 +107,8 @@ By default, Twenty runs on `localhost` at port `3000`. To access it via an exter
#### Understanding `SERVER_URL`
- **Protocol:** Use `http` or `https` depending on your setup.
- Use `http` if you haven't set up SSL.
- Use `https` if you have SSL configured.
- Use `http` if you haven't set up SSL.
- Use `https` if you have SSL configured.
- **Domain/IP:** This is the domain name or IP address where your application is accessible.
- **Port:** Include the port number if you're not using the default ports (`80` for `http`, `443` for `https`).
@@ -132,80 +123,77 @@ We strongly recommend setting up Twenty behind a reverse proxy with SSL terminat
#### Configuring `SERVER_URL`
1. **Determine Your Access URL**
- **Without Reverse Proxy (Direct Access):**
- **Without Reverse Proxy (Direct Access):**
If you're accessing the application directly without a reverse proxy:
If you're accessing the application directly without a reverse proxy:
```ini
SERVER_URL=http://your-domain-or-ip:3000
```
```ini
SERVER_URL=http://your-domain-or-ip:3000
```
- **With Reverse Proxy (Standard Ports):**
- **With Reverse Proxy (Standard Ports):**
If you're using a reverse proxy like Nginx or Traefik and have SSL configured:
```ini
SERVER_URL=https://your-domain-or-ip
```
If you're using a reverse proxy like Nginx or Traefik and have SSL configured:
- **With Reverse Proxy (Custom Ports):**
```ini
SERVER_URL=https://your-domain-or-ip
```
- **With Reverse Proxy (Custom Ports):**
If you're using non-standard ports:
```ini
SERVER_URL=https://your-domain-or-ip:custom-port
```
If you're using non-standard ports:
```ini
SERVER_URL=https://your-domain-or-ip:custom-port
````
2. **Update the `.env` File**
Open your `.env` file and update the `SERVER_URL`:
Open your `.env` file and update the `SERVER_URL`:
```ini
SERVER_URL=http(s)://your-domain-or-ip:your-port
```
```ini
SERVER_URL=http(s)://your-domain-or-ip:your-port
```
**Examples:**
- Direct access without SSL:
```ini
SERVER_URL=http://123.45.67.89:3000
```
- Access via domain with SSL:
```ini
SERVER_URL=https://mytwentyapp.com
```
**Examples:**
- Direct access without SSL:
```ini
SERVER_URL=http://123.45.67.89:3000
```
- Access via domain with SSL:
```ini
SERVER_URL=https://mytwentyapp.com
```
3. **Restart the Application**
For changes to take effect, restart the Docker containers:
```bash
docker compose down
docker compose up -d
```
For changes to take effect, restart the Docker containers:
```bash
docker compose down
docker compose up -d
```
#### Considerations
- **Reverse Proxy Configuration:**
Ensure your reverse proxy forwards requests to the correct internal port (`3000` by default). Configure SSL termination and any necessary headers.
Ensure your reverse proxy forwards requests to the correct internal port (`3000` by default). Configure SSL termination and any necessary headers.
- **Firewall Settings:**
Open necessary ports in your firewall to allow external access.
Open necessary ports in your firewall to allow external access.
- **Consistency:**
The `SERVER_URL` must match how users access your application in their browsers.
The `SERVER_URL` must match how users access your application in their browsers.
#### Persistence
- **Data Volumes:**
The Docker Compose configuration uses volumes to persist data for the database and server storage.
The Docker Compose configuration uses volumes to persist data for the database and server storage.
- **Stateless Environments:**
If deploying to a stateless environment (e.g., certain cloud services), configure external storage to persist data.
If deploying to a stateless environment (e.g., certain cloud services), configure external storage to persist data.
## Backup and Restore
@@ -228,19 +216,16 @@ Add to your crontab (`crontab -e`):
### Restore from Backup
1. Stop the application:
```bash
docker compose stop twenty-server twenty-front
```
2. Restore the database:
```bash
docker exec -i twenty-postgres psql -U postgres twenty < backup_20240115.sql
```
3. Restart services:
```bash
docker compose up -d
```
@@ -255,3 +240,4 @@ docker compose up -d
## Troubleshooting
If you encounter any problem, check [Troubleshooting](/developers/self-host/capabilities/troubleshooting) for solutions.
@@ -1,6 +1,6 @@
---
title: Key rotation
icon: 'rotate'
icon: "rotate"
---
Twenty has two independent key families:
@@ -17,11 +17,9 @@ Each key carries a `publicKey` (kept indefinitely so it can verify previously is
### Rotate the current key
- **Manual** — **Settings → Admin Panel → Signing keys → Revoke** on the current row. Revoking wipes its encrypted private material and demotes it; the next sign call automatically mints a fresh ES256 keypair as the new current. Tokens signed under any other (non-revoked) `kid` keep verifying until they expire.
- **Enterprise (automatic)** — a daily cron (`'15 3 * * *'` UTC) issues a new current key once the existing one has been current for `SIGNING_KEY_ROTATION_DAYS` (default `90`). The previous key is _not_ revoked, so tokens signed under it keep verifying. Register it once with `yarn command:prod cron:register:all`.
- **Enterprise (automatic)** — a daily cron (`'15 3 * * *'` UTC) issues a new current key once the existing one has been current for `SIGNING_KEY_ROTATION_DAYS` (default `90`). The previous key is *not* revoked, so tokens signed under it keep verifying. Register it once with `yarn command:prod cron:register:all`.
<Note>
The Enterprise cron and `SIGNING_KEY_ROTATION_DAYS` ship in v2.6+.
</Note>
<Note>The Enterprise cron and `SIGNING_KEY_ROTATION_DAYS` ship in v2.6+.</Note>
### Revoke a key (leak / emergency only)
@@ -29,9 +27,7 @@ Each key carries a `publicKey` (kept indefinitely so it can verify previously is
## Rotate `ENCRYPTION_KEY`
<Note>
The `secret-encryption:rotate` command described below ships in v2.6+.
</Note>
<Note>The `secret-encryption:rotate` command described below ships in v2.6+.</Note>
Every encrypted value is wrapped as `enc:v2:<keyId>:<payload>`, where `<keyId>` is an 8-hex prefix derived from the raw key. Rotation is online and resumable.
@@ -43,18 +39,16 @@ Every encrypted value is wrapped as `enc:v2:<keyId>:<payload>`, where `<keyId>`
```
New writes use the new key, existing rows still decrypt via the fallback.
3. **Re-encrypt existing rows**:
```bash
docker exec -it {server_container} yarn command:prod secret-encryption:rotate
```
The command walks six sites (`connected-account-tokens`, `application-variable`, `application-registration-variable`, `signing-key-private-keys`, `sensitive-config-storage`, `totp-secrets`). A SQL filter skips rows already on the new `<keyId>`, so the command is idempotent: interrupt and re-run as needed. Exits non-zero if any row fails — re-run to retry.
| Flag | Description |
| ---------------------- | -------------------------------------------------- |
| `-s, --site <site>` | Limit to a single site. |
| `-b, --batch-size <n>` | Rows per batch (default `200`, max `5000`). |
| `-d, --dry-run` | Decrypt + re-encrypt in memory, skip the `UPDATE`. |
| Flag | Description |
| --- | --- |
| `-s, --site <site>` | Limit to a single site. |
| `-b, --batch-size <n>` | Rows per batch (default `200`, max `5000`). |
| `-d, --dry-run` | Decrypt + re-encrypt in memory, skip the `UPDATE`. |
4. **Drop the fallback** once `--dry-run` shows zero remaining rows: remove `FALLBACK_ENCRYPTION_KEY` and restart.
@@ -1,14 +1,12 @@
---
title: Setup
icon: 'gear'
icon: "gear"
---
# Configuration Management
<Warning>
**First time installing?** Follow the [Docker Compose installation
guide](/developers/self-host/capabilities/docker-compose) to get Twenty
running, then return here for configuration.
**First time installing?** Follow the [Docker Compose installation guide](/developers/self-host/capabilities/docker-compose) to get Twenty running, then return here for configuration.
</Warning>
Twenty offers **two configuration modes** to suit different deployment needs:
@@ -29,15 +27,10 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
4. Changes take effect immediately (within 15 seconds for multi-container deployments)
<Warning>
**Multi-Container Deployments:** When using database configuration
(`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), both server and worker containers
read from the same database. Admin panel changes affect both automatically,
eliminating the need to duplicate environment variables between containers
(except for infrastructure variables).
**Multi-Container Deployments:** When using database configuration (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), both server and worker containers read from the same database. Admin panel changes affect both automatically, eliminating the need to duplicate environment variables between containers (except for infrastructure variables).
</Warning>
**What you can configure through the admin panel:**
- **Authentication** - Google/Microsoft OAuth, password settings
- **Email** - SMTP settings, templates, verification
- **Storage** - S3 configuration, local storage paths
@@ -52,17 +45,16 @@ Each variable is documented with descriptions in your admin panel at **Settings
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and secrets (`ENCRYPTION_KEY`, `FALLBACK_ENCRYPTION_KEY`) can only be configured via `.env` file.
[Complete technical reference →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
</Warning>
## Encryption keys
Twenty uses two env-only encryption keys:
| Variable | Purpose | Required |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `ENCRYPTION_KEY` | Primary key used to encrypt secrets at rest (OAuth tokens, application variables, signing-key private keys, TOTP secrets, sensitive config values). | Yes for new installs (legacy installs may instead rely on `APP_SECRET` — see below) |
| `FALLBACK_ENCRYPTION_KEY` | Verification-only key. Set during a rotation to the _previous_ `ENCRYPTION_KEY` so existing rows remain decryptable. | Only during rotation |
| Variable | Purpose | Required |
| --- | --- | --- |
| `ENCRYPTION_KEY` | Primary key used to encrypt secrets at rest (OAuth tokens, application variables, signing-key private keys, TOTP secrets, sensitive config values). | Yes for new installs (legacy installs may instead rely on `APP_SECRET` — see below) |
| `FALLBACK_ENCRYPTION_KEY` | Verification-only key. Set during a rotation to the *previous* `ENCRYPTION_KEY` so existing rows remain decryptable. | Only during rotation |
For backward compatibility, if `ENCRYPTION_KEY` is unset, Twenty falls back to `APP_SECRET` for at-rest encryption — matching the legacy behaviour for older deployments. New installs should always set a dedicated `ENCRYPTION_KEY`.
@@ -108,7 +100,6 @@ DEFAULT_SUBDOMAIN=app # default value
Enable multi-workspace mode for SaaS-like deployments where multiple independent teams need their own workspaces on the same Twenty instance.
**Key differences from single-workspace mode:**
- Multiple workspaces can be created on the same instance
- Each workspace gets its own subdomain (e.g., `sales.your-domain.com`, `marketing.your-domain.com`)
- Users sign up and log in at `{DEFAULT_SUBDOMAIN}.your-domain.com` (e.g., `app.your-domain.com`)
@@ -116,9 +107,7 @@ Enable multi-workspace mode for SaaS-like deployments where multiple independent
- Workspace-specific settings like subdomain and custom domain become available in workspace settings
<Warning>
**Environment-only setting:** `IS_MULTIWORKSPACE_ENABLED` can only be
configured via `.env` file and requires a restart. It cannot be changed
through the admin panel.
**Environment-only setting:** `IS_MULTIWORKSPACE_ENABLED` can only be configured via `.env` file and requires a restart. It cannot be changed through the admin panel.
</Warning>
### DNS Configuration for Multi-Workspace
@@ -148,7 +137,6 @@ When enabled, only users with `canAccessFullAdminPanel` can create additional wo
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing one
3. Enable these APIs:
- [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
- [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
- [People API](https://console.cloud.google.com/apis/library/people.googleapis.com)
@@ -174,14 +162,11 @@ When enabled, only users with `canAccessFullAdminPanel` can create additional wo
- `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
<Warning>
**Environment-only mode:** If you set
`IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env`
file instead.
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
</Warning>
**Required scopes** (automatically configured):
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
- `https://www.googleapis.com/auth/calendar.events`
- `https://www.googleapis.com/auth/gmail.readonly`
- `https://www.googleapis.com/auth/profile.emails.read`
@@ -195,10 +180,7 @@ Under [OAuth consent screen](https://console.cloud.google.com/apis/credentials/c
## Microsoft 365 Integration
<Warning>
Users must have a [Microsoft 365
Licence](https://admin.microsoft.com/Adminportal/Home) to be able to use the
Calendar and Messaging API. They will not be able to sync their account on
Twenty without one.
Users must have a [Microsoft 365 Licence](https://admin.microsoft.com/Adminportal/Home) to be able to use the Calendar and Messaging API. They will not be able to sync their account on Twenty without one.
</Warning>
### Create a project in Microsoft Azure
@@ -223,7 +205,6 @@ Note: "Mail.ReadWrite" and "Mail.Send" are only mandatory if you want to send em
### Authorized redirect URIs
You need to add the following redirect URIs to your project:
- `https://{your-domain}/auth/microsoft/redirect` if you want to use Microsoft SSO
- `https://{your-domain}/auth/microsoft-apis/get-access-token`
@@ -241,15 +222,11 @@ You need to add the following redirect URIs to your project:
- `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
<Warning>
**Environment-only mode:** If you set
`IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env`
file instead.
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
</Warning>
### Configure scopes
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
- 'openid'
- 'email'
- 'profile'
@@ -269,7 +246,6 @@ Add your test users to the "Users and groups" section.
After configuring Gmail, Google Calendar, or Microsoft 365 integrations, you need to start the background jobs that sync data.
Register the following recurring jobs in your worker container:
```bash
# from your worker container
yarn command:prod cron:messaging:messages-import
@@ -291,56 +267,48 @@ yarn command:prod cron:workflow:automated-cron-trigger
<ArticleTab>
You will need to provision an [App Password](https://support.google.com/accounts/answer/185833).
- EMAIL_DRIVER=smtp
- EMAIL_SMTP_HOST=smtp.gmail.com
- EMAIL_SMTP_PORT=465
- EMAIL_SMTP_USER=gmail_email_address
- EMAIL_SMTP_PASSWORD='gmail_app_password'
</ArticleTab>
<ArticleTab>
Keep in mind that if you have 2FA enabled, you will need to provision an [App Password](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
- EMAIL_DRIVER=smtp
- EMAIL_SMTP_HOST=smtp.office365.com
- EMAIL_SMTP_PORT=587
- EMAIL_SMTP_USER=office365_email_address
- EMAIL_SMTP_PASSWORD='office365_password'
</ArticleTab>
<ArticleTab>
**smtp4dev** is a fake SMTP email server for development and testing.
- Run the smtp4dev image: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
- Access the smtp4dev ui here: [http://localhost:8090](http://localhost:8090)
- Set the following variables:
You will need to provision an [App Password](https://support.google.com/accounts/answer/185833).
- EMAIL_DRIVER=smtp
- EMAIL_SMTP_HOST=localhost
- EMAIL_SMTP_PORT=2525
- EMAIL_SMTP_HOST=smtp.gmail.com
- EMAIL_SMTP_PORT=465
- EMAIL_SMTP_USER=gmail_email_address
- EMAIL_SMTP_PASSWORD='gmail_app_password'
</ArticleTab>
<ArticleTab>
Keep in mind that if you have 2FA enabled, you will need to provision an [App Password](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
- EMAIL_DRIVER=smtp
- EMAIL_SMTP_HOST=smtp.office365.com
- EMAIL_SMTP_PORT=587
- EMAIL_SMTP_USER=office365_email_address
- EMAIL_SMTP_PASSWORD='office365_password'
</ArticleTab>
<ArticleTab>
**smtp4dev** is a fake SMTP email server for development and testing.
- Run the smtp4dev image: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
- Access the smtp4dev ui here: [http://localhost:8090](http://localhost:8090)
- Set the following variables:
- EMAIL_DRIVER=smtp
- EMAIL_SMTP_HOST=localhost
- EMAIL_SMTP_PORT=2525
</ArticleTab>
</ArticleTabs>
<Warning>
**Environment-only mode:** If you set
`IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env`
file instead.
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
</Warning>
## S3 Storage
<Warning>
By default, Twenty stores uploaded files on the local filesystem. For
production deployments, use S3 or an S3-compatible service (MinIO,
DigitalOcean Spaces, etc.) to ensure files persist across container restarts
and scale across multiple server instances.
By default, Twenty stores uploaded files on the local filesystem. For production deployments, use S3 or an S3-compatible service (MinIO, DigitalOcean Spaces, etc.) to ensure files persist across container restarts and scale across multiple server instances.
</Warning>
Set `STORAGE_TYPE=S_3` and configure the `STORAGE_S3_*` variables through the admin panel or `.env`. See the [config-variables.ts reference](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts) for the full list of S3 variables.
@@ -358,32 +326,25 @@ Twenty supports logic functions for workflows and the code interpreter for AI da
**In development (NODE_ENV=development):** Both default to **LOCAL** for convenience when running locally.
<Warning>
**Security Notice:** The local driver (`LOGIC_FUNCTION_TYPE=LOCAL` or
`CODE_INTERPRETER_TYPE=LOCAL`) runs code directly on the host in a Node.js
process with no sandboxing. It should only be used for trusted code in
development. For production deployments handling untrusted code, use
`LOGIC_FUNCTION_TYPE=LAMBDA` or `CODE_INTERPRETER_TYPE=E2B` (with sandboxing),
or keep them disabled.
**Security Notice:** The local driver (`LOGIC_FUNCTION_TYPE=LOCAL` or `CODE_INTERPRETER_TYPE=LOCAL`) runs code directly on the host in a Node.js process with no sandboxing. It should only be used for trusted code in development. For production deployments handling untrusted code, use `LOGIC_FUNCTION_TYPE=LAMBDA` or `CODE_INTERPRETER_TYPE=E2B` (with sandboxing), or keep them disabled.
</Warning>
### Logic Functions - Available Drivers
| Driver | Environment Variable | Use Case | Security Level |
| -------- | ------------------------------ | ------------------------------------ | ------------------------------- |
| Disabled | `LOGIC_FUNCTION_TYPE=DISABLED` | Disable logic functions entirely | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
| Driver | Environment Variable | Use Case | Security Level |
|--------|---------------------|----------|----------------|
| Disabled | `LOGIC_FUNCTION_TYPE=DISABLED` | Disable logic functions entirely | N/A |
| Local | `LOGIC_FUNCTION_TYPE=LOCAL` | Development and trusted environments | Low (no sandboxing) |
| Lambda | `LOGIC_FUNCTION_TYPE=LAMBDA` | Production with untrusted code | High (hardware-level isolation) |
### Logic Functions - Recommended Configuration
**For development:**
```bash
LOGIC_FUNCTION_TYPE=LOCAL # default when NODE_ENV=development
```
**For production (AWS):**
```bash
LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
@@ -393,21 +354,18 @@ LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key
```
**To disable logic functions:**
```bash
LOGIC_FUNCTION_TYPE=DISABLED # default when NODE_ENV=production
```
### Code Interpreter - Available Drivers
| Driver | Environment Variable | Use Case | Security Level |
| -------- | -------------------------------- | ----------------------------------- | ----------------------- |
| Disabled | `CODE_INTERPRETER_TYPE=DISABLED` | Disable AI code execution | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Development only | Low (no sandboxing) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production with sandboxed execution | High (isolated sandbox) |
| Driver | Environment Variable | Use Case | Security Level |
|--------|---------------------|----------|----------------|
| Disabled | `CODE_INTERPRETER_TYPE=DISABLED` | Disable AI code execution | N/A |
| Local | `CODE_INTERPRETER_TYPE=LOCAL` | Development only | Low (no sandboxing) |
| E2B | `CODE_INTERPRETER_TYPE=E_2_B` | Production with sandboxed execution | High (isolated sandbox) |
<Note>
When using `LOGIC_FUNCTION_TYPE=DISABLED` or `CODE_INTERPRETER_TYPE=DISABLED`,
any attempt to execute will return an error. This is useful if you want to run
Twenty without these capabilities.
When using `LOGIC_FUNCTION_TYPE=DISABLED` or `CODE_INTERPRETER_TYPE=DISABLED`, any attempt to execute will return an error. This is useful if you want to run Twenty without these capabilities.
</Note>
@@ -1,8 +1,9 @@
---
title: Troubleshooting
icon: 'wrench'
icon: "wrench"
---
## Troubleshooting
If you encounter any problem while setting up environment for development, upgrading your instance or self-hosting,
@@ -22,7 +23,6 @@ The password you set during the first installation becomes permanently stored in
Only proceed if this is a fresh installation with no important data.
In order to update the `PG_DATABASE_PASSWORD` you need to:
```sh
# Update the PG_DATABASE_PASSWORD in .env
docker compose down --volumes
@@ -92,9 +92,7 @@ Restart your machine helps to clean up ghost processes.
That's expected as command `npx nx start` is running more commands under the hood
#### No emails are sent
Most of the time, it's because the `worker` is not running in the background. Try to run
```
npx nx worker twenty-server
```
@@ -114,37 +112,26 @@ Warnings are informing about pulling additional dependencies which aren't explic
That's expected as user is unauthorized when logged out since its identity is not verified.
#### How to check if your worker is running?
- Go to [webhook-test.com](https://webhook-test.com/) and copy **Your Unique Webhook URL**.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/self-hosting/webhook-test.jpg"
alt="Webhook test"
/>
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Webhook test" />
</div>
- Open your Twenty app, navigate to `/settings`, and enable the **Advanced** toggle at the bottom left of the screen.
- Create a new webhook.
- Paste **Your Unique Webhook URL** in the **Endpoint Url** field in Twenty. Set the **Filters** to `Companies` and `Created`.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/self-hosting/webhook-settings.jpg"
alt="Webhook settings"
/>
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Webhook settings" />
</div>
- Go to `/objects/companies` and create a new company record.
- Return to [webhook-test.com](https://webhook-test.com/) and check if a new **POST request** has been received.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/self-hosting/webhook-test-result.jpg"
alt="Webhook test result"
/>
</div>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Webhook test result" />
</div>
- If a **POST request** is received, your worker is running successfully. Otherwise, you need to troubleshoot your worker.
#### Front-end fails to start and returns error TS5042: Option 'project' cannot be mixed with source files on a command line
Comment out checker plugin in `packages/twenty-ui/vite-config.ts` like in example below
```
plugins: [
react({ jsxImportSource: 'react' }),
@@ -180,17 +167,16 @@ In production, logic functions are disabled by default. Set the `LOGIC_FUNCTION_
#### Unable to Log In
If you can't log in after setup:
1. Run the following commands:
```bash
docker exec -it twenty-server-1 yarn
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
```
```bash
docker exec -it twenty-server-1 yarn
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
```
2. Restart the Docker containers:
```bash
docker compose down
docker compose up -d
```
```bash
docker compose down
docker compose up -d
```
Note the database:reset command will completely erase your database and recreate it from scratch.
@@ -200,15 +186,16 @@ If you're running Twenty behind a reverse proxy and experiencing connection issu
1. **Verify SERVER_URL:**
Ensure `SERVER_URL` in your `.env` file matches your external access URL, including `https` if SSL is enabled.
Ensure `SERVER_URL` in your `.env` file matches your external access URL, including `https` if SSL is enabled.
2. **Check Reverse Proxy Settings:**
- Confirm that your reverse proxy is correctly forwarding requests to the Twenty server.
- Ensure headers like `X-Forwarded-For` and `X-Forwarded-Proto` are properly set.
- Confirm that your reverse proxy is correctly forwarding requests to the Twenty server.
- Ensure headers like `X-Forwarded-For` and `X-Forwarded-Proto` are properly set.
3. **Restart Services:**
After making changes, restart both the reverse proxy and Twenty containers.
After making changes, restart both the reverse proxy and Twenty containers.
#### Error when uploading an image - permission denied
@@ -220,12 +207,11 @@ If you encounter issues not covered in this guide:
- Check Logs:
View container logs for error messages:
```bash
docker compose logs
```
View container logs for error messages:
```bash
docker compose logs
```
- Community Support:
Reach out to the [Twenty community](https://github.com/twentyhq/twenty/issues) or [support channels](https://discord.gg/cx5n4Jzs57) for assistance.
Reach out to the [Twenty community](https://github.com/twentyhq/twenty/issues) or [support channels](https://discord.gg/cx5n4Jzs57) for assistance.
@@ -1,6 +1,6 @@
---
title: Upgrade guide
icon: 'arrow-up-right-dots'
icon: "arrow-up-right-dots"
---
## General guidelines
@@ -80,10 +80,10 @@ Summary
### Options
| Flag | Description |
| ------------------------- | ---------------------------------------------------------------- |
| `-w, --workspace-id <id>` | Filter to a specific workspace. Can be passed multiple times. |
| `-f, --failed-only` | Hide up-to-date workspaces, only show behind and failed entries. |
| Flag | Description |
| --- | --- |
| `-w, --workspace-id <id>` | Filter to a specific workspace. Can be passed multiple times. |
| `-f, --failed-only` | Hide up-to-date workspaces, only show behind and failed entries. |
## Troubleshooting
@@ -2,7 +2,6 @@
title: Self-Host
description: Deploy and manage Twenty on your own infrastructure.
---
<Frame>
<img src="/images/user-guide/what-is-twenty/20.png" alt="AI" />
</Frame>
@@ -19,18 +18,11 @@ Twenty can be self-hosted on your own infrastructure, giving you full control ov
## Getting Started
<CardGroup cols={2}>
<Card
title="Docker Compose"
icon="docker"
href="/developers/self-host/capabilities/docker-compose"
>
<Card title="Docker Compose" icon="docker" href="/developers/self-host/capabilities/docker-compose">
Quick setup with Docker
</Card>
<Card
title="Cloud Providers"
icon="cloud"
href="/developers/self-host/capabilities/cloud-providers"
>
<Card title="Cloud Providers" icon="cloud" href="/developers/self-host/capabilities/cloud-providers">
Deploy on AWS, GCP, or Azure
</Card>
</CardGroup>
+210 -70
View File
@@ -66,7 +66,9 @@
"groups": [
{
"group": "Overview",
"pages": ["user-guide/introduction"]
"pages": [
"user-guide/introduction"
]
},
{
"group": "Data Model",
@@ -229,7 +231,9 @@
},
{
"group": "How-Tos",
"pages": ["user-guide/ai/how-tos/ai-faq"]
"pages": [
"user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -326,7 +330,9 @@
},
{
"group": "How-Tos",
"pages": ["user-guide/billing/how-tos/billing-faq"]
"pages": [
"user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -348,7 +354,9 @@
},
{
"group": "How-Tos",
"pages": ["user-guide/settings/how-tos/settings-faq"]
"pages": [
"user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -359,7 +367,9 @@
"groups": [
{
"group": "Overview",
"pages": ["developers/introduction"]
"pages": [
"developers/introduction"
]
},
{
"group": "Apps",
@@ -489,7 +499,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/fr/user-guide/introduction"]
"pages": [
"l/fr/user-guide/introduction"
]
},
{
"group": "Modèle de données",
@@ -652,7 +664,9 @@
},
{
"group": "Guides pratiques",
"pages": ["l/fr/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/fr/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -749,7 +763,9 @@
},
{
"group": "Guides pratiques",
"pages": ["l/fr/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/fr/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -771,7 +787,9 @@
},
{
"group": "Guides pratiques",
"pages": ["l/fr/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/fr/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -782,7 +800,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/fr/developers/introduction"]
"pages": [
"l/fr/developers/introduction"
]
},
{
"group": "Apps",
@@ -912,7 +932,9 @@
"groups": [
{
"group": "نظرة عامة",
"pages": ["l/ar/user-guide/introduction"]
"pages": [
"l/ar/user-guide/introduction"
]
},
{
"group": "نموذج البيانات",
@@ -1075,7 +1097,9 @@
},
{
"group": "الإرشادات",
"pages": ["l/ar/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/ar/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -1172,7 +1196,9 @@
},
{
"group": "الإرشادات",
"pages": ["l/ar/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/ar/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -1194,7 +1220,9 @@
},
{
"group": "الإرشادات",
"pages": ["l/ar/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/ar/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -1205,7 +1233,9 @@
"groups": [
{
"group": "نظرة عامة",
"pages": ["l/ar/developers/introduction"]
"pages": [
"l/ar/developers/introduction"
]
},
{
"group": "التطبيقات",
@@ -1335,7 +1365,9 @@
"groups": [
{
"group": "Přehled",
"pages": ["l/cs/user-guide/introduction"]
"pages": [
"l/cs/user-guide/introduction"
]
},
{
"group": "Datový model",
@@ -1498,7 +1530,9 @@
},
{
"group": "Návody",
"pages": ["l/cs/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/cs/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -1595,7 +1629,9 @@
},
{
"group": "Návody",
"pages": ["l/cs/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/cs/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -1617,7 +1653,9 @@
},
{
"group": "Návody",
"pages": ["l/cs/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/cs/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -1628,7 +1666,9 @@
"groups": [
{
"group": "Přehled",
"pages": ["l/cs/developers/introduction"]
"pages": [
"l/cs/developers/introduction"
]
},
{
"group": "Aplikace",
@@ -1758,7 +1798,9 @@
"groups": [
{
"group": "Übersicht",
"pages": ["l/de/user-guide/introduction"]
"pages": [
"l/de/user-guide/introduction"
]
},
{
"group": "Datenmodell",
@@ -1921,7 +1963,9 @@
},
{
"group": "Anleitungen",
"pages": ["l/de/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/de/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -2018,7 +2062,9 @@
},
{
"group": "Anleitungen",
"pages": ["l/de/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/de/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -2040,7 +2086,9 @@
},
{
"group": "Anleitungen",
"pages": ["l/de/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/de/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -2051,7 +2099,9 @@
"groups": [
{
"group": "Übersicht",
"pages": ["l/de/developers/introduction"]
"pages": [
"l/de/developers/introduction"
]
},
{
"group": "Apps",
@@ -2181,7 +2231,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/es/user-guide/introduction"]
"pages": [
"l/es/user-guide/introduction"
]
},
{
"group": "Modelo de datos",
@@ -2344,7 +2396,9 @@
},
{
"group": "Guías prácticas",
"pages": ["l/es/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/es/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -2441,7 +2495,9 @@
},
{
"group": "Guías prácticas",
"pages": ["l/es/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/es/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -2463,7 +2519,9 @@
},
{
"group": "Guías prácticas",
"pages": ["l/es/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/es/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -2474,7 +2532,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/es/developers/introduction"]
"pages": [
"l/es/developers/introduction"
]
},
{
"group": "Apps",
@@ -2604,7 +2664,9 @@
"groups": [
{
"group": "Panoramica",
"pages": ["l/it/user-guide/introduction"]
"pages": [
"l/it/user-guide/introduction"
]
},
{
"group": "Modello dati",
@@ -2767,7 +2829,9 @@
},
{
"group": "Guide pratiche",
"pages": ["l/it/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/it/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -2864,7 +2928,9 @@
},
{
"group": "Guide pratiche",
"pages": ["l/it/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/it/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -2886,7 +2952,9 @@
},
{
"group": "Guide pratiche",
"pages": ["l/it/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/it/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -2897,7 +2965,9 @@
"groups": [
{
"group": "Panoramica",
"pages": ["l/it/developers/introduction"]
"pages": [
"l/it/developers/introduction"
]
},
{
"group": "App",
@@ -3027,7 +3097,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/ja/user-guide/introduction"]
"pages": [
"l/ja/user-guide/introduction"
]
},
{
"group": "データモデル",
@@ -3190,7 +3262,9 @@
},
{
"group": "ハウツー",
"pages": ["l/ja/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/ja/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -3287,7 +3361,9 @@
},
{
"group": "ハウツー",
"pages": ["l/ja/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/ja/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -3309,7 +3385,9 @@
},
{
"group": "ハウツー",
"pages": ["l/ja/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/ja/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -3320,7 +3398,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/ja/developers/introduction"]
"pages": [
"l/ja/developers/introduction"
]
},
{
"group": "Apps",
@@ -3450,7 +3530,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/ko/user-guide/introduction"]
"pages": [
"l/ko/user-guide/introduction"
]
},
{
"group": "데이터 모델",
@@ -3613,7 +3695,9 @@
},
{
"group": "사용 방법",
"pages": ["l/ko/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/ko/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -3710,7 +3794,9 @@
},
{
"group": "사용 방법",
"pages": ["l/ko/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/ko/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -3732,7 +3818,9 @@
},
{
"group": "사용 방법",
"pages": ["l/ko/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/ko/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -3743,7 +3831,9 @@
"groups": [
{
"group": "Overview",
"pages": ["l/ko/developers/introduction"]
"pages": [
"l/ko/developers/introduction"
]
},
{
"group": "Apps",
@@ -3873,7 +3963,9 @@
"groups": [
{
"group": "Visão Geral",
"pages": ["l/pt/user-guide/introduction"]
"pages": [
"l/pt/user-guide/introduction"
]
},
{
"group": "Modelo de dados",
@@ -4036,7 +4128,9 @@
},
{
"group": "How-Tos",
"pages": ["l/pt/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/pt/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -4133,7 +4227,9 @@
},
{
"group": "How-Tos",
"pages": ["l/pt/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/pt/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -4155,7 +4251,9 @@
},
{
"group": "How-Tos",
"pages": ["l/pt/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/pt/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -4166,7 +4264,9 @@
"groups": [
{
"group": "Visão Geral",
"pages": ["l/pt/developers/introduction"]
"pages": [
"l/pt/developers/introduction"
]
},
{
"group": "Aplicativos",
@@ -4296,7 +4396,9 @@
"groups": [
{
"group": "Prezentare generală",
"pages": ["l/ro/user-guide/introduction"]
"pages": [
"l/ro/user-guide/introduction"
]
},
{
"group": "Model de date",
@@ -4459,7 +4561,9 @@
},
{
"group": "How-Tos",
"pages": ["l/ro/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/ro/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -4556,7 +4660,9 @@
},
{
"group": "How-Tos",
"pages": ["l/ro/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/ro/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -4578,7 +4684,9 @@
},
{
"group": "How-Tos",
"pages": ["l/ro/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/ro/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -4589,7 +4697,9 @@
"groups": [
{
"group": "Prezentare generală",
"pages": ["l/ro/developers/introduction"]
"pages": [
"l/ro/developers/introduction"
]
},
{
"group": "Aplicații",
@@ -4719,7 +4829,9 @@
"groups": [
{
"group": "Обзор",
"pages": ["l/ru/user-guide/introduction"]
"pages": [
"l/ru/user-guide/introduction"
]
},
{
"group": "Модель данных",
@@ -4882,7 +4994,9 @@
},
{
"group": "Инструкции",
"pages": ["l/ru/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/ru/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -4979,7 +5093,9 @@
},
{
"group": "Инструкции",
"pages": ["l/ru/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/ru/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -5001,7 +5117,9 @@
},
{
"group": "Инструкции",
"pages": ["l/ru/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/ru/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -5012,7 +5130,9 @@
"groups": [
{
"group": "Обзор",
"pages": ["l/ru/developers/introduction"]
"pages": [
"l/ru/developers/introduction"
]
},
{
"group": "Приложения",
@@ -5142,7 +5262,9 @@
"groups": [
{
"group": "Genel Bakış",
"pages": ["l/tr/user-guide/introduction"]
"pages": [
"l/tr/user-guide/introduction"
]
},
{
"group": "Veri modeli",
@@ -5305,7 +5427,9 @@
},
{
"group": "Nasıl Yapılırlar",
"pages": ["l/tr/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/tr/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -5402,7 +5526,9 @@
},
{
"group": "Nasıl Yapılırlar",
"pages": ["l/tr/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/tr/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -5424,7 +5550,9 @@
},
{
"group": "Nasıl Yapılırlar",
"pages": ["l/tr/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/tr/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -5435,7 +5563,9 @@
"groups": [
{
"group": "Genel Bakış",
"pages": ["l/tr/developers/introduction"]
"pages": [
"l/tr/developers/introduction"
]
},
{
"group": "Uygulamalar",
@@ -5565,7 +5695,9 @@
"groups": [
{
"group": "概览",
"pages": ["l/zh/user-guide/introduction"]
"pages": [
"l/zh/user-guide/introduction"
]
},
{
"group": "数据模型",
@@ -5728,7 +5860,9 @@
},
{
"group": "操作指南",
"pages": ["l/zh/user-guide/ai/how-tos/ai-faq"]
"pages": [
"l/zh/user-guide/ai/how-tos/ai-faq"
]
}
]
},
@@ -5825,7 +5959,9 @@
},
{
"group": "操作指南",
"pages": ["l/zh/user-guide/billing/how-tos/billing-faq"]
"pages": [
"l/zh/user-guide/billing/how-tos/billing-faq"
]
}
]
},
@@ -5847,7 +5983,9 @@
},
{
"group": "操作指南",
"pages": ["l/zh/user-guide/settings/how-tos/settings-faq"]
"pages": [
"l/zh/user-guide/settings/how-tos/settings-faq"
]
}
]
}
@@ -5858,7 +5996,9 @@
"groups": [
{
"group": "概览",
"pages": ["l/zh/developers/introduction"]
"pages": [
"l/zh/developers/introduction"
]
},
{
"group": "应用",
@@ -1,7 +1,7 @@
---
title: AI
description: How Twenty uses AI to enhance your CRM experience.
icon: 'robot'
icon: "robot"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
@@ -1,9 +1,10 @@
---
title: Apps
icon: 'cube'
icon: "cube"
description: Extend Twenty with code — custom objects, server-side logic, UI components, and AI agents, all as TypeScript packages.
---
Most CRMs give you a config panel. Twenty gives you a platform. Apps are how developers extend Twenty beyond what the UI offers — defining data models, server-side logic, UI components, and AI capabilities as code, then deploying them to one or more workspaces.
## Why apps exist
@@ -16,13 +17,13 @@ Apps let you build these as first-class extensions — not brittle scripts talki
An app is a TypeScript package that declares **entities** using the `twenty-sdk`:
| Entity | What it does |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| **Objects & Fields** | New data tables and fields on existing objects — same treatment as built-in ones |
| **Logic Functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
| **Front Components** | Sandboxed React components that render inside Twenty's UI (side panel, widgets, command menu) |
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
| Entity | What it does |
|--------|-------------|
| **Objects & Fields** | New data tables and fields on existing objects — same treatment as built-in ones |
| **Logic Functions** | Server-side TypeScript triggered by HTTP routes, cron schedules, or database events |
| **Front Components** | Sandboxed React components that render inside Twenty's UI (side panel, widgets, command menu) |
| **Skills & Agents** | AI capabilities — reusable instructions and autonomous assistants |
| **Views & Navigation** | Pre-configured list views and sidebar menu items |
Everything is detected via AST analysis at build time — no config files, no registration boilerplate. Put a `export default defineObject(...)` in any `.ts` file and the SDK picks it up.
@@ -36,10 +37,6 @@ Everything is detected via AST analysis at build time — no config files, no re
You write your app as a TypeScript project on your machine. The CLI watches your source files and live-syncs them to a running Twenty server — edit a file, see the change in the UI within a second. The typed API client regenerates automatically when the schema changes. When you're ready, `yarn twenty app:publish --private` pushes to a production server, or `yarn twenty app:publish` lists your app on npm and the Twenty marketplace.
<Card
title="Build your first app"
icon="arrow-right"
href="/developers/extend/apps/getting-started"
>
<Card title="Build your first app" icon="arrow-right" href="/developers/extend/apps/getting-started">
Three-phase walkthrough — scaffold, run a local server, sync your changes.
</Card>
@@ -1,7 +1,7 @@
---
title: Calendar & Email
description: Sync your email and calendar with Twenty.
icon: 'envelope'
icon: "envelope"
---
Twenty connects to your existing tools so your CRM stays up to date without manual data entry.
@@ -25,23 +25,19 @@ Calendar events sync automatically from your connected account. Events appear on
Beyond email and calendar, Twenty connects to external tools through:
| Method | Use case |
| ------------------------- | -------------------------------------------------------------------- |
| **API** | Build custom integrations with the GraphQL or REST API |
| **Webhooks** | Push real-time notifications to external systems when records change |
| **Zapier** | Connect to 5,000+ apps without code |
| **Workflow HTTP actions** | Call any external API as part of an automated workflow |
| Method | Use case |
|--------|----------|
| **API** | Build custom integrations with the GraphQL or REST API |
| **Webhooks** | Push real-time notifications to external systems when records change |
| **Zapier** | Connect to 5,000+ apps without code |
| **Workflow HTTP actions** | Call any external API as part of an automated workflow |
## Custom apps
Developers can build full-featured apps on top of Twenty — adding custom UI, server-side logic, and deep integrations. Apps can be published for the community or kept private.
<CardGroup cols={2}>
<Card
title="Calendar & Email guide"
icon="envelope"
href="/user-guide/calendar-emails/overview"
>
<Card title="Calendar & Email guide" icon="envelope" href="/user-guide/calendar-emails/overview">
Set up email sync, calendar sync, and troubleshoot issues.
</Card>
<Card title="API & Extensions" icon="plug" href="/developers/extend/api">
@@ -1,13 +1,12 @@
---
title: Dashboards
description: Track performance and visualize your CRM data with custom dashboards.
icon: 'chart-bar'
icon: "chart-bar"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
Dashboards give you real-time visibility into your business metrics — pipeline health, team performance, revenue trends, and anything else you want to track.
<VimeoEmbed videoId="1185511768" title="Dashboard" />
## Widgets
@@ -32,11 +31,6 @@ Each dashboard is made up of widgets. A widget is a single chart or metric tied
Dashboards are workspace-level — everyone on your team can see them. Arrange widgets in a grid layout and resize them to build the view that works for your team.
<Card
title="Full Dashboards guide"
icon="arrow-right"
href="/user-guide/dashboards/overview"
>
Detailed reference on creating dashboards, configuring widgets, and chart
settings.
<Card title="Full Dashboards guide" icon="arrow-right" href="/user-guide/dashboards/overview">
Detailed reference on creating dashboards, configuring widgets, and chart settings.
</Card>
@@ -1,7 +1,7 @@
---
title: Data Model
description: Understand how Twenty structures your data with objects, fields, and relations.
icon: 'database'
icon: "database"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
@@ -24,11 +24,11 @@ You can also create **custom objects** for anything your business needs — proj
Fields are the properties on each object. Twenty supports a wide range of field types:
| Category | Types |
| ------------- | -------------------------------------------------------------------- |
| **Basic** | Text, Number, Boolean, Date, Currency, Rating, Select |
| Category | Types |
|----------|-------|
| **Basic** | Text, Number, Boolean, Date, Currency, Rating, Select |
| **Composite** | Address (street, city, state, zip), Full Name, Links, Phones, Emails |
| **Special** | Relation, File Attachment, JSON, Actor (who created/modified) |
| **Special** | Relation, File Attachment, JSON, Actor (who created/modified) |
Every object also gets automatic system fields: `id`, `createdAt`, `updatedAt`, `createdBy`, and `position`.
@@ -40,11 +40,7 @@ Objects connect to each other through relations. A Company has many People, an O
Unlike traditional CRMs where you're limited to pre-defined fields on pre-defined objects, Twenty lets you model your data exactly the way your business works. Custom objects get the same first-class treatment as built-in ones — including API endpoints, views, permissions, and workflow triggers.
<Card
title="Deep dive into the Data Model"
icon="arrow-right"
href="/user-guide/data-model/overview"
>
<Card title="Deep dive into the Data Model" icon="arrow-right" href="/user-guide/data-model/overview">
Full reference on objects, fields, relations, and how to configure them.
</Card>
@@ -1,105 +1,81 @@
---
title: Glossary
icon: 'book'
icon: "book"
description: Key terms used throughout Twenty.
---
## API
API (Application Programming Interface) allows you to connect Twenty with other software systems and build custom integrations.
## Apps
Apps are custom extensions built as code that can define data models and logic functions. They enable developers to create reusable customizations that can be deployed across multiple workspaces.
## Code Actions
Code Actions are workflow steps that let you write custom JavaScript to transform data, make calculations, or perform complex logic that isn't possible with built-in actions.
## Command Menu
The Command Menu is a quick-access interface (opened with `Cmd + K` on Mac and `Ctrl + K` on Windows) that lets you perform actions, create records, and navigate your workspace efficiently.
## Company & People
The CRM has two fundamental types of records:
- A `Company` represents a business or organization.
- `People` represent your company's current and prospective customers or clients.
## Custom Fields
Custom Fields are data fields you create to capture information specific to your business needs and processes.
## Data Model
A Data Model is the structure that defines how information is organized in your CRM, including what objects exist, their properties (fields), and how they relate to each other.
## Favorites
Favorites are records you've marked for quick access, appearing in your sidebar for instant navigation to important data.
## Field
A field refers to a specific area where particular data is stored for an entity.
## Iterator
An Iterator is a workflow action that loops through an array of items, executing subsequent actions for each item in the list.
## Kanban
A `Kanban` is a visual way to track your business processes using cards and columns. Each column represents a stage in your process (for example: new, ongoing, won, lost), and you move records through these stages as they progress.
## Object
An Object is a data structure that represents a specific type of entity in your CRM (like People, Companies, or Opportunities). Objects can be standard (built-in) or custom (created by you).
## Opportunities
Opportunities in Twenty CRM are potential deals or sales with accounts or contacts.
## Record
A Record indicates an instance of an object, like a specific account or contact.
## Relation Fields
Relation Fields create connections between different objects, allowing you to link records together (like connecting a Person to a Company).
## Standard Fields
Standard Fields are pre-built data fields that come with objects by default and provide common functionality across all workspaces.
## Tasks
Tasks in Twenty CRM are assigned activities relating to contacts, accounts, or opportunities.
## Triggers
Triggers are the starting point of a workflow — the event or condition that initiates the automation. Examples include record creation, record updates, webhooks, or scheduled times.
## Views
You can customize the display of your records using views, setting different filters, layouts and sorting options for each view.
## Upsert
Upsert is an operation that combines "update" and "insert" — it updates an existing record if a match is found, or creates a new record if no match exists.
## Webhooks
Webhooks are automated messages sent from Twenty to other applications when specific events occur, enabling real-time data synchronization.
## Workflows
Workflows are automated processes that trigger actions based on specific conditions, helping you automate repetitive tasks and business processes.
## Workspace
A `Workspace` typically represents a company using Twenty. It holds all the records and data that you and your team members add to Twenty.
It has a single domain name, which is typically the domain name your company uses for employee email addresses.
## Workspace Members
Workspace Members are the Twenty users from your team who have access to your workspace. They can be assigned as owners or assignees for records.
@@ -1,7 +1,7 @@
---
title: Layout
description: How to navigate, browse, and view records in Twenty.
icon: 'table-columns'
icon: "table-columns"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
@@ -10,12 +10,11 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
The center of the screen is where your records live — people, companies, opportunities, tasks, notes, dashboards, workflows, and any custom objects. You view, edit, and delete records here, and create new views.
<img src="/images/user-guide/home/main-layout.png" style={{ width: '100%' }} />
<img src="/images/user-guide/home/main-layout.png" style={{width:'100%'}}/>
## Navigation bar
The left sidebar gives you:
- **Workspace switcher** — switch between workspaces or create a new one (top dropdown)
- **Search** — press `/` to focus instantly, searches across all objects
- **Settings** — access from the top left
@@ -25,34 +24,30 @@ The left sidebar gives you:
Drag items to reorder, create folders to group related objects, hide what you don't use.
<img
src="/images/user-guide/home/navigation-bar.png"
style={{ width: '100%' }}
/>
<img src="/images/user-guide/home/navigation-bar.png" style={{width:'100%'}}/>
## Command menu
Press `Cmd+K` (Mac) or `Ctrl+K` (Windows) — or click the three dots in the top right. From here you can:
- Create new records
- Import and export data via CSV
- Create new views
- Access deleted records (Twenty supports soft and hard deletes)
- See keyboard shortcuts for navigating your workspace
<img src="/images/user-guide/home/command-menu.png" style={{ width: '100%' }} />
<img src="/images/user-guide/home/command-menu.png" style={{width:'100%'}}/>
## Search
Accessible via the Command Menu, the top of the navigation bar, or by pressing `/`. Search works across all objects.
<img src="/images/user-guide/home/search-bar.png" style={{ width: '100%' }} />
<img src="/images/user-guide/home/search-bar.png" style={{width:'100%'}}/>
## Side panel
Click a record to open the side panel on the right — a quick overview of the record's key information without leaving the current page. Click **Open** to go to the full record page.
<img src="/images/user-guide/home/side-panel.png" style={{ width: '100%' }} />
<img src="/images/user-guide/home/side-panel.png" style={{width:'100%'}}/>
## Views
@@ -64,18 +59,15 @@ Every object supports multiple views — unlimited per object. Use the dropdown
Each view saves its own filters, sorting, and field visibility. Share views with your workspace or keep them private. Favorite views for fast access from the sidebar.
<img src="/images/user-guide/home/view-menu.png" style={{ width: '100%' }} />
<img src="/images/user-guide/home/view-menu.png" style={{width:'100%'}}/>
## Record pages
When you open a record, the detail page is built from configurable **tabs** and **widgets**. Add, remove, reorder, and resize widgets on a grid — fields, related records, emails, timeline, tasks, notes, files, charts, iframes, and more. Each object type has its own layout.
<Card
title="Full Layout guide"
icon="arrow-right"
href="/user-guide/layout/overview"
>
<Card title="Full Layout guide" icon="arrow-right" href="/user-guide/layout/overview">
Navigation, views, record pages — detailed reference and how-tos.
</Card>
<VimeoEmbed videoId="1185511790" title="Layout" />
@@ -1,7 +1,7 @@
---
title: Workflows
description: Automate your business processes with Twenty's visual workflow builder.
icon: 'bolt'
icon: "bolt"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
@@ -10,6 +10,7 @@ Workflows let you automate repetitive tasks and connect Twenty to external tools
<VimeoEmbed videoId="1185511711" title="Workflow" />
## How workflows work
Every workflow has three parts:
@@ -20,12 +21,12 @@ Every workflow has three parts:
## Triggers
| Trigger | When it fires |
| ---------------- | -------------------------------------------------------------------------- |
| **Record event** | A record is created, updated, deleted, or upserted |
| **Manual** | A user clicks a button (on a single record, multiple records, or globally) |
| **Schedule** | On a recurring interval (cron syntax) |
| **Webhook** | An external system sends an HTTP POST |
| Trigger | When it fires |
|---------|--------------|
| **Record event** | A record is created, updated, deleted, or upserted |
| **Manual** | A user clicks a button (on a single record, multiple records, or globally) |
| **Schedule** | On a recurring interval (cron syntax) |
| **Webhook** | An external system sends an HTTP POST |
## Actions
@@ -50,11 +51,6 @@ Workflows can chain any combination of:
- Generate PDFs or invoices from record data
- Auto-reply to inbound emails matching certain criteria
<Card
title="Full Workflows guide"
icon="arrow-right"
href="/user-guide/workflows/overview"
>
Detailed reference on triggers, actions, variables, and real-world automation
recipes.
<Card title="Full Workflows guide" icon="arrow-right" href="/user-guide/workflows/overview">
Detailed reference on triggers, actions, variables, and real-world automation recipes.
</Card>
@@ -1,12 +1,13 @@
---
title: Why Twenty
icon: 'heart'
icon: "heart"
---
import { CardTitle } from '/snippets/card-title.mdx';
import { CardTitle } from "/snippets/card-title.mdx"
You've been choosing between software that's easy to start but impossible to change, and software that's flexible but takes months to set up. Twenty is the third option: **a production-ready CRM you can reshape as you go.**
## What makes Twenty different
Twenty is a platform you can build on, not a product you configure.
@@ -14,8 +15,7 @@ Twenty is a platform you can build on, not a product you configure.
<CardGroup cols={2}>
<Card img="/images/user-guide/halftone/ai.png">
<CardTitle>Built for Agents</CardTitle>
Agents operate inside your data model with real permissions. Skills, Tools,
MCP.
Agents operate inside your data model with real permissions. Skills, Tools, MCP.
</Card>
<Card img="/images/user-guide/halftone/permissions.png">
<CardTitle>Secured Extensibility</CardTitle>
@@ -23,8 +23,7 @@ Twenty is a platform you can build on, not a product you configure.
</Card>
<Card img="/images/user-guide/halftone/dev-api.png">
<CardTitle>Modern Stack</CardTitle>
React, TypeScript. Your team already knows how to extend Twenty. No
proprietary languages, no gatekeeping.
React, TypeScript. Your team already knows how to extend Twenty. No proprietary languages, no gatekeeping.
</Card>
<Card img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>No Lock-In</CardTitle>
@@ -46,15 +45,16 @@ Twenty is a platform you can build on, not a product you configure.
- **Privacy-conscious organizations** — you need to
self-host and own your data end to end. Regulatory,
contractual, or just principle.
- **Salesforce partners** —
your developers are tired of presenting license cost increases to your clients.
You want a better DX, and to deliver projects faster at a lower license cost for your clients.
- **Salesforce partners**
your developers are tired of presenting license cost increases to your clients.
You want a better DX, and to deliver projects faster at a lower license cost for your clients.
- **Web development agencies** — your team knows TypeScript, React,
and PostgreSQL. Twenty opens the CRM market
to you without learning APEX or getting proprietary certifications.
and PostgreSQL. Twenty opens the CRM market
to you without learning APEX or getting proprietary certifications.
## Who Twenty is not for
- **Teams that want a CRM they never have to think
about.** Twenty rewards teams that want to stay close to
their tools and shape them over time. If you want
@@ -70,10 +70,7 @@ Twenty is a platform you can build on, not a product you configure.
and executive briefings. We win with teams that have the
authority to pick their own tools.
<Card
title="Ready to get started?"
icon="rocket"
href="/getting-started/quickstart"
>
<Card title="Ready to get started?" icon="rocket" href="/getting-started/quickstart">
Set up Twenty in under 5 minutes — cloud or self-hosted.
</Card>
@@ -1,10 +1,10 @@
---
title: Key Features
description: A tour of everything Twenty can do — from custom data models to AI-powered automation.
icon: 'star'
icon: "star"
---
import { CardTitle } from '/snippets/card-title.mdx';
import { CardTitle } from "/snippets/card-title.mdx"
Twenty is a full-featured CRM platform. Here's what you can build with it.
@@ -15,88 +15,52 @@ Twenty is a full-featured CRM platform. Here's what you can build with it.
<br />Create custom objects, add custom fields with 20+ field types, and build relationships between any objects.
</Card>
<Card
href="/user-guide/views-pipelines/overview"
img="/images/user-guide/halftone/layout.png"
>
<CardTitle>Views & Pipelines</CardTitle>
Switch between table views, kanban boards, and calendar views.
<br />
Filter with AND/OR logic, sort by multiple fields, group records, and save
custom views.
</Card>
<Card href="/user-guide/views-pipelines/overview" img="/images/user-guide/halftone/layout.png">
<CardTitle>Views & Pipelines</CardTitle>
Switch between table views, kanban boards, and calendar views.
<br />Filter with AND/OR logic, sort by multiple fields, group records, and save custom views.
</Card>
<Card
href="/user-guide/workflows/overview"
img="/images/user-guide/halftone/workflows.png"
>
<CardTitle>Workflows & Automation</CardTitle>
Automate any business process without writing code.
<br />
Trigger workflows on record changes, schedules, manual actions, or incoming
webhooks.
</Card>
<Card href="/user-guide/workflows/overview" img="/images/user-guide/halftone/workflows.png">
<CardTitle>Workflows & Automation</CardTitle>
Automate any business process without writing code.
<br />Trigger workflows on record changes, schedules, manual actions, or incoming webhooks.
</Card>
<Card
href="/user-guide/calendar-emails/overview"
img="/images/user-guide/halftone/calendar-emails.png"
>
<CardTitle>Calendar & Email Sync</CardTitle>
Connect your Google Workspace or Microsoft 365 account.
<br />
Emails and calendar events automatically appear on the relevant CRM records.
</Card>
<Card href="/user-guide/calendar-emails/overview" img="/images/user-guide/halftone/calendar-emails.png">
<CardTitle>Calendar & Email Sync</CardTitle>
Connect your Google Workspace or Microsoft 365 account.
<br />Emails and calendar events automatically appear on the relevant CRM records.
</Card>
<Card href="/user-guide/ai/overview" img="/images/user-guide/halftone/ai.png">
<CardTitle>AI</CardTitle>
AI agents that work autonomously within your CRM — answering questions,
enriching records, and executing multi-step tasks within your permission
model.
</Card>
<Card href="/user-guide/ai/overview" img="/images/user-guide/halftone/ai.png">
<CardTitle>AI</CardTitle>
AI agents that work autonomously within your CRM — answering questions, enriching records, and executing multi-step tasks within your permission model.
</Card>
<Card
href="/user-guide/dashboards/overview"
img="/images/user-guide/halftone/dashboards.png"
>
<CardTitle>Dashboards & Reporting</CardTitle>
Build custom dashboards with real-time widgets.
<br />
Track pipeline metrics, team performance, and business KPIs with configurable
charts and filters.
</Card>
<Card href="/user-guide/dashboards/overview" img="/images/user-guide/halftone/dashboards.png">
<CardTitle>Dashboards & Reporting</CardTitle>
Build custom dashboards with real-time widgets.
<br />Track pipeline metrics, team performance, and business KPIs with configurable charts and filters.
</Card>
<Card
href="/user-guide/permissions-access/overview"
img="/images/user-guide/halftone/permissions.png"
>
<CardTitle>Permissions & Access Control</CardTitle>
Role-based access control at every level — objects, fields, and individual
records.
<br />
Configure SSO with SAML or OIDC. Audit logs track who did what.
</Card>
<Card href="/user-guide/permissions-access/overview" img="/images/user-guide/halftone/permissions.png">
<CardTitle>Permissions & Access Control</CardTitle>
Role-based access control at every level — objects, fields, and individual records.
<br />Configure SSO with SAML or OIDC. Audit logs track who did what.
</Card>
<Card
href="/developers/extend/api"
img="/images/user-guide/halftone/dev-api.png"
>
<CardTitle>API & Extensibility</CardTitle>
A developer-first API that adapts to your custom data model.
<br />
Both GraphQL and REST endpoints, with auto-generated documentation per
workspace.
</Card>
<Card href="/developers/extend/api" img="/images/user-guide/halftone/dev-api.png">
<CardTitle>API & Extensibility</CardTitle>
A developer-first API that adapts to your custom data model.
<br />Both GraphQL and REST endpoints, with auto-generated documentation per workspace.
</Card>
<Card
href="/user-guide/data-migration/overview"
img="/images/user-guide/halftone/data-migration.png"
>
<CardTitle>Data Import & Export</CardTitle>
Import data from CSV files or via API.
<br />
Field mapping, duplicate detection, and error handling built in. Export your
data anytime — no lock-in.
</Card>
<Card href="/user-guide/data-migration/overview" img="/images/user-guide/halftone/data-migration.png">
<CardTitle>Data Import & Export</CardTitle>
Import data from CSV files or via API.
<br />Field mapping, duplicate detection, and error handling built in. Export your data anytime — no lock-in.
</Card>
<Card href="/developers/self-host/self-host" img="/images/user-guide/halftone/dev-self-host.png">
<CardTitle>Self-Hosting</CardTitle>
@@ -1,7 +1,7 @@
---
title: Quickstart
description: Get Twenty up and running in under 5 minutes — on the cloud or self-hosted.
icon: 'play'
icon: "play"
---
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
@@ -10,22 +10,21 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
<Steps>
<Step title="Create your account">
Go to [app.twenty.com](https://app.twenty.com) and sign up with Google,
Microsoft, or email.
Go to [app.twenty.com](https://app.twenty.com) and sign up with Google, Microsoft, or email.
</Step>
<Step title="Choose a trial">
Pick **30 days** (with card) or **7 days** (without card). Both include full
access — unlimited contacts, email integration, custom objects, API. You can
change plan or billing interval anytime.
Pick **30 days** (with card) or **7 days** (without card). Both include full access — unlimited contacts, email integration, custom objects, API. You can change plan or billing interval anytime.
</Step>
<Step title="Create your workspace">
After payment confirmation via Stripe, you'll set up your workspace name and
user profile. You can cancel anytime.
After payment confirmation via Stripe, you'll set up your workspace name and user profile. You can cancel anytime.
</Step>
</Steps>
<VimeoEmbed videoId="1185227242" title="Creating a workspace" />
## Configure your workspace
Once you're in, three steps to make Twenty yours:
@@ -34,22 +33,17 @@ Once you're in, three steps to make Twenty yours:
Go to **Settings → Accounts** and connect your Google or Microsoft account. Twenty will import your emails and calendar events, and auto-create contacts from interactions. Using another provider? You can add mailboxes via SMTP or calendars via CalDAV from the same page.
<Note>
Start here — connecting a mailbox gives your team immediate value with real
data before you customize anything else.
</Note>
<Note>Start here — connecting a mailbox gives your team immediate value with real data before you customize anything else.</Note>
<VimeoEmbed videoId="1185416821" title="Connect your mailbox" />
### 2. Shape your data model
Go to **Settings → Data Model** to create custom objects and fields. A few things to know:
- Custom objects and fields are **unlimited on all plans** — no upsell.
- **People, Companies, and Opportunities** are the objects that show synced emails and meetings. Use them as your base and add fields to categorize (e.g., a `Person Type` field) rather than creating separate objects that won't have email history.
- Two People can't share the same email. Two Companies can't share the same domain.
- You can deactivate standard fields/objects you don't need, and hide fields from views without deleting them.
[Data Model reference →](/user-guide/data-model/overview)
<VimeoEmbed videoId="1185416793" title="Shape your data model" />
### 3. Import your data
@@ -57,24 +51,15 @@ Go to **Settings → Data Model** to create custom objects and fields. A few thi
Use the Command Menu (`Cmd+K` / `Ctrl+K`) to import People, Companies, Opportunities, or any custom object via CSV. Download the sample file first to see the expected format. Limit files to 10k records and deduplicate emails/domains before importing.
[Data Migration guide →](/user-guide/data-migration/overview)
<VimeoEmbed videoId="1185416775" title="Import your data" />
## Next steps
<CardGroup cols={2}>
<Card
title="Learn the layout"
icon="table-columns"
href="/getting-started/core-concepts/layout"
>
<Card title="Learn the layout" icon="table-columns" href="/getting-started/core-concepts/layout">
Navigation, views, command menu, side panel.
</Card>
<Card
title="Build Workflows"
icon="bolt"
href="/user-guide/workflows/overview"
>
<Card title="Build Workflows" icon="bolt" href="/user-guide/workflows/overview">
Automate your business processes.
</Card>
<Card title="Create Views" icon="table" href="/user-guide/layout/overview">
@@ -10,11 +10,8 @@ title: كائنات مخصصة
## مخطط على مستوى عالي
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/custom-object-schema.png"
alt="مخطط على مستوى عالي"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="مخطط على مستوى عالي" />
</div>
<br />
@@ -23,26 +20,20 @@ title: كائنات مخصصة
الكائنات المخصصة تأتي من جداول الميتاداتا التي تحدد شكل، اسم، ونوع الكائنات. كل هذه المعلومات موجودة في قاعدة بيانات المخطط البياني للبيانات الوصفية، والتي تتكون من جداول:
- **مصدر البيانات**: يشير إلى مكان وجود البيانات.
- **الكائن**: يصف الكائن ويرتبط بمصدر البيانات.
- **الحقل**: يحدد حقول الكائن ويرتبط بالكائن.
* **مصدر البيانات**: يشير إلى مكان وجود البيانات.
* **الكائن**: يصف الكائن ويرتبط بمصدر البيانات.
* **الحقل**: يحدد حقول الكائن ويرتبط بالكائن.
لإضافة كائن مخصص، سيقوم عضو مساحة العمل بالاستعلام عن واجهة برمجة التطبيقات /metadata. يقوم هذا بتحديث البيانات الوصفية وفقًا لذلك ويحسب مخطط GraphQL استنادًا إلى البيانات الوصفية، ويخزنها في ذاكرة GQL للاستخدام لاحقًا.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/add-custom-objects.jpeg"
alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة" />
</div>
<br />
لجلب البيانات، تتضمن العملية إجراء استعلامات من خلال نقطة النهاية /graphql وتمريرها من خلال محلل الاستعلام.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/server/custom-object-schema.png"
alt="استعلام نقطة النهاية /graphql لجلب البيانات"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="استعلام نقطة النهاية /graphql لجلب البيانات" />
</div>
@@ -95,8 +95,8 @@ workspace
مصانع متخصصة لتوليد الإنشاءات المتعلقة بـ GraphQL.
- المصنع النوعي يترجم بيانات الحقول الوصفية إلى أنواع GraphQL باستخدام `TypeMapperService`.
- ينشئ type-definition.factory كائنات إدخال أو إخراج لـ GraphQL مشتقة من `objectMetadata`.
* المصنع النوعي يترجم بيانات الحقول الوصفية إلى أنواع GraphQL باستخدام `TypeMapperService`.
* ينشئ type-definition.factory كائنات إدخال أو إخراج لـ GraphQL مشتقة من `objectMetadata`.
#### أنواع GraphQL
@@ -18,9 +18,7 @@ title: قائمة انتظار الرسائل
```ts
class Resolver {
constructor(
@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService,
) {}
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
async onSomeAction() {
//business logic
@@ -30,9 +28,7 @@ class Resolver {
//async worker
class CustomWorker {
constructor(
@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService,
) {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
this.initWorker();
}
@@ -61,27 +61,27 @@ Prisma كان أول ORM استخدمناه. ولكن للسماح للمستخ
**النواة**
- [NestJS](https://nestjs.com/)
- [TypeORM](https://typeorm.io/)
- [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
* [NestJS](https://nestjs.com/)
* [TypeORM](https://typeorm.io/)
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
**قاعدة البيانات**
- [Postgres](https://www.postgresql.org/)
* [Postgres](https://www.postgresql.org/)
**التكاملات مع جهات خارجية**
- [Sentry](https://sentry.io/welcome/) لتتبع الأخطاء
* [Sentry](https://sentry.io/welcome/) لتتبع الأخطاء
**الاختبار**
- [Jest](https://jestjs.io/)
* [Jest](https://jestjs.io/)
**الأدوات**
- [Yarn](https://yarnpkg.com/)
- [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
* [Yarn](https://yarnpkg.com/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
**التطوير**
- [AWS EKS](https://aws.amazon.com/eks/)
* [AWS EKS](https://aws.amazon.com/eks/)
@@ -48,9 +48,9 @@ git checkout -b feature/your-feature-name
استخدم أسماء فروع وصفية:
- `feature/add-export-button`
- `fix/login-redirect-issue`
- `docs/update-api-guide`
* `feature/add-export-button`
* `fix/login-redirect-issue`
* `docs/update-api-guide`
### أجرِ تعديلاتك
@@ -73,7 +73,7 @@ git push origin feature/your-feature-name
### قائمة التحقق لطلب السحب
- [ ] الشفرة تتبع إرشادات أسلوب المشروع
- [ ] تجتاز الاختبارات محليًا
- [ ] الوثائق محدّثة
- [ ] وصف طلب السحب يوضح التغييرات
* [ ] الشفرة تتبع إرشادات أسلوب المشروع
* [ ] تجتاز الاختبارات محليًا
* [ ] الوثائق محدّثة
* [ ] وصف طلب السحب يوضح التغييرات
@@ -33,10 +33,13 @@ export const MyComponent = () => {
return (
<div>
<input value={myAtom} onChange={(e) => setMyAtom(e.target.value)} />
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
};
}
```
### لا تستخدم `useRef` لتخزين الحالة
@@ -87,7 +90,7 @@ export const PageComponent = () => {
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if (someDependency !== data) {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
@@ -95,7 +98,9 @@ export const PageComponent = () => {
return <div>{data}</div>;
};
export const App = () => <PageComponent />;
export const App = () => (
<PageComponent />
);
```
```tsx
@@ -112,7 +117,7 @@ export const PageData = () => {
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if (someDependency !== data) {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
@@ -179,7 +184,7 @@ const [email, setEmail] = useState('');
#### بعض الكلمات يجب تجنبها في أسماء المتغيرات
- dummy
* dummy
### معالجات الأحداث
@@ -285,8 +290,12 @@ const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
```tsx
// ❌ سيّئ، يحدد المسار النسبي بالكامل
import { CatalogDecorator } from '../../../../../testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '../../../../../testing/decorators/ComponentDecorator';
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
@@ -63,17 +63,17 @@ module1
انظر [GraphQL](https://graphql.org/learn/) لمزيد من التفاصيل.
- الأجزاء
* الأجزاء
الجزء هو جزء قابل لإعادة الاستخدام من استعلام، والذي يمكنك استخدامه في أماكن مختلفة. باستخدام الأجزاء، يصبح من الأسهل تجنب تكرار التعليمات البرمجية.
انظر [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) لمزيد من التفاصيل.
- الاستعلامات
* الاستعلامات
انظر [GraphQL Queries](https://graphql.org/learn/queries/) لمزيد من التفاصيل.
- التحويرات
* التحويرات
انظر [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) لمزيد من التفاصيل.
@@ -85,7 +85,7 @@ module1
تشمل منطق إدارة الحالة. [Jotai](https://jotai.org) يتولّى ذلك.
- المحددات: الذرات المشتقة (باستخدام `createAtomSelector`) تحسب قيماً من ذرات أخرى وتخزن نتائجها في الذاكرة مؤقتاً تلقائياً.
* المحددات: الذرات المشتقة (باستخدام `createAtomSelector`) تحسب قيماً من ذرات أخرى وتخزن نتائجها في الذاكرة مؤقتاً تلقائياً.
لا تزال إدارة الحالة المدمجة في React تتولّى الحالة داخل المكوّن.
@@ -47,22 +47,22 @@ icon: الطرفية
"**التطبيق**"
- "[React](https://react.dev/)"
- "[Apollo](https://www.apollographql.com/docs/)"
- "[GraphQL Codegen](https://the-guild.dev/graphql/codegen)"
- [Jotai](https://jotai.org/)
- "[TypeScript](https://www.typescriptlang.org/)"
* "[React](https://react.dev/)"
* "[Apollo](https://www.apollographql.com/docs/)"
* "[GraphQL Codegen](https://the-guild.dev/graphql/codegen)"
* [Jotai](https://jotai.org/)
* "[TypeScript](https://www.typescriptlang.org/)"
**الاختبار**
- [Jest](https://jestjs.io/)
- "[Storybook](https://storybook.js.org/)"
* [Jest](https://jestjs.io/)
* "[Storybook](https://storybook.js.org/)"
**الأدوات**
- [Yarn](https://yarnpkg.com/)
- "[Craco](https://craco.js.org/docs/)"
- [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
* [Yarn](https://yarnpkg.com/)
* "[Craco](https://craco.js.org/docs/)"
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
## "الهيكلية"
@@ -89,7 +89,9 @@ const ExamplePageWithModal = () => {
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(ExampleHotkeyScopes.ExampleModal);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
@@ -98,13 +100,11 @@ const ExamplePageWithModal = () => {
goBackToPreviousHotkeyScope();
};
return (
<div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>
);
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
@@ -118,7 +118,7 @@ const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
useScopedHotkeys(
Key.Escape,
() => {
onClose();
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
@@ -32,7 +32,7 @@ export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
}
};
```
### الإزاحة
@@ -50,9 +50,7 @@ type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => (
<div>Hello {name}</div>
);
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### امتنع عن استخدام `React.FC` أو `React.FunctionComponent` لتحديد أنواع الخصائص
@@ -93,7 +91,7 @@ const EmailField = ({ value }: EmailFieldProps) => (
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
};
}
```
```tsx
@@ -107,9 +105,9 @@ const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
المبرر:
- نظرة سريعة تسهل معرفة الخصائص التي تمررها التعليمات البرمجية، مما يسهل من الفهم والصيانة.
- يساعد على منع نشوء تعقيدات كبيرة بين المكونات من خلال خصائصها.
- أدوات الفحص تجعل من السهل تحديد الخصائص التي بها أخطاء إملائية أو غير مستخدمة عندما تسرد الخصائص بشكل صريح.
* نظرة سريعة تسهل معرفة الخصائص التي تمررها التعليمات البرمجية، مما يسهل من الفهم والصيانة.
* يساعد على منع نشوء تعقيدات كبيرة بين المكونات من خلال خصائصها.
* أدوات الفحص تجعل من السهل تحديد الخصائص التي بها أخطاء إملائية أو غير مستخدمة عندما تسرد الخصائص بشكل صريح.
## JavaScript
@@ -160,9 +158,9 @@ type MyType = {
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue',
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
@@ -171,7 +169,7 @@ let color = Color.Red;
```tsx
// ✅ جيد، يستخدم حرفا مشفوعا
let color: 'red' | 'green' | 'blue' = 'red';
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL والمكتبات الداخلية
@@ -257,7 +255,7 @@ const StyledButton = styled.button`
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
@@ -278,11 +276,11 @@ import { Meta, StoryObj } from '@storybook/react';
### لماذا لا نوع استيرادات
- **الاتساق**: من خلال تجنب استيراد الأنواع واستخدام أسلوب استيراد واحد لكل من الأنواع والقيم، يبقى الكود موحدًا في أسلوب استيراد الوحدة.
* **الاتساق**: من خلال تجنب استيراد الأنواع واستخدام أسلوب استيراد واحد لكل من الأنواع والقيم، يبقى الكود موحدًا في أسلوب استيراد الوحدة.
- **القراءة**: استيرادات بلا نوع تحسن من قابلية القراءة للرمز من خلال توضيح عندما تقوم باستيراد القيم أو الأنواع. يقلل هذا من الغموض ويجعل من الأسهل فهم الهدف من الرموز المستوردة.
* **القراءة**: استيرادات بلا نوع تحسن من قابلية القراءة للرمز من خلال توضيح عندما تقوم باستيراد القيم أو الأنواع. يقلل هذا من الغموض ويجعل من الأسهل فهم الهدف من الرموز المستوردة.
- **الصيانة**: يعزز الصيانة داخل قاعدة الكود لأن المطوّرين يمكنهم تحديد مواقع استيرادات الأنواع فقط عند استعراض أو تعديل الكود.
* **الصيانة**: يعزز الصيانة داخل قاعدة الكود لأن المطوّرين يمكنهم تحديد مواقع استيرادات الأنواع فقط عند استعراض أو تعديل الكود.
### قاعدة Oxlint
@@ -22,9 +22,9 @@ info: تعرّف على كيفية التعاون باستخدام Figma الخ
من الشريط الجانبي الأيسر، يمكنك الوصول إلى الصفحات المختلفة في Figma الخاص بـ Twenty. هكذا هم مُنظمون:
- **صفحة المكونات:** هذه هي الصفحة الأولى. يستخدمها المصمم لإنشاء وتنظيم العناصر التصميمية القابلة لإعادة الاستخدام في ملف التصميم. على سبيل المثال، الأزرار، الأيقونات، الرموز أو أي مكونات أخرى قابلة لإعادة الاستخدام. تعمل على الحفاظ على التناسق عبر التصميم.
- **الصفحة الرئيسية:** الصفحة الثانية هي الصفحة الرئيسية التي تظهر واجهة المستخدم الكاملة للمشروع. يمكنك الضغط على **_تشغيل_** لاستخدام النموذج الأولي الكامل للتطبيق.
- **صفحات الميزات:** الصفحات الأخرى تكون مخصصة عادة للميزات قيد التقدم. تحتوي على تصميم الميزات أو الوحدات المحددة للتطبيق أو الموقع الإلكتروني. عادة ما تكون لا تزال قيد التقدم.
* **صفحة المكونات:** هذه هي الصفحة الأولى. يستخدمها المصمم لإنشاء وتنظيم العناصر التصميمية القابلة لإعادة الاستخدام في ملف التصميم. على سبيل المثال، الأزرار، الأيقونات، الرموز أو أي مكونات أخرى قابلة لإعادة الاستخدام. تعمل على الحفاظ على التناسق عبر التصميم.
* **الصفحة الرئيسية:** الصفحة الثانية هي الصفحة الرئيسية التي تظهر واجهة المستخدم الكاملة للمشروع. يمكنك الضغط على ***تشغيل*** لاستخدام النموذج الأولي الكامل للتطبيق.
* **صفحات الميزات:** الصفحات الأخرى تكون مخصصة عادة للميزات قيد التقدم. تحتوي على تصميم الميزات أو الوحدات المحددة للتطبيق أو الموقع الإلكتروني. عادة ما تكون لا تزال قيد التقدم.
## نصائح مفيدة
@@ -10,16 +10,14 @@ description: الدليل للمساهمين (أو المطورين الفضول
<Tab title="Linux و macOS">
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [Node v24.5.0](https://nodejs.org/en/download)
- [yarn v4](https://yarnpkg.com/getting-started/install)
- [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
لن يعمل `npm` ، يجب عليك استخدام `yarn` بدلًا من ذلك. يأتي Yarn الآن مع
Node.js، لذا لست بحاجة إلى تثبيته بشكل منفصل. عليك فقط تشغيل `corepack enable`
لتفعيل Yarn إذا لم تقم بذلك بعد.
لن يعمل `npm` ، يجب عليك استخدام `yarn` بدلًا من ذلك. يأتي Yarn الآن مع Node.js، لذا لست بحاجة إلى تثبيته بشكل منفصل.
عليك فقط تشغيل `corepack enable` لتفعيل Yarn إذا لم تقم بذلك بعد.
</Warning>
</Tab>
@@ -28,11 +26,9 @@ description: الدليل للمساهمين (أو المطورين الفضول
1. ثبّت WSL
افتح PowerShell كمسؤول ثم نفّذ:
```powershell
wsl --install
```
يجب أن ترى الآن مطالبة لإعادة تشغيل جهاز الكمبيوتر الخاص بك. إذا لم يكن كذلك، فأعد تشغيله يدويًا.
عند إعادة التشغيل، ستُفتح نافذة PowerShell وسيتم تثبيت Ubuntu. قد يستغرق هذا وقتًا طويلاً.
@@ -51,8 +47,7 @@ git config --global user.email "youremail@domain.com"
3. تثبيت nvm و node.js و yarn
<Warning>
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع
المشاركين لنفس النسخة.
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع المشاركين لنفس النسخة.
</Warning>
```bash
@@ -60,7 +55,6 @@ sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
أغلق وأعد فتح برنامجك الطرفي لاستخدام nvm. ثم قم بتشغيل الأوامر التالية.
```bash
@@ -120,7 +114,6 @@ cd twenty
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="نظام Mac OS">
**الخيار 1 (المفضل):** لتوفير قاعدة بياناتك محليًا مع `brew`:
@@ -177,7 +170,6 @@ cd twenty
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="ويندوز (WSL)">
يجب أن تُنفذ جميع الخطوات التالية في تيرمينال WSL (داخل جهازك الافتراضي)
@@ -195,7 +187,6 @@ cd twenty
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
@@ -216,7 +207,6 @@ cd twenty
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**الخيار 1 (المفضل):** لتوفير Redis الخاص بك محليًا مع `brew`:
@@ -232,7 +222,6 @@ cd twenty
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="ويندوز (WSL)">
**الخيار 1:** لتوفير Redis الخاص بك محليًا:
@@ -242,7 +231,6 @@ cd twenty
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
@@ -260,12 +248,7 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة
حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار
الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true`
في ملف الخادم `.env`. راجع [وضع تعدد مساحات
العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode)
للحصول على التفاصيل.
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true` في ملف الخادم `.env`. راجع [وضع تعدد مساحات العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode) للحصول على التفاصيل.
</Info>
## الخطوة 6: تثبيت التبعيات
@@ -282,20 +265,18 @@ yarn
<Tabs>
<Tab title="Linux">
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا. إذا لم
يكن كذلك، تحقق من [دليل تثبيت
Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)
لتوزيعتك.
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
إذا لم يكن كذلك، تحقق من [دليل تثبيت Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
</Tab>
<Tab title="نظام Mac OS">
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
```bash brew services start redis ```
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
```bash
brew services start redis
```
</Tab>
<Tab title="ويندوز (WSL)">
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا. إذا لم
يكن كذلك، تحقق من [دليل تثبيت
ريديس](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/)
لتوزيعتك.
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
إذا لم يكن كذلك، تحقق من [دليل تثبيت ريديس](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
</Tab>
</Tabs>
@@ -323,14 +304,14 @@ npx nx start
**الواجهة الأمامية**
ستكون واجهة Twenty الأمامية تعمل على [http://localhost:3001](http://localhost:3001).
ستكون واجهة Twenty الأمامية تعمل على [http://localhost:3001](http://localhost:3001).
يمكنك تسجيل الدخول باستخدام حساب العرض التوضيحي الافتراضي: `tim@apple.dev` (كلمة المرور: `tim@apple.dev`)
**الخلفية**
- سيكون خادم Twenty متصلاً ويعمل على [http://localhost:3000](http://localhost:3000)
- يمكن الوصول إلى واجهة برمجة التطبيقات GraphQL في [http://localhost:3000/graphql](http://localhost:3000/graphql)
- يمكن الوصول إلى واجهة برمجة التطبيقات REST في [http://localhost:3000/rest](http://localhost:3000/rest)
* سيكون خادم Twenty متصلاً ويعمل على [http://localhost:3000](http://localhost:3000)
* يمكن الوصول إلى واجهة برمجة التطبيقات GraphQL في [http://localhost:3000/graphql](http://localhost:3000/graphql)
* يمكن الوصول إلى واجهة برمجة التطبيقات REST في [http://localhost:3000/rest](http://localhost:3000/rest)
## استكشاف الأخطاء وإصلاحها
@@ -4,10 +4,7 @@ description: ساهم في تطوير Twenty المفتوح المصدر.
---
<Frame>
<img
src="/images/user-guide/github/github-header.png"
alt="الذكاء الاصطناعي"
/>
<img src="/images/user-guide/github/github-header.png" alt="الذكاء الاصطناعي" />
</Frame>
## نظرة عامة
@@ -16,11 +13,11 @@ Twenty مفتوح المصدر ويرحب بمساهمات المجتمع. سو
## طرق المساهمة
- **الإبلاغ عن الأخطاء**: ساعد في تحديد المشكلات وتوثيقها
- **تقديم الميزات**: اقترح ونفّذ وظائف جديدة
- **تحسين التوثيق**: اجعل وثائقنا أوضح وأكثر فائدة
- **تطوير الواجهة الأمامية**: اعمل على واجهة المستخدم المبنية على React
- **تطوير الواجهة الخلفية**: ساهم في خادم NestJS
* **الإبلاغ عن الأخطاء**: ساعد في تحديد المشكلات وتوثيقها
* **تقديم الميزات**: اقترح ونفّذ وظائف جديدة
* **تحسين التوثيق**: اجعل وثائقنا أوضح وأكثر فائدة
* **تطوير الواجهة الأمامية**: اعمل على واجهة المستخدم المبنية على React
* **تطوير الواجهة الخلفية**: ساهم في خادم NestJS
## البدء
@@ -20,7 +20,7 @@ export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
}
};
```
### الخصائص
@@ -32,9 +32,7 @@ type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => (
<div>Hello {name}</div>
);
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### لا تستخدم نشر الخصائص من متغير واحد
@@ -44,9 +42,7 @@ export const MyComponent = ({ name }: MyComponentProps) => (
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => (
<Other {...{ prop1, prop2 }} />
);
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## إدارة الحالة
@@ -63,25 +59,23 @@ export const myAtomState = createAtomState<string>({
});
```
- فضّل الذرات على تمرير الخصائص عبر المستويات (prop drilling)
- لا تستخدم `useRef` للحالة — استخدم `useState` أو الذرات
- استخدم عائلات الذرات والمحددات للقوائم
* فضّل الذرات على تمرير الخصائص عبر المستويات (prop drilling)
* لا تستخدم `useRef` للحالة — استخدم `useState` أو الذرات
* استخدم عائلات الذرات والمحددات للقوائم
### تجنّب عمليات إعادة التصيير غير الضرورية
- انقل `useEffect` وجلب البيانات إلى مكوّنات جانبية شقيقة (sidecar)
- فضّل معالِجات الأحداث (`handleClick`, `handleChange`) على `useEffect`
- لا تستخدم `React.memo()` — أصلِح السبب الجذري بدلًا من ذلك
- حدّد استخدام `useCallback` / `useMemo`
* انقل `useEffect` وجلب البيانات إلى مكوّنات جانبية شقيقة (sidecar)
* فضّل معالِجات الأحداث (`handleClick`, `handleChange`) على `useEffect`
* لا تستخدم `React.memo()` — أصلِح السبب الجذري بدلًا من ذلك
* حدّد استخدام `useCallback` / `useMemo`
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => {
setData(dep);
}, [dep]);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
@@ -89,9 +83,7 @@ export const Page = () => {
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => {
setData(dep);
}, [dep]);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
@@ -102,11 +94,11 @@ export const Page = () => {
## TypeScript
- **`type` بدلًا من `interface`** — أكثر مرونة وأسهل في التركيب
- **النصوص الحرفية بدل التعدادات** — باستثناء تعدادات codegen الخاصة بـ GraphQL وواجهات برمجة تطبيقات المكتبة الداخلية
- **بدون `any`** — يتم فرض TypeScript الصارم
- **عدم استيراد الأنواع** — استخدم استيرادات عادية (مفروض بواسطة Oxlint `typescript/consistent-type-imports`)
- **استخدم [Zod](https://github.com/colinhacks/zod)** للتحقق وقت التشغيل من الكائنات غير محددة النوع
* **`type` بدلًا من `interface`** — أكثر مرونة وأسهل في التركيب
* **النصوص الحرفية بدل التعدادات** — باستثناء تعدادات codegen الخاصة بـ GraphQL وواجهات برمجة تطبيقات المكتبة الداخلية
* **بدون `any`** — يتم فرض TypeScript الصارم
* **عدم استيراد الأنواع** — استخدم استيرادات عادية (مفروض بواسطة Oxlint `typescript/consistent-type-imports`)
* **استخدم [Zod](https://github.com/colinhacks/zod)** للتحقق وقت التشغيل من الكائنات غير محددة النوع
## JavaScript
@@ -120,13 +112,13 @@ onClick?.();
## التسمية
- **المتغيرات**: camelCase، وصفية (`email` وليس `value`، `fieldMetadata` وليس `fm`)
- **الثوابت**: SCREAMING_SNAKE_CASE
- **الأنواع/الفئات**: PascalCase
- **الملفات/المجلدات**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
- **معالجات الأحداث**: `handleClick` (وليس `onClick` لدالة المعالج)
- **خصائص المكوّن**: ابدأ باسم المكوّن (`ButtonProps`)
- **مكوّنات Styled**: ابدأ بـ `Styled` (`StyledTitle`)
* **المتغيرات**: camelCase، وصفية (`email` وليس `value`، `fieldMetadata` وليس `fm`)
* **الثوابت**: SCREAMING_SNAKE_CASE
* **الأنواع/الفئات**: PascalCase
* **الملفات/المجلدات**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **معالجات الأحداث**: `handleClick` (وليس `onClick` لدالة المعالج)
* **خصائص المكوّن**: ابدأ باسم المكوّن (`ButtonProps`)
* **مكوّنات Styled**: ابدأ بـ `Styled` (`StyledTitle`)
## التنسيق
@@ -179,6 +171,6 @@ front
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
- يمكن للوحدات الاستيراد من وحدات أخرى، لكن يجب أن يبقى `ui/` خاليًا من التبعيات
- استخدم المجلدات الفرعية `internal/` للشيفرة الخاصة بالوحدة
- المكوّنات أقل من 300 سطر، والخدمات أقل من 500 سطر
* يمكن للوحدات الاستيراد من وحدات أخرى، لكن يجب أن يبقى `ui/` خاليًا من التبعيات
* استخدم المجلدات الفرعية `internal/` للشيفرة الخاصة بالوحدة
* المكوّنات أقل من 300 سطر، والخدمات أقل من 500 سطر
@@ -29,15 +29,13 @@ your-app/
```
<Note>
**تنظيم الملفات متروك لك.** يعتمد اكتشاف الكيانات على AST — يعثر SDK على
استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف.
بنية المجلدات أعلاه هي اصطلاح وليست متطلبًا.
**تنظيم الملفات متروك لك.** يعتمد اكتشاف الكيانات على AST — يعثر SDK على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف. بنية المجلدات أعلاه هي اصطلاح وليست متطلبًا.
</Note>
## أنواع الكيانات
| كيان | الغرض | وثائق |
| ---------------------- | ------------------------------------------------------ | ------------------------------------------------------------------- |
| كيان | الغرض | وثائق |
| ---------------------- | ------------------------------------------------------ | -------------------------------------------------------------- |
| **تطبيق** | هوية التطبيق، الأذونات، المتغيرات | [نموذج البيانات](/l/ar/developers/extend/apps/data-model) |
| **دور** | مجموعات الأذونات للكائنات والحقول | [نموذج البيانات](/l/ar/developers/extend/apps/data-model) |
| **الكائن** | جداول بيانات مخصّصة مع حقول | [نموذج البيانات](/l/ar/developers/extend/apps/data-model) |
@@ -52,9 +50,9 @@ your-app/
## العزل
- **الدوال المنطقية** تعمل في عمليات Node.js معزولة على الخادم. لا تصل إلى البيانات إلا عبر عميل API مضبوط الأنواع، ومقيَّد بأذونات دور التطبيق.
- **المكوّنات الأمامية** تعمل ضمن Web Workers باستخدام Remote DOM — معزولة عن الصفحة الرئيسية لكنها تعرض عناصر DOM الأصلية (وليس iframes). تتواصل مع Twenty عبر واجهة API للمضيف تعتمد تمرير الرسائل.
- **الأذونات** تُطبَّق على مستوى واجهة API. يُشتق رمز وقت التشغيل (`TWENTY_APP_ACCESS_TOKEN`) من الدور المعرَّف في `defineApplication()`.
* **الدوال المنطقية** تعمل في عمليات Node.js معزولة على الخادم. لا تصل إلى البيانات إلا عبر عميل API مضبوط الأنواع، ومقيَّد بأذونات دور التطبيق.
* **المكوّنات الأمامية** تعمل ضمن Web Workers باستخدام Remote DOM — معزولة عن الصفحة الرئيسية لكنها تعرض عناصر DOM الأصلية (وليس iframes). تتواصل مع Twenty عبر واجهة API للمضيف تعتمد تمرير الرسائل.
* **الأذونات** تُطبَّق على مستوى واجهة API. يُشتق رمز وقت التشغيل (`TWENTY_APP_ACCESS_TOKEN`) من الدور المعرَّف في `defineApplication()`.
## دورة حياة التطبيق
@@ -75,9 +73,9 @@ your-app/
└─────────────────────────────────────────────────────────┘
```
- **`yarn twenty dev`** — يراقب ملفات المصدر لديك ويزامن التغييرات مباشرةً إلى خادم Twenty متصل. يُعاد توليد عميل API مضبوط الأنواع تلقائيًا عند تغيّر المخطط.
- **`yarn twenty build`** — يجمّع TypeScript، ويضمّن الدوال المنطقية والمكوّنات الأمامية باستخدام esbuild، وينتج ملف بيان.
- **خطّافات ما قبل/ما بعد التثبيت** — دوال منطقية اختيارية تعمل أثناء التثبيت. راجع [الدوال المنطقية](/l/ar/developers/extend/apps/logic-functions) للحصول على التفاصيل.
* **`yarn twenty dev`** — يراقب ملفات المصدر لديك ويزامن التغييرات مباشرةً إلى خادم Twenty متصل. يُعاد توليد عميل API مضبوط الأنواع تلقائيًا عند تغيّر المخطط.
* **`yarn twenty build`** — يجمّع TypeScript، ويضمّن الدوال المنطقية والمكوّنات الأمامية باستخدام esbuild، وينتج ملف بيان.
* **خطّافات ما قبل/ما بعد التثبيت** — دوال منطقية اختيارية تعمل أثناء التثبيت. راجع [الدوال المنطقية](/l/ar/developers/extend/apps/logic-functions) للحصول على التفاصيل.
## الخطوات التالية
@@ -10,12 +10,12 @@ icon: terminal
الملفات الموضوعة في `public/` هي:
- **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا حاجة إلى مصادقة للوصول إليها.
- **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
- **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
- **مستخدمة لبيانات تعريف السوق** — يشير حقلا `logoUrl` و`screenshots` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق.
- **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `public/` أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
- **مضمَّنة في عمليات البناء** — يقوم `yarn twenty build` بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.
* **متاحة للعامة** — بمجرد مزامنتها إلى الخادم، تُقدَّم الأصول عبر عنوان URL عام. لا حاجة إلى مصادقة للوصول إليها.
* **متاحة في المكوّنات الأمامية** — استخدم عناوين الأصول لعرض الصور أو الأيقونات أو أي وسائط داخل مكوّنات React لديك.
* **متاحة في الدوال المنطقية** — أشِر إلى عناوين الأصول في رسائل البريد الإلكتروني أو استجابات واجهات البرمجة أو أي منطق على جهة الخادم.
* **مستخدمة لبيانات تعريف السوق** — يشير حقلا `logoUrl` و`screenshots` في `defineApplication()` إلى ملفات من هذا المجلد (مثل `public/logo.png`). تُعرَض هذه عند نشر تطبيقك في السوق.
* **تُزامَن تلقائيًا في وضع التطوير** — عند إضافة ملف في `public/` أو تحديثه أو حذفه، تتم مزامنته إلى الخادم تلقائيًا. لا حاجة لإعادة التشغيل.
* **مضمَّنة في عمليات البناء** — يقوم `yarn twenty build` بتجميع جميع الأصول العامة ضمن مخرجات التوزيع.
### الوصول إلى الأصول العامة باستخدام `getPublicAssetUrl`
@@ -183,19 +183,15 @@ beforeAll(async () => {
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,
},
JSON.stringify({
remotes: {
local: {
apiUrl: process.env.TWENTY_API_URL,
apiKey: process.env.TWENTY_API_KEY,
},
defaultRemote: 'local',
},
null,
2,
),
defaultRemote: 'local',
}, null, 2),
);
});
```
@@ -340,8 +336,7 @@ yarn twenty logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
يختلف هذا عن `yarn twenty server logs`، الذي يعرض سجلات حاوية Docker. يعرض
`yarn twenty logs` سجلات تنفيذ دوال تطبيقك من خادم Twenty.
يختلف هذا عن `yarn twenty server logs`، الذي يعرض سجلات حاوية Docker. يعرض `yarn twenty logs` سجلات تنفيذ دوال تطبيقك من خادم Twenty.
</Note>
### إلغاء تثبيت تطبيق (`yarn twenty uninstall`)
@@ -7,10 +7,8 @@ icon: database
توفر حزمة `twenty-sdk` دوالّ `defineEntity` لتعريف نموذج بيانات تطبيقك. يجب عليك استخدام `export default defineEntity({...})` لكي يكتشف SDK الكيانات الخاصة بك. تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
<Note>
**تنظيم الملفات يعود إليك.** يعتمد اكتشاف الكيانات على AST — حيث يعثر SDK على
استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف.
تجميع الملفات حسب النوع (مثلًا، `logic-functions/` و`roles/`) هو مجرّد عرف،
وليس متطلبًا.
**تنظيم الملفات يعود إليك.**
يعتمد اكتشاف الكيانات على AST — حيث يعثر SDK على استدعاءات `export default defineEntity(...)` بغض النظر عن مكان وجود الملف. تجميع الملفات حسب النوع (مثلًا، `logic-functions/` و`roles/`) هو مجرّد عرف، وليس متطلبًا.
</Note>
<AccordionGroup>
@@ -52,8 +50,7 @@ export default defineRole({
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
@@ -67,10 +64,10 @@ export default defineRole({
يجب أن يحتوي كل تطبيق على استدعاء واحد فقط لـ `defineApplication` يصف:
- **الهوية**: المعرّفات، اسم العرض، والوصف.
- **الأذونات**: أيُّ دورٍ تستخدمه وظائفه ومكوّناته الأمامية.
- **(اختياري) المتغيرات**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
- **(اختياري) دوال ما قبل التثبيت/ما بعد التثبيت**: دوال منطقية تعمل قبل التثبيت أو بعده.
* **الهوية**: المعرّفات، اسم العرض، والوصف.
* **الأذونات**: أيُّ دورٍ تستخدمه وظائفه ومكوّناته الأمامية.
* **(اختياري) المتغيرات**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
* **(اختياري) دوال ما قبل التثبيت/ما بعد التثبيت**: دوال منطقية تعمل قبل التثبيت أو بعده.
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
@@ -94,11 +91,10 @@ export default defineApplication({
```
الملاحظات:
- حقول `universalIdentifier` هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
- `applicationVariables` تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
- `defaultRoleUniversalIdentifier` يجب أن يُشير إلى دور مُعرَّف باستخدام `defineRole()` (انظر أعلاه).
- يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في `defineApplication()`.
* حقول `universalIdentifier` هي معرّفات حتمية تملكها أنت. أنشِئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
* `applicationVariables` تصبح متغيرات بيئة لوظائفك ومكوّناتك الأمامية (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
* `defaultRoleUniversalIdentifier` يجب أن يُشير إلى دور مُعرَّف باستخدام `defineRole()` (انظر أعلاه).
* يتم اكتشاف دوال ما قبل التثبيت وما بعده تلقائيًا أثناء بناء البيان — لا حاجة للإشارة إليها في `defineApplication()`.
#### بيانات التعريف لسوق التطبيقات
@@ -120,9 +116,9 @@ export default defineApplication({
يُحدّد الحقل `defaultRoleUniversalIdentifier` في `application-config.ts` الدور الافتراضي الذي تستخدمه وظائف المنطق والمكوّنات الأمامية في تطبيقك. راجع `defineRole` أعلاه للحصول على التفاصيل.
- رمز وقت التشغيل المحقون باسم `TWENTY_APP_ACCESS_TOKEN` مستمد من هذا الدور.
- العميل مضبوط الأنواع مقيَّد بالأذونات الممنوحة لذلك الدور.
- اتبع مبدأ أقل الامتياز: أنشئ دورًا مخصصًا يضم فقط الأذونات التي تحتاجها وظائفك.
* رمز وقت التشغيل المحقون باسم `TWENTY_APP_ACCESS_TOKEN` مستمد من هذا الدور.
* العميل مضبوط الأنواع مقيَّد بالأذونات الممنوحة لذلك الدور.
* اتبع مبدأ أقل الامتياز: أنشئ دورًا مخصصًا يضم فقط الأذونات التي تحتاجها وظائفك.
##### الدور الافتراضي للوظيفة
@@ -154,15 +150,14 @@ export default defineRole({
يُشار إلى `universalIdentifier` لهذا الدور في `application-config.ts` باسم `defaultRoleUniversalIdentifier`:
- **\*.role.ts** يحدد ما يمكن أن يفعله الدور.
- **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
* **\*.role.ts** يحدد ما يمكن أن يفعله الدور.
* **application-config.ts** يشير إلى ذلك الدور بحيث ترث وظائفك أذوناته.
الملاحظات:
- ابدأ من الدور المُنشأ بالقالب، ثم قيّده تدريجيًا باتباع مبدأ أقل الامتياز.
- استبدل `objectPermissions` و`fieldPermissions` بالكائنات والحقول التي تحتاجها وظائفك فعليًا.
- `permissionFlags` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
- اطّلع على مثال عملي: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
* ابدأ من الدور المُنشأ بالقالب، ثم قيّده تدريجيًا باتباع مبدأ أقل الامتياز.
* استبدل `objectPermissions` و`fieldPermissions` بالكائنات والحقول التي تحتاجها وظائفك فعليًا.
* `permissionFlags` تتحكم في الوصول إلى القدرات على مستوى المنصة. اجعلها في حدّها الأدنى.
* اطّلع على مثال عملي: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
</Accordion>
<Accordion title="defineObject" description="تعريف كائنات مخصصة مع حقول">
@@ -218,30 +213,10 @@ export default defineObject({
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{
value: PostCardStatus.DRAFT,
label: 'Draft',
position: 0,
color: 'gray',
},
{
value: PostCardStatus.SENT,
label: 'Sent',
position: 1,
color: 'orange',
},
{
value: PostCardStatus.DELIVERED,
label: 'Delivered',
position: 2,
color: 'green',
},
{
value: PostCardStatus.RETURNED,
label: 'Returned',
position: 3,
color: 'orange',
},
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
@@ -259,18 +234,18 @@ export default defineObject({
النقاط الرئيسية:
- استخدم `defineObject()` للحصول على تحقق مدمج ودعم أفضل من IDE.
- `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
- يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
- المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
- يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
* استخدم `defineObject()` للحصول على تحقق مدمج ودعم أفضل من IDE.
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
<Note>
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty
تلقائيًا حقولًا قياسية مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy`
و`updatedBy` و`deletedAt`. لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط
حقولك المخصصة. يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في
مصفوفة `fields` الخاصة بك، لكن هذا غير مستحسن.
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt`.
لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة `fields` الخاصة بك،
لكن هذا غير مستحسن.
</Note>
</Accordion>
@@ -297,10 +272,9 @@ export default defineField({
```
النقاط الرئيسية:
- `objectUniversalIdentifier` يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` المُصدَّر من `twenty-sdk`.
- عند تعريف الحقول بشكل مضمّن في `defineObject()`، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
- `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
* `objectUniversalIdentifier` يحدّد الكائن الهدف. بالنسبة للكائنات القياسية، استخدم `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` المُصدَّر من `twenty-sdk`.
* عند تعريف الحقول بشكل مضمّن في `defineObject()`، **لا** تحتاج إلى `objectUniversalIdentifier` — إذ يُورَّث من الكائن الأب.
* `defineField()` هي الطريقة الوحيدة لإضافة حقول إلى كائنات لم تُنشئها باستخدام `defineObject()`.
</Accordion>
<Accordion title="defineField — حقول العلاقات" description="وصِل الكائنات معًا بعلاقات ثنائية الاتجاه">
@@ -335,8 +309,7 @@ import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID =
'a1111111-1111-1111-1111-111111111111';
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
@@ -347,8 +320,7 @@ export default defineField({
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
@@ -359,12 +331,7 @@ export default defineField({
**الخطوة 2: عرّف جانب MANY_TO_ONE على PostCardRecipient** (جانب "العديد" — يحمل المفتاح الخارجي):
```ts src/fields/post-card-on-post-card-recipient.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
} from 'twenty-sdk/define';
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
@@ -380,8 +347,7 @@ export default defineField({
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
@@ -392,10 +358,7 @@ export default defineField({
```
<Note>
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُحيل كلٌ منهما إلى
`universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر
معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام
البناء بحلّها في وقت التجميع.
**الاستيرادات الدائرية:** كلا حقلي العلاقة يُحيل كلٌ منهما إلى `universalIdentifier` الخاص بالآخر. لتجنّب مشكلات الاستيراد الدائري، صدّر معرّفات الحقول كثوابت مسمّاة من كل ملف، واستوردها في الملف الآخر. يقوم نظام البناء بحلّها في وقت التجميع.
</Note>
#### الربط مع الكائنات القياسية
@@ -413,8 +376,7 @@ import {
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID =
'd4444444-4444-4444-4444-444444444444';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
@@ -426,8 +388,7 @@ export default defineField({
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
SELF_HOSTING_USER_REVERSE_FIELD_ID,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
@@ -462,10 +423,8 @@ export default defineObject({
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier:
POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier:
POST_CARD_RECIPIENTS_FIELD_ID,
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
@@ -476,7 +435,6 @@ export default defineObject({
],
});
```
</Accordion>
</AccordionGroup>
@@ -500,8 +458,8 @@ yarn twenty add frontComponent
### أنواع الكيانات المتاحة
| نوع الكيان | أمر | الملف المُولَّد |
| ------------------ | ------------------------------------ | -------------------------------------- |
| نوع الكيان | أمر | الملف المُولَّد |
| ------------------ | ------------------------------------ | ------------------------------------------------------- |
| كائن | `yarn twenty add object` | `src/objects/\<name>.ts` |
| الحقل | `yarn twenty add field` | `src/fields/\<name>.ts` |
| دالة منطقية | `yarn twenty add logicFunction` | `src/logic-functions/\<name>.ts` |
@@ -10,8 +10,8 @@ icon: window-maximize
يمكن عرض مكوّنات الواجهة الأمامية في موقعين داخل Twenty:
- **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
- **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
* **اللوحة الجانبية** — المكوّنات غير عديمة الرأس تفتح في اللوحة الجانبية اليمنى. هذا هو السلوك الافتراضي عندما يتم تشغيل مكوّن واجهة أمامية من قائمة الأوامر.
* **الويدجت (لوحات المعلومات وصفحات السجلات)** — يمكن تضمين مكوّنات الواجهة الأمامية كويدجت داخل تخطيطات الصفحات. عند تكوين لوحة معلومات أو تخطيط صفحة سجل، يمكن للمستخدمين إضافة ويدجت لمكوّن واجهة أمامية.
## مثال أساسي
@@ -47,11 +47,8 @@ export default defineFrontComponent({
بعد المزامنة باستخدام `yarn twenty dev` (أو تشغيل الأمر لمرة واحدة `yarn twenty dev --once`)، يظهر الإجراء السريع في الزاوية العلوية اليمنى من الصفحة:
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/quick-action.png"
alt="زر إجراء سريع في الزاوية العلوية اليمنى"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="زر إجراء سريع في الزاوية العلوية اليمنى" />
</div>
انقره لعرض المكوّن مضمنًا داخل الصفحة.
@@ -88,10 +85,7 @@ const SyncTracker = () => {
const recordId = useRecordId();
useEffect(() => {
enqueueSnackbar({
message: `Tracking record ${recordId}`,
variant: 'info',
});
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
}, [recordId]);
return null;
@@ -114,10 +108,10 @@ export default defineFrontComponent({
استوردها من `twenty-sdk/command`:
- **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
- **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
- **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
- **`CommandOpenSidePanelPage`** — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: `page`، `pageTitle`، `pageIcon`.
* **`Command`** — يشغّل رد نداء غير متزامن عبر الخاصية `execute`.
* **`CommandLink`** — ينتقل إلى مسار في التطبيق. الخصائص: `to`، `params`، `queryParams`، `options`.
* **`CommandModal`** — يفتح نافذة تأكيد منبثقة. إذا أكّد المستخدم، ينفّذ رد النداء `execute`. الخصائص: `title`، `subtitle`، `execute`، `confirmButtonText`، `confirmButtonAccent`.
* **`CommandOpenSidePanelPage`** — يفتح صفحة محدّدة في اللوحة الجانبية. الخصائص: `page`، `pageTitle`، `pageIcon`.
فيما يلي مثال كامل لمكوّن واجهة أمامية عديم الرأس يستخدم `Command` لتشغيل إجراء من قائمة الأوامر:
@@ -396,12 +390,12 @@ export default defineFrontComponent({
تدعم المكوّنات الأمامية عدة أساليب للتنسيق. يمكنك استخدام:
- **أنماط مضمنة** — `style={{ color: 'red' }}`
- **مكوّنات Twenty لواجهة المستخدم** — استورد من `twenty-sdk/ui` (Button وTag وStatus وChip وAvatar وغيرها)
- **Emotion** — CSS-in-JS مع `@emotion/react`
- **Styled-components** — أنماط `styled.div`
- **Tailwind CSS** — أصناف مساعدة
- **أي مكتبة CSS-in-JS** متوافقة مع React
* **أنماط مضمنة** — `style={{ color: 'red' }}`
* **مكوّنات Twenty لواجهة المستخدم** — استورد من `twenty-sdk/ui` (Button وTag وStatus وChip وAvatar وغيرها)
* **Emotion** — CSS-in-JS مع `@emotion/react`
* **Styled-components** — أنماط `styled.div`
* **Tailwind CSS** — أصناف مساعدة
* **أي مكتبة CSS-in-JS** متوافقة مع React
```tsx
import { defineFrontComponent } from 'twenty-sdk/define';
@@ -12,9 +12,9 @@ description: أنشئ أول تطبيق Twenty خلال دقائق.
قبل أن تبدأ، تأكّد من تثبيت ما يلي على جهازك:
- **Node.js 24+** — [نزّل من هنا](https://nodejs.org/)
- **Yarn 4** — يأتي مع Node.js عبر Corepack. قم بتمكينه عبر تشغيل `corepack enable`
- **Docker** — [نزّل من هنا](https://www.docker.com/products/docker-desktop/). مطلوب لتشغيل مثيل محلي من Twenty. غير مطلوب إذا كان لديك خادم Twenty قيد التشغيل بالفعل.
* **Node.js 24+** — [نزّل من هنا](https://nodejs.org/)
* **Yarn 4** — يأتي مع Node.js عبر Corepack. قم بتمكينه عبر تشغيل `corepack enable`
* **Docker** — [نزّل من هنا](https://www.docker.com/products/docker-desktop/). مطلوب لتشغيل مثيل محلي من Twenty. غير مطلوب إذا كان لديك خادم Twenty قيد التشغيل بالفعل.
## قم بإنشاء تطبيقك الأول
@@ -36,28 +36,22 @@ npx create-twenty-app@latest my-twenty-app
> **هل ترغب في إعداد مثيل محلي من Twenty؟**
- **اكتب `yes`** (موصى به) — سيؤدي ذلك إلى سحب صورة Docker `twenty-app-dev` وبدء تشغيل خادم Twenty محلي على المنفذ `2020`. تأكّد من أن Docker قيد التشغيل قبل المتابعة.
- **اكتب `no`** — اختر هذا إذا كان لديك خادم Twenty يعمل محليًا بالفعل.
* **اكتب `yes`** (موصى به) — سيؤدي ذلك إلى سحب صورة Docker `twenty-app-dev` وبدء تشغيل خادم Twenty محلي على المنفذ `2020`. تأكّد من أن Docker قيد التشغيل قبل المتابعة.
* **اكتب `no`** — اختر هذا إذا كان لديك خادم Twenty يعمل محليًا بالفعل.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/start-instance.png"
alt="هل يجب بدء المثيل المحلي؟"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="هل يجب بدء المثيل المحلي؟" />
</div>
### سجّل الدخول إلى مساحة العمل الخاصة بك
بعد ذلك، ستُفتح نافذة متصفح تعرض صفحة تسجيل الدخول الخاصة بـ Twenty. سجّل الدخول باستخدام حساب العرض التوضيحي المُجهَّز مسبقًا:
- **البريد الإلكتروني:** `tim@apple.dev`
- **كلمة المرور:** `tim@apple.dev`
* **البريد الإلكتروني:** `tim@apple.dev`
* **كلمة المرور:** `tim@apple.dev`
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/login.png"
alt="شاشة تسجيل الدخول إلى Twenty"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/login.png" alt="شاشة تسجيل الدخول إلى Twenty" />
</div>
### قم بتفويض التطبيق
@@ -66,20 +60,14 @@ npx create-twenty-app@latest my-twenty-app
انقر **Authorize** للمتابعة.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/authorize.png"
alt="شاشة تفويض واجهة الأوامر (CLI) الخاصة بـ Twenty"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/authorize.png" alt="شاشة تفويض واجهة الأوامر (CLI) الخاصة بـ Twenty" />
</div>
بمجرد منح التفويض، ستؤكّد الطرفية أن كل شيء قد تم إعداده.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/scaffolded.png"
alt="تم إنشاء هيكل التطبيق بنجاح"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="تم إنشاء هيكل التطبيق بنجاح" />
</div>
### ابدأ التطوير
@@ -100,17 +88,11 @@ yarn twenty dev --verbose
```
<Warning>
وضع التطوير متاح فقط على مثيلات Twenty التي تعمل في وضع التطوير
(`NODE_ENV=development`). المثيلات الإنتاجية ترفض طلبات مزامنة وضع التطوير.
استخدم `yarn twenty deploy` للنشر إلى خوادم الإنتاج — اطّلع على [نشر
التطبيقات](/l/ar/developers/extend/apps/publishing) للتفاصيل.
وضع التطوير متاح فقط على مثيلات Twenty التي تعمل في وضع التطوير (`NODE_ENV=development`). المثيلات الإنتاجية ترفض طلبات مزامنة وضع التطوير. استخدم `yarn twenty deploy` للنشر إلى خوادم الإنتاج — اطّلع على [نشر التطبيقات](/l/ar/developers/extend/apps/publishing) للتفاصيل.
</Warning>
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/dev.jpg"
alt="مخرجات الطرفية في وضع التطوير"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="مخرجات الطرفية في وضع التطوير" />
</div>
#### مزامنة لمرة واحدة باستخدام `yarn twenty dev --once`
@@ -132,38 +114,26 @@ yarn twenty dev --once
افتح [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) في متصفحك. انتقل إلى **Settings > Apps** واختر علامة التبويب **Developer**. يُفترض أن ترى تطبيقك مُدرجًا تحت **Your Apps**:
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-1.png"
alt="قائمة &#x22;Your Apps&#x22; تعرض &#x22;My twenty app&#x22;"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="قائمة &#x22;Your Apps&#x22; تعرض &#x22;My twenty app&#x22;" />
</div>
انقر على **My twenty app** لفتح **تسجيل التطبيق** الخاص به. التسجيل عبارة عن سجل على مستوى الخادم يصف تطبيقك — اسمه، والمعرّف الفريد، وبيانات اعتماد OAuth، والمصدر (محلي، npm، أو tarball). يُخزَّن على الخادم، وليس داخل أي مساحة عمل محددة. عند تثبيت تطبيق في مساحة عمل، ينشئ Twenty **تطبيقًا** بنطاق مساحة العمل يُشير مرة أخرى إلى هذا التسجيل. يمكن تثبيت تسجيل واحد عبر عدة مساحات عمل على الخادم نفسه.
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-2.png"
alt="تفاصيل تسجيل التطبيق"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="تفاصيل تسجيل التطبيق" />
</div>
انقر **View installed app** لعرض التطبيق المثبّت. تعرض علامة التبويب **About** الإصدار الحالي وخيارات الإدارة:
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-3.png"
alt="التطبيق المثبّت — علامة تبويب About"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="التطبيق المثبّت — علامة تبويب About" />
</div>
انتقل إلى علامة التبويب **Content** لمشاهدة كل ما يقدمه تطبيقك — الكائنات، والحقول، ودوال المنطق، والوكلاء:
<div style={{ textAlign: 'center' }}>
<img
src="/images/docs/developers/extends/apps/app-in-ui-4.png"
alt="التطبيق المثبّت — علامة تبويب Content"
/>
<div style={{textAlign: 'center'}}>
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="التطبيق المثبّت — علامة تبويب Content" />
</div>
أنت جاهز تمامًا! حرّر أي ملف في `src/` وسيتم التقاط التغييرات تلقائيًا.
@@ -271,8 +241,7 @@ npx create-twenty-app@latest my-twenty-app --example postcard
يعمل مثيل الاختبار في حاوية Docker خاصة به (`twenty-app-dev-test`) مع وحدات تخزين مخصصة (`twenty-app-dev-test-data`, `twenty-app-dev-test-storage`) وتهيئة مستقلة، بحيث يمكنه العمل بالتوازي مع مثيلك الرئيسي من دون تعارضات. اجمع `--test` مع `--port` لتجاوز القيمة الافتراضية 2021.
<Note>
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker
not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
يتطلّب الخادم أن يكون **Docker** قيد التشغيل. إذا ظهرت لك رسالة خطأ "Docker not running"، فتأكّد من تشغيل Docker Desktop (أو خادوم Docker).
</Note>
## إعداد يدوي (بدون المهيئ)
@@ -298,17 +267,16 @@ yarn add twenty-sdk twenty-client-sdk
يمكنك الآن تشغيل `yarn twenty dev`، و`yarn twenty help`، وجميع الأوامر الأخرى.
<Note>
لا تثبّت `twenty-sdk` عالميًا. استخدمه دائمًا كاعتماد محلي للمشروع بحيث يتمكن
كل مشروع من تثبيت إصداره الخاص.
لا تثبّت `twenty-sdk` عالميًا. استخدمه دائمًا كاعتماد محلي للمشروع بحيث يتمكن كل مشروع من تثبيت إصداره الخاص.
</Note>
## استكشاف الأخطاء وإصلاحها
إذا واجهت مشاكل:
- تأكّد من أن **Docker قيد التشغيل** قبل تشغيل أداة إنشاء الهيكل مع مثيل محلي.
- تأكّد من أنك تستخدم **Node.js 24+** (`node -v` للتحقق).
- تأكّد من **تمكين Corepack** (`corepack enable`) حتى يتوفر Yarn 4.
- جرّب حذف `node_modules` وتشغيل `yarn install` مرة أخرى إذا بدت الاعتمادات معطّلة.
* تأكّد من أن **Docker قيد التشغيل** قبل تشغيل أداة إنشاء الهيكل مع مثيل محلي.
* تأكّد من أنك تستخدم **Node.js 24+** (`node -v` للتحقق).
* تأكّد من **تمكين Corepack** (`corepack enable`) حتى يتوفر Yarn 4.
* جرّب حذف `node_modules` وتشغيل `yarn install` مرة أخرى إذا بدت الاعتمادات معطّلة.
ما زلت عالقًا؟ اطلب المساعدة على [خادم Twenty على Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).

Some files were not shown because too many files have changed in this diff Show More