Add Twenty app settings custom app (#18273)

## Context
This PR adds the ability to define a front-component as a custom tab for
the application settings, allowing app creators to inject some
logic/rendering the their app settings.


## Example
```typescript
// packages/twenty-apps/my-test-app/src/front-components/settings-custom-tab.tsx
import { defineFrontComponent } from 'twenty-sdk';

export const SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER =
  'a42a88a8-21ce-4d22-bc44-d5146da64726';

export const SettingsCustomTab = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>My Test App Settings</h2>
      <p>This is a custom settings tab provided by My Test App.</p>
      <p>Application creators can customize this component freely.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
  name: 'settings-custom-tab',
  description: 'Custom settings tab for the application',
  component: SettingsCustomTab,
});
```

```typescript
// packages/twenty-apps/my-test-app/src/application-config.ts
export default defineApplication({
  universalIdentifier: '52870ce6-e584-4bd4-bc1a-6c63c508982e',
  displayName: 'My test app',
  description: '',
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  settingsCustomTabFrontComponentUniversalIdentifier:
    SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
});
```
<img width="786" height="757" alt="Screenshot 2026-02-26 at 14 54 09"
src="https://github.com/user-attachments/assets/fcba70db-35da-48f1-bd65-359e894a691d"
/>
This commit is contained in:
Weiko
2026-02-26 16:26:49 +01:00
committed by GitHub
parent 2d2fc06265
commit 3bbaff801a
12 changed files with 114 additions and 5 deletions
@@ -304,6 +304,7 @@ export type Application = {
objects: Array<Object>;
packageJsonChecksum?: Maybe<Scalars['String']>;
packageJsonFileId?: Maybe<Scalars['UUID']>;
settingsCustomTabFrontComponentId?: Maybe<Scalars['UUID']>;
universalIdentifier: Scalars['String'];
version: Scalars['String'];
yarnLockChecksum?: Maybe<Scalars['String']>;
@@ -5527,7 +5528,7 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{
export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> };
export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -5539,7 +5540,7 @@ export type FindOneApplicationQueryVariables = Exact<{
}>;
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array<string>, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string }> } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
@@ -7164,6 +7165,7 @@ export const ApplicationFieldsFragmentDoc = gql`
universalIdentifier
canBeUninstalled
defaultRoleId
settingsCustomTabFrontComponentId
availablePackages
applicationVariables {
id
@@ -15,6 +15,7 @@ export const APPLICATION_FRAGMENT = gql`
universalIdentifier
canBeUninstalled
defaultRoleId
settingsCustomTabFrontComponentId
availablePackages
applicationVariables {
id
@@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import {
IconApps,
IconBox,
IconInfoCircle,
IconLock,
@@ -19,6 +20,7 @@ import { SettingsApplicationDetailContentTab } from '~/pages/settings/applicatio
import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab';
import { SettingsApplicationDetailSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab';
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
import { SettingsApplicationCustomTab } from '~/pages/settings/applications/tabs/SettingsApplicationCustomTab';
const APPLICATION_DETAIL_ID = 'application-detail-id';
@@ -43,11 +45,17 @@ export const SettingsApplicationDetails = () => {
? t`Application details`
: applicationName;
const settingsCustomTabFrontComponentId =
application?.settingsCustomTabFrontComponentId;
const tabs = [
{ id: 'about', title: t`About`, Icon: IconInfoCircle },
{ id: 'content', title: t`Content`, Icon: IconBox },
{ id: 'permissions', title: t`Permissions`, Icon: IconLock },
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
...(isDefined(settingsCustomTabFrontComponentId)
? [{ id: 'custom', title: t`Custom`, Icon: IconApps }]
: []),
];
const renderActiveTabContent = () => {
@@ -68,6 +76,16 @@ export const SettingsApplicationDetails = () => {
return (
<SettingsApplicationDetailSettingsTab application={application} />
);
case 'custom':
return isDefined(settingsCustomTabFrontComponentId) ? (
<SettingsApplicationCustomTab
settingsCustomTabFrontComponentId={
settingsCustomTabFrontComponentId
}
/>
) : (
<></>
);
default:
return <></>;
}
@@ -0,0 +1,32 @@
import styled from '@emotion/styled';
import { Suspense, lazy } from 'react';
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
(module) => ({ default: module.FrontComponentRenderer }),
),
);
const StyledContainer = styled.div`
height: 100%;
overflow: auto;
width: 100%;
`;
type SettingsApplicationCustomTabProps = {
settingsCustomTabFrontComponentId: string;
};
export const SettingsApplicationCustomTab = ({
settingsCustomTabFrontComponentId,
}: SettingsApplicationCustomTabProps) => {
return (
<StyledContainer>
<Suspense fallback={null}>
<FrontComponentRenderer
frontComponentId={settingsCustomTabFrontComponentId}
/>
</Suspense>
</StyledContainer>
);
};
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddSettingsCustomTabFrontComponentIdToApplication1771840510113
implements MigrationInterface
{
name = 'AddSettingsCustomTabFrontComponentIdToApplication1771840510113';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."application" ADD "settingsCustomTabFrontComponentId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."application" DROP COLUMN "settingsCustomTabFrontComponentId"`,
);
}
}
@@ -88,6 +88,9 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
@Field(() => RoleDTO, { nullable: true })
defaultRole: RoleDTO | null;
@Column({ nullable: true, type: 'uuid' })
settingsCustomTabFrontComponentId: string | null;
@Column({ nullable: false, type: 'boolean', default: true })
canBeUninstalled: boolean;
@@ -73,6 +73,11 @@ export class ApplicationDTO {
@Field({ nullable: true })
defaultRoleId?: string;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
settingsCustomTabFrontComponentId?: string;
@IsOptional()
@Field(() => RoleDTO, { nullable: true })
defaultLogicFunctionRole?: RoleDTO;
@@ -144,10 +144,12 @@ export class ApplicationManifestMigrationService {
flatRoleMaps: refreshedFlatRoleMaps,
flatObjectMetadataMaps: refreshedFlatObjectMetadataMaps,
flatFieldMetadataMaps: refreshedFlatFieldMetadataMaps,
flatFrontComponentMaps: refreshedFlatFrontComponentMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatRoleMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatFrontComponentMaps',
]);
let defaultRoleId: string | null = null;
@@ -181,11 +183,32 @@ export class ApplicationManifestMigrationService {
}
}
if (isDefined(defaultRoleId)) {
await this.applicationService.update(ownerFlatApplication.id, {
defaultRoleId,
let settingsCustomTabFrontComponentId: string | null = null;
const settingsCustomTabUniversalIdentifier =
manifest.application.settingsCustomTabFrontComponentUniversalIdentifier;
if (isDefined(settingsCustomTabUniversalIdentifier)) {
const flatFrontComponent = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatFrontComponentMaps,
universalIdentifier: settingsCustomTabUniversalIdentifier,
});
if (!isDefined(flatFrontComponent)) {
throw new ApplicationException(
`Failed to resolve front component for settingsCustomTabFrontComponentUniversalIdentifier ${settingsCustomTabUniversalIdentifier}`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
settingsCustomTabFrontComponentId = flatFrontComponent.id;
}
await this.applicationService.update(ownerFlatApplication.id, {
workspaceId,
settingsCustomTabFrontComponentId,
...(isDefined(defaultRoleId) ? { defaultRoleId } : {}),
});
}
private async syncApplicationRolePermissions({
@@ -13,6 +13,7 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages,
universalIdentifier,
version,
settingsCustomTabFrontComponentId,
}: FlatApplication): ApplicationDTO => {
return {
canBeUninstalled,
@@ -27,5 +28,7 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages: availablePackages ?? {},
universalIdentifier,
version: version ?? undefined,
settingsCustomTabFrontComponentId:
settingsCustomTabFrontComponentId ?? undefined,
};
};
@@ -36,6 +36,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
logicFunctionLayerId: null,
defaultRoleId: null,
defaultRole: null,
settingsCustomTabFrontComponentId: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),
@@ -31,6 +31,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
logicFunctionLayerId: null,
defaultRoleId: null,
defaultRole: null,
settingsCustomTabFrontComponentId: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),
@@ -21,6 +21,7 @@ export type ApplicationManifest = SyncableEntityOptions & {
marketplaceData?: ApplicationMarketplaceData;
preInstallLogicFunctionUniversalIdentifier?: string;
postInstallLogicFunctionUniversalIdentifier?: string;
settingsCustomTabFrontComponentUniversalIdentifier?: string;
packageJsonChecksum: string | null;
yarnLockChecksum: string | null;
apiClientChecksum: string | null;