diff --git a/packages/twenty-docs/l/de/developers/extend/apps/connections.mdx b/packages/twenty-docs/l/de/developers/extend/apps/connections.mdx
new file mode 100644
index 0000000000..02f1ce90fd
--- /dev/null
+++ b/packages/twenty-docs/l/de/developers/extend/apps/connections.mdx
@@ -0,0 +1,193 @@
+---
+title: Connections
+description: Let your app act on a user's behalf in third-party services via OAuth.
+icon: plug
+---
+
+Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
+
+Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
+
+
+
+
+
+A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
+
+A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
+
+```ts src/connection-providers/linear-connection.ts
+import { defineConnectionProvider } from 'twenty-sdk/define';
+
+export default defineConnectionProvider({
+ universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
+ name: 'linear',
+ displayName: 'Linear',
+ icon: 'IconBrandLinear',
+ type: 'oauth',
+ oauth: {
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
+ scopes: ['read', 'write'],
+ // These must match keys in `defineApplication.serverVariables` below.
+ clientIdVariable: 'LINEAR_CLIENT_ID',
+ clientSecretVariable: 'LINEAR_CLIENT_SECRET',
+ // Optional: defaults to 'json'. Some providers (Linear, Slack) want
+ // 'form-urlencoded' for the token request.
+ tokenRequestContentType: 'form-urlencoded',
+ // Optional: defaults to true. Disable only if the provider rejects PKCE.
+ usePkce: false,
+ // Optional: extra query params on the authorize URL.
+ // authorizationParams: { prompt: 'consent' },
+ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
+ // revokeEndpoint: 'https://example.com/oauth/revoke',
+ },
+});
+```
+
+```ts src/application.config.ts
+import { defineApplication } from 'twenty-sdk/define';
+
+export default defineApplication({
+ universalIdentifier: '...',
+ displayName: 'Linear',
+ description: 'Connect Linear to Twenty.',
+ defaultRoleUniversalIdentifier: '...',
+ // OAuth client credentials live on the app registration (one OAuth app per
+ // Twenty server, configured by the admin) — not per-workspace. Declare them
+ // as serverVariables so the admin can fill them in once for all installs.
+ serverVariables: {
+ LINEAR_CLIENT_ID: {
+ description: 'OAuth client ID from your Linear OAuth application.',
+ isSecret: false,
+ isRequired: true,
+ },
+ LINEAR_CLIENT_SECRET: {
+ description: 'OAuth client secret from your Linear OAuth application.',
+ isSecret: true,
+ isRequired: true,
+ },
+ },
+});
+```
+
+Hauptpunkte:
+
+* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
+* `displayName` shows in the per-app settings tab and in the AI tool list.
+* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
+* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
+* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
+* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
+
+The OAuth callback URL your provider needs to whitelist is:
+
+```
+https:///apps/oauth/callback
+```
+
+
+
+
+
+Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
+
+```ts src/logic-functions/handlers/create-linear-issue-handler.ts
+import { listConnections } from 'twenty-sdk/logic-function';
+
+export const createLinearIssueHandler = async (input: {
+ teamId?: string;
+ title?: string;
+}) => {
+ if (!input.teamId || !input.title) {
+ return { success: false, error: 'teamId and title are required' };
+ }
+
+ const connections = await listConnections({ providerName: 'linear' });
+
+ // Workspace-shared credentials win when present; fall back to the first
+ // user-visibility one. For HTTP-route triggers you typically pick the
+ // request user's connection via event.userWorkspaceId instead.
+ const connection =
+ connections.find((c) => c.visibility === 'workspace') ?? connections[0];
+
+ if (!connection) {
+ return {
+ success: false,
+ error:
+ 'Linear is not connected. Open the app settings and click "Add connection".',
+ };
+ }
+
+ // Use connection.accessToken to call the third-party API.
+ const response = await fetch('https://api.linear.app/graphql', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
+ }),
+ });
+
+ return { success: response.ok };
+};
+```
+
+Each connection has:
+
+| Feld | Beschreibung |
+| ------------------ | -------------------------------------------------------------------------------------------------------- |
+| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
+| `sichtbarkeit` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
+| `geltungsbereiche` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
+| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
+| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
+| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
+| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
+
+Hauptpunkte:
+
+* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
+* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
+* `getConnection(id)` is the single-row equivalent.
+
+
+
+
+
+When a user clicks "Add connection," they're prompted to pick a visibility:
+
+* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
+* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
+
+Use the right one for each handler:
+
+```ts
+// HTTP-route trigger — prefer the request user's own connection.
+const conn =
+ connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
+ connections.find((c) => c.visibility === 'workspace');
+
+// Cron trigger — no request user; only shared credentials are sensible.
+const conn = connections.find((c) => c.visibility === 'workspace');
+```
+
+Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
+
+
+
+
+
+For each connection provider, the server admin needs to register an OAuth app at the third party first.
+
+1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
+2. Set the **Redirect URI** to `\/apps/oauth/callback`.
+3. Copy the generated **Client ID** and **Client Secret**.
+4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
+5. Workspace members can then add connections from the per-app **Connections** section.
+
+
+
+
diff --git a/packages/twenty-docs/l/de/developers/extend/apps/publishing.mdx b/packages/twenty-docs/l/de/developers/extend/apps/publishing.mdx
index 03e2972b0c..9c739c4d85 100644
--- a/packages/twenty-docs/l/de/developers/extend/apps/publishing.mdx
+++ b/packages/twenty-docs/l/de/developers/extend/apps/publishing.mdx
@@ -77,6 +77,39 @@ Pre-Release-Tags funktionieren wie erwartet: Das Erhöhen von `1.0.0-rc.1` → `
{/* TODO: add screenshot of the Upgrade button */}
+### Server version compatibility
+
+If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
+
+```json filename="package.json"
+{
+ "name": "twenty-my-app",
+ "version": "1.0.0",
+ "engines": {
+ "node": "^24.5.0",
+ "twenty": ">=2.3.0"
+ }
+}
+```
+
+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 |
+| `>=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` |
+
+**What happens at deploy and install time:**
+
+* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
+* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
+* If the server has no `APP_VERSION` configured, the check is skipped.
+
+
+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.
+
+
## Automatisiertes CI/CD (vorgefertigte Workflows)
Apps, die mit `create-twenty-app` erzeugt wurden, enthalten von Haus aus zwei GitHub-Actions-Workflows unter `.github/workflows/`. Sie sind einsatzbereit, sobald Sie das Repository zu GitHub pushen — für CI ist keine zusätzliche Einrichtung erforderlich, und für CD ist nur ein einziges Secret nötig.
diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/connections.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/connections.mdx
new file mode 100644
index 0000000000..e46666c0c2
--- /dev/null
+++ b/packages/twenty-docs/l/pt/developers/extend/apps/connections.mdx
@@ -0,0 +1,193 @@
+---
+title: Connections
+description: Let your app act on a user's behalf in third-party services via OAuth.
+icon: plug
+---
+
+Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
+
+Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
+
+
+
+
+
+A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
+
+A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
+
+```ts src/connection-providers/linear-connection.ts
+import { defineConnectionProvider } from 'twenty-sdk/define';
+
+export default defineConnectionProvider({
+ universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
+ name: 'linear',
+ displayName: 'Linear',
+ icon: 'IconBrandLinear',
+ type: 'oauth',
+ oauth: {
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
+ scopes: ['read', 'write'],
+ // These must match keys in `defineApplication.serverVariables` below.
+ clientIdVariable: 'LINEAR_CLIENT_ID',
+ clientSecretVariable: 'LINEAR_CLIENT_SECRET',
+ // Optional: defaults to 'json'. Some providers (Linear, Slack) want
+ // 'form-urlencoded' for the token request.
+ tokenRequestContentType: 'form-urlencoded',
+ // Optional: defaults to true. Disable only if the provider rejects PKCE.
+ usePkce: false,
+ // Optional: extra query params on the authorize URL.
+ // authorizationParams: { prompt: 'consent' },
+ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
+ // revokeEndpoint: 'https://example.com/oauth/revoke',
+ },
+});
+```
+
+```ts src/application.config.ts
+import { defineApplication } from 'twenty-sdk/define';
+
+export default defineApplication({
+ universalIdentifier: '...',
+ displayName: 'Linear',
+ description: 'Connect Linear to Twenty.',
+ defaultRoleUniversalIdentifier: '...',
+ // OAuth client credentials live on the app registration (one OAuth app per
+ // Twenty server, configured by the admin) — not per-workspace. Declare them
+ // as serverVariables so the admin can fill them in once for all installs.
+ serverVariables: {
+ LINEAR_CLIENT_ID: {
+ description: 'OAuth client ID from your Linear OAuth application.',
+ isSecret: false,
+ isRequired: true,
+ },
+ LINEAR_CLIENT_SECRET: {
+ description: 'OAuth client secret from your Linear OAuth application.',
+ isSecret: true,
+ isRequired: true,
+ },
+ },
+});
+```
+
+Pontos-chave:
+
+* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
+* `displayName` shows in the per-app settings tab and in the AI tool list.
+* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
+* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
+* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
+* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
+
+The OAuth callback URL your provider needs to whitelist is:
+
+```
+https:///apps/oauth/callback
+```
+
+
+
+
+
+Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
+
+```ts src/logic-functions/handlers/create-linear-issue-handler.ts
+import { listConnections } from 'twenty-sdk/logic-function';
+
+export const createLinearIssueHandler = async (input: {
+ teamId?: string;
+ title?: string;
+}) => {
+ if (!input.teamId || !input.title) {
+ return { success: false, error: 'teamId and title are required' };
+ }
+
+ const connections = await listConnections({ providerName: 'linear' });
+
+ // Workspace-shared credentials win when present; fall back to the first
+ // user-visibility one. For HTTP-route triggers you typically pick the
+ // request user's connection via event.userWorkspaceId instead.
+ const connection =
+ connections.find((c) => c.visibility === 'workspace') ?? connections[0];
+
+ if (!connection) {
+ return {
+ success: false,
+ error:
+ 'Linear is not connected. Open the app settings and click "Add connection".',
+ };
+ }
+
+ // Use connection.accessToken to call the third-party API.
+ const response = await fetch('https://api.linear.app/graphql', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
+ }),
+ });
+
+ return { success: response.ok };
+};
+```
+
+Each connection has:
+
+| Campo | Descrição |
+| ----------------- | -------------------------------------------------------------------------------------------------------- |
+| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
+| `visibilidade` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
+| `escopos` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
+| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
+| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
+| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
+| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
+
+Pontos-chave:
+
+* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
+* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
+* `getConnection(id)` is the single-row equivalent.
+
+
+
+
+
+When a user clicks "Add connection," they're prompted to pick a visibility:
+
+* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
+* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
+
+Use the right one for each handler:
+
+```ts
+// HTTP-route trigger — prefer the request user's own connection.
+const conn =
+ connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
+ connections.find((c) => c.visibility === 'workspace');
+
+// Cron trigger — no request user; only shared credentials are sensible.
+const conn = connections.find((c) => c.visibility === 'workspace');
+```
+
+Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
+
+
+
+
+
+For each connection provider, the server admin needs to register an OAuth app at the third party first.
+
+1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
+2. Set the **Redirect URI** to `\/apps/oauth/callback`.
+3. Copy the generated **Client ID** and **Client Secret**.
+4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
+5. Workspace members can then add connections from the per-app **Connections** section.
+
+
+
+
diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/publishing.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/publishing.mdx
index 743967a752..3ad6233f94 100644
--- a/packages/twenty-docs/l/pt/developers/extend/apps/publishing.mdx
+++ b/packages/twenty-docs/l/pt/developers/extend/apps/publishing.mdx
@@ -77,6 +77,39 @@ Tags de pré-lançamento funcionam como esperado: incrementar `1.0.0-rc.1` → `
{/* TODO: add screenshot of the Upgrade button */}
+### Server version compatibility
+
+If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
+
+```json filename="package.json"
+{
+ "name": "twenty-my-app",
+ "version": "1.0.0",
+ "engines": {
+ "node": "^24.5.0",
+ "twenty": ">=2.3.0"
+ }
+}
+```
+
+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 |
+| `>=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` |
+
+**What happens at deploy and install time:**
+
+* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
+* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
+* If the server has no `APP_VERSION` configured, the check is skipped.
+
+
+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.
+
+
## CI/CD automatizado (fluxos de trabalho pré-configurados)
Os apps gerados com `create-twenty-app` já vêm com dois fluxos de trabalho do GitHub Actions prontos, em `.github/workflows/`. Eles estão prontos para executar assim que você fizer push do repositório para o GitHub — nenhuma configuração extra é necessária para CI, e CD requer apenas um único segredo.
diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/connections.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/connections.mdx
new file mode 100644
index 0000000000..2f026bf4eb
--- /dev/null
+++ b/packages/twenty-docs/l/ru/developers/extend/apps/connections.mdx
@@ -0,0 +1,193 @@
+---
+title: Connections
+description: Let your app act on a user's behalf in third-party services via OAuth.
+icon: plug
+---
+
+Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
+
+Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
+
+
+
+
+
+A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
+
+A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
+
+```ts src/connection-providers/linear-connection.ts
+import { defineConnectionProvider } from 'twenty-sdk/define';
+
+export default defineConnectionProvider({
+ universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
+ name: 'linear',
+ displayName: 'Linear',
+ icon: 'IconBrandLinear',
+ type: 'oauth',
+ oauth: {
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
+ scopes: ['read', 'write'],
+ // These must match keys in `defineApplication.serverVariables` below.
+ clientIdVariable: 'LINEAR_CLIENT_ID',
+ clientSecretVariable: 'LINEAR_CLIENT_SECRET',
+ // Optional: defaults to 'json'. Some providers (Linear, Slack) want
+ // 'form-urlencoded' for the token request.
+ tokenRequestContentType: 'form-urlencoded',
+ // Optional: defaults to true. Disable only if the provider rejects PKCE.
+ usePkce: false,
+ // Optional: extra query params on the authorize URL.
+ // authorizationParams: { prompt: 'consent' },
+ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
+ // revokeEndpoint: 'https://example.com/oauth/revoke',
+ },
+});
+```
+
+```ts src/application.config.ts
+import { defineApplication } from 'twenty-sdk/define';
+
+export default defineApplication({
+ universalIdentifier: '...',
+ displayName: 'Linear',
+ description: 'Connect Linear to Twenty.',
+ defaultRoleUniversalIdentifier: '...',
+ // OAuth client credentials live on the app registration (one OAuth app per
+ // Twenty server, configured by the admin) — not per-workspace. Declare them
+ // as serverVariables so the admin can fill them in once for all installs.
+ serverVariables: {
+ LINEAR_CLIENT_ID: {
+ description: 'OAuth client ID from your Linear OAuth application.',
+ isSecret: false,
+ isRequired: true,
+ },
+ LINEAR_CLIENT_SECRET: {
+ description: 'OAuth client secret from your Linear OAuth application.',
+ isSecret: true,
+ isRequired: true,
+ },
+ },
+});
+```
+
+Основные моменты:
+
+* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
+* `displayName` shows in the per-app settings tab and in the AI tool list.
+* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
+* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
+* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
+* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
+
+The OAuth callback URL your provider needs to whitelist is:
+
+```
+https:///apps/oauth/callback
+```
+
+
+
+
+
+Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
+
+```ts src/logic-functions/handlers/create-linear-issue-handler.ts
+import { listConnections } from 'twenty-sdk/logic-function';
+
+export const createLinearIssueHandler = async (input: {
+ teamId?: string;
+ title?: string;
+}) => {
+ if (!input.teamId || !input.title) {
+ return { success: false, error: 'teamId and title are required' };
+ }
+
+ const connections = await listConnections({ providerName: 'linear' });
+
+ // Workspace-shared credentials win when present; fall back to the first
+ // user-visibility one. For HTTP-route triggers you typically pick the
+ // request user's connection via event.userWorkspaceId instead.
+ const connection =
+ connections.find((c) => c.visibility === 'workspace') ?? connections[0];
+
+ if (!connection) {
+ return {
+ success: false,
+ error:
+ 'Linear is not connected. Open the app settings and click "Add connection".',
+ };
+ }
+
+ // Use connection.accessToken to call the third-party API.
+ const response = await fetch('https://api.linear.app/graphql', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
+ }),
+ });
+
+ return { success: response.ok };
+};
+```
+
+Each connection has:
+
+| Поле | Описание |
+| ----------------- | -------------------------------------------------------------------------------------------------------- |
+| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
+| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
+| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
+| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
+| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
+| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
+| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
+
+Основные моменты:
+
+* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
+* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
+* `getConnection(id)` is the single-row equivalent.
+
+
+
+
+
+When a user clicks "Add connection," they're prompted to pick a visibility:
+
+* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
+* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
+
+Use the right one for each handler:
+
+```ts
+// HTTP-route trigger — prefer the request user's own connection.
+const conn =
+ connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
+ connections.find((c) => c.visibility === 'workspace');
+
+// Cron trigger — no request user; only shared credentials are sensible.
+const conn = connections.find((c) => c.visibility === 'workspace');
+```
+
+Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
+
+
+
+
+
+For each connection provider, the server admin needs to register an OAuth app at the third party first.
+
+1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
+2. Set the **Redirect URI** to `\/apps/oauth/callback`.
+3. Copy the generated **Client ID** and **Client Secret**.
+4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
+5. Workspace members can then add connections from the per-app **Connections** section.
+
+
+
+
diff --git a/packages/twenty-docs/l/ru/developers/extend/apps/publishing.mdx b/packages/twenty-docs/l/ru/developers/extend/apps/publishing.mdx
index 1d67841564..d0a1d3604c 100644
--- a/packages/twenty-docs/l/ru/developers/extend/apps/publishing.mdx
+++ b/packages/twenty-docs/l/ru/developers/extend/apps/publishing.mdx
@@ -77,6 +77,39 @@ When updating an already deployed tarball app, the server requires the `version`
{/* TODO: add screenshot of the Upgrade button */}
+### Server version compatibility
+
+If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
+
+```json filename="package.json"
+{
+ "name": "twenty-my-app",
+ "version": "1.0.0",
+ "engines": {
+ "node": "^24.5.0",
+ "twenty": ">=2.3.0"
+ }
+}
+```
+
+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 |
+| `>=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` |
+
+**What happens at deploy and install time:**
+
+* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
+* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
+* If the server has no `APP_VERSION` configured, the check is skipped.
+
+
+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.
+
+
## Автоматизированный CI/CD (рабочие процессы, сгенерированные шаблоном)
Приложения, созданные с помощью `create-twenty-app`, «из коробки» включают два рабочих процесса GitHub Actions в каталоге `.github/workflows/`. Они готовы к запуску, как только вы запушите репозиторий на GitHub — для CI не требуется дополнительной настройки, а для CD нужен лишь один секрет.
diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/connections.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/connections.mdx
new file mode 100644
index 0000000000..d06594ae9c
--- /dev/null
+++ b/packages/twenty-docs/l/tr/developers/extend/apps/connections.mdx
@@ -0,0 +1,193 @@
+---
+title: Connections
+description: Let your app act on a user's behalf in third-party services via OAuth.
+icon: plug
+---
+
+Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
+
+Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
+
+
+
+
+
+A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
+
+A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
+
+```ts src/connection-providers/linear-connection.ts
+import { defineConnectionProvider } from 'twenty-sdk/define';
+
+export default defineConnectionProvider({
+ universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
+ name: 'linear',
+ displayName: 'Linear',
+ icon: 'IconBrandLinear',
+ type: 'oauth',
+ oauth: {
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
+ scopes: ['read', 'write'],
+ // These must match keys in `defineApplication.serverVariables` below.
+ clientIdVariable: 'LINEAR_CLIENT_ID',
+ clientSecretVariable: 'LINEAR_CLIENT_SECRET',
+ // Optional: defaults to 'json'. Some providers (Linear, Slack) want
+ // 'form-urlencoded' for the token request.
+ tokenRequestContentType: 'form-urlencoded',
+ // Optional: defaults to true. Disable only if the provider rejects PKCE.
+ usePkce: false,
+ // Optional: extra query params on the authorize URL.
+ // authorizationParams: { prompt: 'consent' },
+ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
+ // revokeEndpoint: 'https://example.com/oauth/revoke',
+ },
+});
+```
+
+```ts src/application.config.ts
+import { defineApplication } from 'twenty-sdk/define';
+
+export default defineApplication({
+ universalIdentifier: '...',
+ displayName: 'Linear',
+ description: 'Connect Linear to Twenty.',
+ defaultRoleUniversalIdentifier: '...',
+ // OAuth client credentials live on the app registration (one OAuth app per
+ // Twenty server, configured by the admin) — not per-workspace. Declare them
+ // as serverVariables so the admin can fill them in once for all installs.
+ serverVariables: {
+ LINEAR_CLIENT_ID: {
+ description: 'OAuth client ID from your Linear OAuth application.',
+ isSecret: false,
+ isRequired: true,
+ },
+ LINEAR_CLIENT_SECRET: {
+ description: 'OAuth client secret from your Linear OAuth application.',
+ isSecret: true,
+ isRequired: true,
+ },
+ },
+});
+```
+
+Önemli noktalar:
+
+* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
+* `displayName` shows in the per-app settings tab and in the AI tool list.
+* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
+* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
+* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
+* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
+
+The OAuth callback URL your provider needs to whitelist is:
+
+```
+https:///apps/oauth/callback
+```
+
+
+
+
+
+Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
+
+```ts src/logic-functions/handlers/create-linear-issue-handler.ts
+import { listConnections } from 'twenty-sdk/logic-function';
+
+export const createLinearIssueHandler = async (input: {
+ teamId?: string;
+ title?: string;
+}) => {
+ if (!input.teamId || !input.title) {
+ return { success: false, error: 'teamId and title are required' };
+ }
+
+ const connections = await listConnections({ providerName: 'linear' });
+
+ // Workspace-shared credentials win when present; fall back to the first
+ // user-visibility one. For HTTP-route triggers you typically pick the
+ // request user's connection via event.userWorkspaceId instead.
+ const connection =
+ connections.find((c) => c.visibility === 'workspace') ?? connections[0];
+
+ if (!connection) {
+ return {
+ success: false,
+ error:
+ 'Linear is not connected. Open the app settings and click "Add connection".',
+ };
+ }
+
+ // Use connection.accessToken to call the third-party API.
+ const response = await fetch('https://api.linear.app/graphql', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
+ }),
+ });
+
+ return { success: response.ok };
+};
+```
+
+Each connection has:
+
+| Alan | Açıklama |
+| ----------------- | -------------------------------------------------------------------------------------------------------- |
+| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
+| `visibility` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
+| `scopes` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
+| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
+| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
+| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
+| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
+
+Önemli noktalar:
+
+* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
+* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
+* `getConnection(id)` is the single-row equivalent.
+
+
+
+
+
+When a user clicks "Add connection," they're prompted to pick a visibility:
+
+* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
+* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
+
+Use the right one for each handler:
+
+```ts
+// HTTP-route trigger — prefer the request user's own connection.
+const conn =
+ connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
+ connections.find((c) => c.visibility === 'workspace');
+
+// Cron trigger — no request user; only shared credentials are sensible.
+const conn = connections.find((c) => c.visibility === 'workspace');
+```
+
+Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
+
+
+
+
+
+For each connection provider, the server admin needs to register an OAuth app at the third party first.
+
+1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
+2. Set the **Redirect URI** to `\/apps/oauth/callback`.
+3. Copy the generated **Client ID** and **Client Secret**.
+4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
+5. Workspace members can then add connections from the per-app **Connections** section.
+
+
+
+
diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/publishing.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/publishing.mdx
index 0f82662a23..a78af24fee 100644
--- a/packages/twenty-docs/l/tr/developers/extend/apps/publishing.mdx
+++ b/packages/twenty-docs/l/tr/developers/extend/apps/publishing.mdx
@@ -77,6 +77,39 @@ Bir güncelleme yayımlamak için:
{/* TODO: add screenshot of the Upgrade button */}
+### Server version compatibility
+
+If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
+
+```json filename="package.json"
+{
+ "name": "twenty-my-app",
+ "version": "1.0.0",
+ "engines": {
+ "node": "^24.5.0",
+ "twenty": ">=2.3.0"
+ }
+}
+```
+
+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 |
+| `>=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` |
+
+**What happens at deploy and install time:**
+
+* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
+* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
+* If the server has no `APP_VERSION` configured, the check is skipped.
+
+
+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.
+
+
## Otomatik CI/CD (hazır şablonlu iş akışları)
`create-twenty-app` ile oluşturulan uygulamalar, kutudan çıktığı gibi `.github/workflows/` altında iki GitHub Actions iş akışıyla gelir. Depoyu GitHub’a iter itmez çalışmaya hazırdır — CI için ek bir kurulum gerekmez ve CD yalnızca tek bir gizli anahtar gerektirir.
diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/connections.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/connections.mdx
new file mode 100644
index 0000000000..2a44bbabfd
--- /dev/null
+++ b/packages/twenty-docs/l/zh/developers/extend/apps/connections.mdx
@@ -0,0 +1,193 @@
+---
+title: Connections
+description: Let your app act on a user's behalf in third-party services via OAuth.
+icon: plug
+---
+
+Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.
+
+Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.
+
+
+
+
+
+A connection provider describes the OAuth handshake your app needs. The user clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace.
+
+A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.
+
+```ts src/connection-providers/linear-connection.ts
+import { defineConnectionProvider } from 'twenty-sdk/define';
+
+export default defineConnectionProvider({
+ universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
+ name: 'linear',
+ displayName: 'Linear',
+ icon: 'IconBrandLinear',
+ type: 'oauth',
+ oauth: {
+ authorizationEndpoint: 'https://linear.app/oauth/authorize',
+ tokenEndpoint: 'https://api.linear.app/oauth/token',
+ scopes: ['read', 'write'],
+ // These must match keys in `defineApplication.serverVariables` below.
+ clientIdVariable: 'LINEAR_CLIENT_ID',
+ clientSecretVariable: 'LINEAR_CLIENT_SECRET',
+ // Optional: defaults to 'json'. Some providers (Linear, Slack) want
+ // 'form-urlencoded' for the token request.
+ tokenRequestContentType: 'form-urlencoded',
+ // Optional: defaults to true. Disable only if the provider rejects PKCE.
+ usePkce: false,
+ // Optional: extra query params on the authorize URL.
+ // authorizationParams: { prompt: 'consent' },
+ // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
+ // revokeEndpoint: 'https://example.com/oauth/revoke',
+ },
+});
+```
+
+```ts src/application.config.ts
+import { defineApplication } from 'twenty-sdk/define';
+
+export default defineApplication({
+ universalIdentifier: '...',
+ displayName: 'Linear',
+ description: 'Connect Linear to Twenty.',
+ defaultRoleUniversalIdentifier: '...',
+ // OAuth client credentials live on the app registration (one OAuth app per
+ // Twenty server, configured by the admin) — not per-workspace. Declare them
+ // as serverVariables so the admin can fill them in once for all installs.
+ serverVariables: {
+ LINEAR_CLIENT_ID: {
+ description: 'OAuth client ID from your Linear OAuth application.',
+ isSecret: false,
+ isRequired: true,
+ },
+ LINEAR_CLIENT_SECRET: {
+ description: 'OAuth client secret from your Linear OAuth application.',
+ isSecret: true,
+ isRequired: true,
+ },
+ },
+});
+```
+
+关键点:
+
+* `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
+* `displayName` shows in the per-app settings tab and in the AI tool list.
+* `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
+* Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
+* Until both `serverVariables` are filled in, the per-app settings tab shows a "needs server admin" hint and the "Add connection" button is disabled.
+* `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.
+
+The OAuth callback URL your provider needs to whitelist is:
+
+```
+https:///apps/oauth/callback
+```
+
+
+
+
+
+Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.
+
+```ts src/logic-functions/handlers/create-linear-issue-handler.ts
+import { listConnections } from 'twenty-sdk/logic-function';
+
+export const createLinearIssueHandler = async (input: {
+ teamId?: string;
+ title?: string;
+}) => {
+ if (!input.teamId || !input.title) {
+ return { success: false, error: 'teamId and title are required' };
+ }
+
+ const connections = await listConnections({ providerName: 'linear' });
+
+ // Workspace-shared credentials win when present; fall back to the first
+ // user-visibility one. For HTTP-route triggers you typically pick the
+ // request user's connection via event.userWorkspaceId instead.
+ const connection =
+ connections.find((c) => c.visibility === 'workspace') ?? connections[0];
+
+ if (!connection) {
+ return {
+ success: false,
+ error:
+ 'Linear is not connected. Open the app settings and click "Add connection".',
+ };
+ }
+
+ // Use connection.accessToken to call the third-party API.
+ const response = await fetch('https://api.linear.app/graphql', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${connection.accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
+ }),
+ });
+
+ return { success: response.ok };
+};
+```
+
+Each connection has:
+
+| 字段 | 描述 |
+| ----------------- | -------------------------------------------------------------------------------------------------------- |
+| `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
+| `可见性` | `'user'` (private to one workspace member) or `'workspace'` (shared with all members) |
+| `范围` | OAuth permissions granted by the upstream provider (distinct from `visibility` — those are unrelated) |
+| `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
+| `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
+| `name` / `handle` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
+| `authFailedAt` | Set when the most recent refresh failed; the user must reconnect |
+
+关键点:
+
+* Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
+* The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
+* `getConnection(id)` is the single-row equivalent.
+
+
+
+
+
+When a user clicks "Add connection," they're prompted to pick a visibility:
+
+* **Just for me** — the credential is private to the connecting user. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.
+* **Workspace shared** — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
+
+Use the right one for each handler:
+
+```ts
+// HTTP-route trigger — prefer the request user's own connection.
+const conn =
+ connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
+ connections.find((c) => c.visibility === 'workspace');
+
+// Cron trigger — no request user; only shared credentials are sensible.
+const conn = connections.find((c) => c.visibility === 'workspace');
+```
+
+Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.
+
+
+
+
+
+For each connection provider, the server admin needs to register an OAuth app at the third party first.
+
+1. Go to the provider's developer settings (e.g. https://linear.app/settings/api/applications/new).
+2. Set the **Redirect URI** to `\/apps/oauth/callback`.
+3. Copy the generated **Client ID** and **Client Secret**.
+4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
+5. Workspace members can then add connections from the per-app **Connections** section.
+
+
+
+
diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/publishing.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/publishing.mdx
index 2854f77a3d..317042578d 100644
--- a/packages/twenty-docs/l/zh/developers/extend/apps/publishing.mdx
+++ b/packages/twenty-docs/l/zh/developers/extend/apps/publishing.mdx
@@ -77,6 +77,39 @@ yarn twenty deploy
{/* TODO: add screenshot of the Upgrade button */}
+### Server version compatibility
+
+If your app uses a feature introduced in a specific Twenty server version (for example, OAuth providers added in v2.3.0), you should declare the minimum server version your app requires using the `engines.twenty` field in `package.json`:
+
+```json filename="package.json"
+{
+ "name": "twenty-my-app",
+ "version": "1.0.0",
+ "engines": {
+ "node": "^24.5.0",
+ "twenty": ">=2.3.0"
+ }
+}
+```
+
+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 |
+| `>=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` |
+
+**What happens at deploy and install time:**
+
+* If `engines.twenty` is set and the target server's version does not satisfy the range, the deploy (tarball upload) or install is rejected with a `SERVER_VERSION_INCOMPATIBLE` error and a message indicating both the required range and the actual server version.
+* If `engines.twenty` is **not set**, the app is accepted on any server version (backward-compatible with existing apps).
+* If the server has no `APP_VERSION` configured, the check is skipped.
+
+
+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.
+
+
## 自动化 CI/CD(脚手架生成的工作流)
使用 `create-twenty-app` 生成的应用开箱即带有两个 GitHub Actions 工作流,位于 `.github/workflows/`。 当你将仓库推送到 GitHub 后即可运行——CI 无需额外设置,CD 只需要一个机密。