diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 63cd099d1e..d465a1c03c 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -592,7 +592,7 @@ type Application {
canBeUninstalled: Boolean!
autoUpgrade: Boolean!
defaultRoleId: String
- settingsCustomTabFrontComponentId: UUID @deprecated(reason: "Custom settings tabs are no longer supported. This field is ignored.")
+ settingsCustomTabFrontComponentId: UUID
defaultLogicFunctionRole: Role
agents: [Agent!]!
frontComponents: [FrontComponent!]!
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index 11f21c72fc..8b68d86075 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -405,7 +405,6 @@ export interface Application {
canBeUninstalled: Scalars['Boolean']
autoUpgrade: Scalars['Boolean']
defaultRoleId?: Scalars['String']
- /** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
settingsCustomTabFrontComponentId?: Scalars['UUID']
defaultLogicFunctionRole?: Role
agents: Agent[]
@@ -3531,7 +3530,6 @@ export interface ApplicationGenqlSelection{
canBeUninstalled?: boolean | number
autoUpgrade?: boolean | number
defaultRoleId?: boolean | number
- /** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
settingsCustomTabFrontComponentId?: boolean | number
defaultLogicFunctionRole?: RoleGenqlSelection
agents?: AgentGenqlSelection
diff --git a/packages/twenty-docs/developers/extend/apps/config/application.mdx b/packages/twenty-docs/developers/extend/apps/config/application.mdx
index 6c5e0b46b1..a12261f868 100644
--- a/packages/twenty-docs/developers/extend/apps/config/application.mdx
+++ b/packages/twenty-docs/developers/extend/apps/config/application.mdx
@@ -36,6 +36,7 @@ Notes:
- Pre-install, post-install, and uninstall functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
- `serverVariables` are instance-scoped configuration and secrets (e.g. API keys). Unlike `applicationVariables`, they declare no value in the manifest — the workspace operator fills them in from the app's settings, and they are injected into logic functions only once set.
+- To render a custom configuration UI inside the app's **Settings** tab (in place of the default variable configuration section), declare a front component with [`defineSettingsFrontComponent()`](/developers/extend/apps/layout/front-components#custom-settings-component) in its own file. Only one is allowed per app. System-managed sections (auto-upgrade, App URL, connections) always remain visible.
## Variable types
diff --git a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
index 7cd31a71ff..b6244699f0 100644
--- a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
+++ b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
@@ -8,15 +8,17 @@ Front components are React components that render directly inside Twenty's UI. T
## Where front components can be used
-Front components can render in two locations within Twenty:
+Front components can render in three 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.
+- **App settings** — Defined with [`defineSettingsFrontComponent()`](#custom-settings-component), the front component renders as a section inside the app's **Settings** tab, in place of the default variable configuration UI.
-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 three 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.
+- **Define it with [`defineSettingsFrontComponent()`](#custom-settings-component)** — renders it as a section inside the app's **Settings** tab, in place of the default variable configuration UI.
## Basic example
@@ -77,6 +79,34 @@ Click it to render the component inline.
Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/developers/extend/apps/layout/page-layouts) for details.
+## Custom settings component
+
+To replace the auto-generated variable configuration UI in your app's **Settings** tab with your own component, define it with `defineSettingsFrontComponent` instead of `defineFrontComponent`. It takes the same [configuration fields](#configuration-fields) (except `isHeadless`, which is not accepted since a settings component always renders visible UI) and additionally marks the component as the app's settings UI.
+
+The component renders as a section **inside** the Settings tab, not as a replacement for the whole tab. Twenty's system-managed sections — auto-upgrade, App URL, and connections — always render above it and cannot be overridden by the app.
+
+```tsx src/front-components/app-settings.tsx
+import { defineSettingsFrontComponent } from 'twenty-sdk/define';
+
+const AppSettings = () => {
+ return (
+
+
My app settings
+ {/* render your own configuration UI here */}
+
+ );
+};
+
+export default defineSettingsFrontComponent({
+ universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
+ name: 'app-settings',
+ description: "Custom UI for the app's Settings tab",
+ component: AppSettings,
+});
+```
+
+Only one settings front component is allowed per app; declaring more than one fails the build. When present, the app's **Settings** tab renders this component in place of the default variable configuration UI.
+
## Headless vs non-headless
Front components come in two rendering modes controlled by the `isHeadless` option:
diff --git a/packages/twenty-docs/developers/extend/apps/layout/overview.mdx b/packages/twenty-docs/developers/extend/apps/layout/overview.mdx
index bb92c9c390..4ff017e4ae 100644
--- a/packages/twenty-docs/developers/extend/apps/layout/overview.mdx
+++ b/packages/twenty-docs/developers/extend/apps/layout/overview.mdx
@@ -51,6 +51,7 @@ A Twenty app's **layout layer** is everything the user sees: where the app surfa
| **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` |
+| **App settings** | A custom configuration section inside the app's Settings tab, in place of the default variables UI | `defineSettingsFrontComponent` |
| **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.
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 57a53965e3..dddc94b764 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -315,7 +315,6 @@ export type Application = {
objects: Array;
packageJsonChecksum?: Maybe;
packageJsonFileId?: Maybe;
- /** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
settingsCustomTabFrontComponentId?: Maybe;
universalIdentifier: Scalars['String']['output'];
version?: Maybe;
diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx
index e66523a7de..8f8c81c48d 100644
--- a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationDetails.tsx
@@ -252,10 +252,14 @@ export const SettingsApplicationDetails = () => {
const hasHttpTriggeredFunctions =
applicationHasHttpTriggeredFunctions(application);
const canShowFunctionDomain = hasHttpTriggeredFunctions;
+ const hasSettingsFrontComponent = isDefined(
+ application?.settingsCustomTabFrontComponentId,
+ );
const hasNothingToConfigure =
!hasVariables &&
!hasConnectionProviders &&
!canShowFunctionDomain &&
+ !hasSettingsFrontComponent &&
!isUpgradableApplicationSourceType(sourceType);
return {
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationCustomSettingsSection.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationCustomSettingsSection.tsx
new file mode 100644
index 0000000000..f8c76fa309
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationCustomSettingsSection.tsx
@@ -0,0 +1,38 @@
+import { styled } from '@linaria/react';
+import { Suspense, lazy } from 'react';
+import { Section } from 'twenty-ui/layout';
+
+import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader';
+
+const FrontComponentRenderer = lazy(() =>
+ import('@/front-components/components/FrontComponentRenderer').then(
+ (module) => ({ default: module.FrontComponentRenderer }),
+ ),
+);
+
+const StyledRendererContainer = styled.div`
+ display: flex;
+ min-height: 400px;
+ width: 100%;
+`;
+
+type SettingsApplicationCustomSettingsSectionProps = {
+ frontComponentId: string;
+};
+
+export const SettingsApplicationCustomSettingsSection = ({
+ frontComponentId,
+}: SettingsApplicationCustomSettingsSectionProps) => {
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab.tsx
index 539de69a98..8b7f3a8de4 100644
--- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab.tsx
@@ -1,6 +1,8 @@
+import { isDefined } from 'twenty-shared/utils';
import { type Application } from '~/generated-metadata/graphql';
import { useUpdateOneApplicationVariable } from '~/pages/settings/applications/hooks/useUpdateOneApplicationVariable';
import { SettingsApplicationConnectionsSection } from '~/pages/settings/applications/tabs/SettingsApplicationConnectionsSection';
+import { SettingsApplicationCustomSettingsSection } from '~/pages/settings/applications/tabs/SettingsApplicationCustomSettingsSection';
import { SettingsApplicationDetailEnvironmentVariablesTable } from '~/pages/settings/applications/tabs/SettingsApplicationDetailEnvironmentVariablesTable';
import { SettingsApplicationFunctionDomainSection } from '~/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection';
import { SettingsApplicationGeneralSection } from '~/pages/settings/applications/tabs/SettingsApplicationGeneralSection';
@@ -19,6 +21,7 @@ export const SettingsApplicationDetailSettingsTab = ({
| 'autoUpgrade'
| 'applicationRegistration'
| 'logicFunctions'
+ | 'settingsCustomTabFrontComponentId'
>;
}) => {
const { updateOneApplicationVariable } = useUpdateOneApplicationVariable();
@@ -34,6 +37,9 @@ export const SettingsApplicationDetailSettingsTab = ({
application?.applicationRegistration?.sourceType,
);
+ const settingsFrontComponentId =
+ application?.settingsCustomTabFrontComponentId;
+
return (
<>
{isUpgradable && application?.id && (
@@ -50,18 +56,24 @@ export const SettingsApplicationDetailSettingsTab = ({
{application?.id && (
)}
-
- application?.id
- ? updateOneApplicationVariable({
- key,
- value,
- applicationId: application.id,
- })
- : null
- }
- />
+ {isDefined(settingsFrontComponentId) ? (
+
+ ) : (
+
+ application?.id
+ ? updateOneApplicationVariable({
+ key,
+ value,
+ applicationId: application.id,
+ })
+ : null
+ }
+ />
+ )}
>
);
};
diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap
index 27fc529f27..0c7f2dd594 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap
+++ b/packages/twenty-sdk/src/cli/utilities/build/common/plugins/__tests__/__snapshots__/stub-twenty-sdk-define.plugin.spec.ts.snap
@@ -84,6 +84,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
"definePostInstallLogicFunction",
"definePreInstallLogicFunction",
"defineRole",
+ "defineSettingsFrontComponent",
"defineSkill",
"defineUninstallLogicFunction",
"defineView",
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts
index 0252f51d47..83b913a80d 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts
@@ -227,6 +227,32 @@ describe('manifestValidate', () => {
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
+
+ it('should not flag a front component referenced via settingsFrontComponent as a duplicate', () => {
+ const frontComponentId = '550e8400-e29b-41d4-a716-446655440050';
+
+ const frontComponent = {
+ universalIdentifier: frontComponentId,
+ name: 'app-settings',
+ componentName: 'AppSettings',
+ sourceComponentPath: 'src/front-components/app-settings.tsx',
+ builtComponentPath: 'dist/app-settings.mjs',
+ builtComponentChecksum: '00000000-0000-4000-8000-000000000000',
+ isHeadless: false,
+ } as unknown as Manifest['frontComponents'][number];
+
+ const result = manifestValidate({
+ ...validManifest,
+ application: {
+ ...validApplication,
+ settingsFrontComponent: { universalIdentifier: frontComponentId },
+ },
+ frontComponents: [frontComponent],
+ });
+
+ expect(result.isValid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ });
});
describe('relation field validation', () => {
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts
index fabfa7b04d..ab3cef101d 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-build.ts
@@ -122,6 +122,7 @@ export const buildManifest = async (
[];
const uninstallLogicFunctions: UninstallLogicFunctionApplicationManifest[] =
[];
+ const settingsFrontComponentUniversalIdentifiers: string[] = [];
const applicationRoleUniversalIdentifiers: string[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -377,6 +378,14 @@ export const buildManifest = async (
frontComponents.push(config);
frontComponentsFilePaths.push(relativePath);
+ if (
+ targetFunctionName === TargetFunction.DefineSettingsFrontComponent
+ ) {
+ settingsFrontComponentUniversalIdentifiers.push(
+ extract.config.universalIdentifier,
+ );
+ }
+
break;
}
case ManifestEntityKey.Views: {
@@ -559,6 +568,12 @@ export const buildManifest = async (
);
}
+ if (settingsFrontComponentUniversalIdentifiers.length > 1) {
+ errors.push(
+ 'Only one settings front component is allowed per application',
+ );
+ }
+
if (applicationRoleUniversalIdentifiers.length > 1) {
errors.push('Only one defineApplicationRole is allowed per application');
}
@@ -611,6 +626,14 @@ export const buildManifest = async (
...(uninstallLogicFunctions.length >= 1
? { uninstallLogicFunction: uninstallLogicFunctions[0] }
: {}),
+ ...(settingsFrontComponentUniversalIdentifiers.length >= 1
+ ? {
+ settingsFrontComponent: {
+ universalIdentifier:
+ settingsFrontComponentUniversalIdentifiers[0],
+ },
+ }
+ : {}),
};
})()
: undefined;
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts
index 3e4cf8496a..6f7135b4c6 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config.ts
@@ -16,6 +16,7 @@ export enum TargetFunction {
DefineAgent = 'defineAgent',
DefineConnectionProvider = 'defineConnectionProvider',
DefineFrontComponent = 'defineFrontComponent',
+ DefineSettingsFrontComponent = 'defineSettingsFrontComponent',
DefineView = 'defineView',
DefineViewField = 'defineViewField',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
@@ -70,6 +71,8 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineConnectionProvider]:
ManifestEntityKey.ConnectionProviders,
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
+ [TargetFunction.DefineSettingsFrontComponent]:
+ ManifestEntityKey.FrontComponents,
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineViewField]: ManifestEntityKey.ViewFields,
[TargetFunction.DefineNavigationMenuItem]:
diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts
index 3eccadc26f..498e45d837 100644
--- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts
+++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts
@@ -74,7 +74,8 @@ const findUniversalIdentifiers = (obj: object): string[] => {
key === 'postInstallLogicFunction' ||
key === 'preInstallLogicFunction' ||
key === 'uninstallLogicFunction' ||
- key === 'onConnectLogicFunction'
+ key === 'onConnectLogicFunction' ||
+ key === 'settingsFrontComponent'
) {
continue;
}
diff --git a/packages/twenty-sdk/src/sdk/define/application/application-config.ts b/packages/twenty-sdk/src/sdk/define/application/application-config.ts
index 1ea4747979..7124afedc6 100644
--- a/packages/twenty-sdk/src/sdk/define/application/application-config.ts
+++ b/packages/twenty-sdk/src/sdk/define/application/application-config.ts
@@ -7,6 +7,7 @@ export type ApplicationConfig = Omit<
| 'requiredServerVersionRange'
| 'postInstallLogicFunction'
| 'preInstallLogicFunction'
+ | 'settingsFrontComponent'
| 'defaultRoleUniversalIdentifier'
| 'aboutDescription'
> & {
diff --git a/packages/twenty-sdk/src/sdk/define/front-component/__tests__/define-settings-front-component.spec.ts b/packages/twenty-sdk/src/sdk/define/front-component/__tests__/define-settings-front-component.spec.ts
new file mode 100644
index 0000000000..d78a614dee
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/define/front-component/__tests__/define-settings-front-component.spec.ts
@@ -0,0 +1,62 @@
+import { defineSettingsFrontComponent } from '@/sdk/define';
+
+const MockComponent = () => null;
+
+describe('defineSettingsFrontComponent', () => {
+ const validConfig = {
+ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
+ name: 'App Settings',
+ component: MockComponent,
+ };
+
+ it('should return successful validation result when valid', () => {
+ const result = defineSettingsFrontComponent(validConfig);
+
+ expect(result.success).toBe(true);
+ expect(result.config).toEqual(validConfig);
+ expect(result.errors).toEqual([]);
+ });
+
+ it('should return error when universalIdentifier is missing', () => {
+ const config = {
+ name: 'App Settings',
+ component: MockComponent,
+ };
+
+ const result = defineSettingsFrontComponent(config as any);
+
+ expect(result.success).toBe(false);
+ expect(result.errors).toContain(
+ 'Settings front component must have a universalIdentifier',
+ );
+ });
+
+ it('should return error when component is missing', () => {
+ const config = {
+ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
+ name: 'App Settings',
+ };
+
+ const result = defineSettingsFrontComponent(config as any);
+
+ expect(result.success).toBe(false);
+ expect(result.errors).toEqual([
+ 'Settings front component must have a component',
+ ]);
+ });
+
+ it('should return error when component is not a function', () => {
+ const config = {
+ universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
+ name: 'App Settings',
+ component: 'not-a-function',
+ };
+
+ const result = defineSettingsFrontComponent(config as any);
+
+ expect(result.success).toBe(false);
+ expect(result.errors).toContain(
+ 'Settings front component component must be a React component',
+ );
+ });
+});
diff --git a/packages/twenty-sdk/src/sdk/define/front-component/define-settings-front-component.ts b/packages/twenty-sdk/src/sdk/define/front-component/define-settings-front-component.ts
new file mode 100644
index 0000000000..e5b16df89e
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/define/front-component/define-settings-front-component.ts
@@ -0,0 +1,24 @@
+import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result';
+import type { DefineEntity } from '@/sdk/define/common/types/define-entity.type';
+import { type SettingsFrontComponentConfig } from '@/sdk/define/front-component/settings-front-component-config';
+
+export const defineSettingsFrontComponent: DefineEntity<
+ SettingsFrontComponentConfig
+> = (config) => {
+ const errors = [];
+
+ if (!config.universalIdentifier) {
+ errors.push('Settings front component must have a universalIdentifier');
+ }
+
+ if (!config.component) {
+ errors.push('Settings front component must have a component');
+ } else if (typeof config.component !== 'function') {
+ errors.push('Settings front component component must be a React component');
+ }
+
+ return createValidationResult({
+ config,
+ errors,
+ });
+};
diff --git a/packages/twenty-sdk/src/sdk/define/front-component/settings-front-component-config.ts b/packages/twenty-sdk/src/sdk/define/front-component/settings-front-component-config.ts
new file mode 100644
index 0000000000..85efc7a272
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/define/front-component/settings-front-component-config.ts
@@ -0,0 +1,8 @@
+import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
+
+// A settings front component always renders visible UI, so `isHeadless`
+// is not configurable.
+export type SettingsFrontComponentConfig = Omit<
+ FrontComponentConfig,
+ 'isHeadless'
+>;
diff --git a/packages/twenty-sdk/src/sdk/define/index.ts b/packages/twenty-sdk/src/sdk/define/index.ts
index 82bd10881e..88be5b6152 100644
--- a/packages/twenty-sdk/src/sdk/define/index.ts
+++ b/packages/twenty-sdk/src/sdk/define/index.ts
@@ -70,10 +70,12 @@ export {
} from '@/sdk/define/conditional-availability/conditional-availability-variables';
export { defineFrontComponent } from '@/sdk/define/front-component/define-front-component';
+export { defineSettingsFrontComponent } from '@/sdk/define/front-component/define-settings-front-component';
export type {
FrontComponentConfig,
FrontComponentType,
} from '@/sdk/define/front-component/front-component-config';
+export type { SettingsFrontComponentConfig } from '@/sdk/define/front-component/settings-front-component-config';
export { defineIndex } from '@/sdk/define/indexes/define-index';
export type { IndexConfig } from '@/sdk/define/indexes/index-config';
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts
index 23ec8afd45..d9df8780bc 100644
--- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/application-manifest-migration.service.ts
@@ -249,7 +249,7 @@ export class ApplicationManifestMigrationService {
);
if (!dryRun) {
- await this.syncDefaultRole({
+ await this.syncDefaultRoleAndSettingsFrontComponent({
manifest,
workspaceId,
ownerFlatApplication,
@@ -262,7 +262,7 @@ export class ApplicationManifestMigrationService {
};
}
- private async syncDefaultRole({
+ private async syncDefaultRoleAndSettingsFrontComponent({
manifest,
workspaceId,
ownerFlatApplication,
@@ -271,10 +271,13 @@ export class ApplicationManifestMigrationService {
workspaceId: string;
ownerFlatApplication: FlatApplication;
}) {
- const { flatRoleMaps: refreshedFlatRoleMaps } =
- await this.workspaceCacheService.getOrRecompute(workspaceId, [
- 'flatRoleMaps',
- ]);
+ const {
+ flatRoleMaps: refreshedFlatRoleMaps,
+ flatFrontComponentMaps: refreshedFlatFrontComponentMaps,
+ } = await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatRoleMaps',
+ 'flatFrontComponentMaps',
+ ]);
let defaultRoleId: string | null = null;
@@ -299,11 +302,31 @@ export class ApplicationManifestMigrationService {
}
}
- if (isDefined(defaultRoleId)) {
- await this.applicationService.update(ownerFlatApplication.id, {
- workspaceId,
- defaultRoleId,
+ let settingsCustomTabFrontComponentId: string | null = null;
+
+ const settingsFrontComponentUniversalIdentifier =
+ manifest.application.settingsFrontComponent?.universalIdentifier;
+
+ if (isDefined(settingsFrontComponentUniversalIdentifier)) {
+ const flatFrontComponent = findFlatEntityByUniversalIdentifier({
+ flatEntityMaps: refreshedFlatFrontComponentMaps,
+ universalIdentifier: settingsFrontComponentUniversalIdentifier,
});
+
+ if (!isDefined(flatFrontComponent)) {
+ throw new ApplicationException(
+ `Failed to resolve front component for settings front component universalIdentifier ${settingsFrontComponentUniversalIdentifier}`,
+ ApplicationExceptionCode.ENTITY_NOT_FOUND,
+ );
+ }
+
+ settingsCustomTabFrontComponentId = flatFrontComponent.id;
}
+
+ await this.applicationService.update(ownerFlatApplication.id, {
+ workspaceId,
+ settingsCustomTabFrontComponentId,
+ ...(isDefined(defaultRoleId) ? { defaultRoleId } : {}),
+ });
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util.ts
index be476be591..c42d7f535b 100644
--- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/converters/from-front-component-manifest-to-universal-flat-front-component.util.ts
@@ -5,10 +5,12 @@ import { type UniversalFlatFrontComponent } from 'src/engine/workspace-manager/w
export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({
frontComponentManifest,
applicationUniversalIdentifier,
+ isSettingsFrontComponent,
now,
}: {
frontComponentManifest: FrontComponentManifest;
applicationUniversalIdentifier: string;
+ isSettingsFrontComponent: boolean;
now: string;
}): UniversalFlatFrontComponent => {
return {
@@ -20,7 +22,10 @@ export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({
builtComponentPath: frontComponentManifest.builtComponentPath,
componentName: frontComponentManifest.componentName,
builtComponentChecksum: frontComponentManifest.builtComponentChecksum,
- isHeadless: frontComponentManifest.isHeadless ?? false,
+ // A settings front component always renders visible UI.
+ isHeadless: isSettingsFrontComponent
+ ? false
+ : (frontComponentManifest.isHeadless ?? false),
usesSdkClient: frontComponentManifest.usesSdkClient ?? false,
createdAt: now,
updatedAt: now,
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service.ts
index 7fb43b9476..cdf2fb5e18 100644
--- a/packages/twenty-server/src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service.ts
@@ -215,12 +215,18 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
});
}
+ const settingsFrontComponentUniversalIdentifier =
+ manifest.application.settingsFrontComponent?.universalIdentifier;
+
for (const frontComponentManifest of manifest.frontComponents) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFrontComponentManifestToUniversalFlatFrontComponent({
frontComponentManifest,
applicationUniversalIdentifier,
+ isSettingsFrontComponent:
+ frontComponentManifest.universalIdentifier ===
+ settingsFrontComponentUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
diff --git a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts
index 9975c20b33..6e648449c8 100644
--- a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts
@@ -113,11 +113,6 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
@Field(() => RoleDTO, { nullable: true })
defaultRole: RoleDTO | null;
- /**
- * @deprecated Custom settings tabs are no longer supported. The column is
- * kept (not dropped) so existing installations upgrade cleanly, but the
- * value is no longer read or synced from manifests.
- */
@Column({ nullable: true, type: 'uuid' })
settingsCustomTabFrontComponentId: string | null;
diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts
index 6067439286..719c2d9ca7 100644
--- a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts
+++ b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts
@@ -95,17 +95,9 @@ export class ApplicationDTO {
@Field({ nullable: true })
defaultRoleId?: string;
- /**
- * @deprecated Custom settings tabs are no longer supported. Kept for
- * backward compatibility with existing installations; the value is ignored.
- */
@IsOptional()
@IsUUID()
- @Field(() => UUIDScalarType, {
- nullable: true,
- deprecationReason:
- 'Custom settings tabs are no longer supported. This field is ignored.',
- })
+ @Field(() => UUIDScalarType, { nullable: true })
settingsCustomTabFrontComponentId?: string;
@IsOptional()
diff --git a/packages/twenty-shared/src/application/applicationType.ts b/packages/twenty-shared/src/application/applicationType.ts
index b28e2de847..9ee1e10820 100644
--- a/packages/twenty-shared/src/application/applicationType.ts
+++ b/packages/twenty-shared/src/application/applicationType.ts
@@ -1,5 +1,6 @@
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
+import { type SettingsFrontComponentApplicationManifest } from '@/application/settingsFrontComponentApplicationType';
import { type UninstallLogicFunctionApplicationManifest } from '@/application/uninstallLogicFunctionApplicationType';
import { type ApplicationCategory } from './applicationCategoryType';
import { type ApplicationVariables } from './applicationVariablesType';
@@ -32,10 +33,10 @@ export type ApplicationManifest = SyncableEntityOptions & {
postInstallLogicFunction?: PostInstallLogicFunctionApplicationManifest;
preInstallLogicFunction?: PreInstallLogicFunctionApplicationManifest;
uninstallLogicFunction?: UninstallLogicFunctionApplicationManifest;
+ settingsFrontComponent?: SettingsFrontComponentApplicationManifest;
/**
- * @deprecated Custom settings tabs are no longer supported. This property is
- * kept for backward compatibility with older manifests but is now ignored.
- * Use typed `applicationVariables` / `serverVariables` instead.
+ * @deprecated Use `defineSettingsFrontComponent()` (exposed on the manifest
+ * as `settingsFrontComponent`) instead. This property is ignored.
*/
settingsCustomTabFrontComponentUniversalIdentifier?: string;
packageJsonChecksum: string | null;
diff --git a/packages/twenty-shared/src/application/index.ts b/packages/twenty-shared/src/application/index.ts
index c1d4437cc2..e637ec716e 100644
--- a/packages/twenty-shared/src/application/index.ts
+++ b/packages/twenty-shared/src/application/index.ts
@@ -135,6 +135,7 @@ export type { RunAgentInput, RunAgentResult } from './runAgentType';
export type { ServerVariables } from './server-variables.type';
export type { ServerRouteDispatchResult } from './serverRouteDispatchResultType';
export type { ServerRouteTriggerSettings } from './serverRouteTriggerSettingsType';
+export type { SettingsFrontComponentApplicationManifest } from './settingsFrontComponentApplicationType';
export type { SkillManifest } from './skillManifestType';
export type { StoredOAuthConnectionProviderConfig } from './storedOAuthConnectionProviderConfigType';
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
diff --git a/packages/twenty-shared/src/application/settingsFrontComponentApplicationType.ts b/packages/twenty-shared/src/application/settingsFrontComponentApplicationType.ts
new file mode 100644
index 0000000000..1c9721d5bb
--- /dev/null
+++ b/packages/twenty-shared/src/application/settingsFrontComponentApplicationType.ts
@@ -0,0 +1,3 @@
+export type SettingsFrontComponentApplicationManifest = {
+ universalIdentifier: string;
+};