From 2e099c91e12d21c95323824c4ac264a0e6a94535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 23 Jun 2026 18:02:44 +0200 Subject: [PATCH] fix(domains): show custom domain DNS records and activation status without a page reload (#22037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Setting up a custom domain had two confusing UX issues, both caused by local state not being refreshed after the relevant mutation: 1. **DNS records didn't appear after saving.** After hitting save you got the green "Custom domain updated" snackbar, but the "Domain Setup" section (the Cloudflare/DNS records to configure) stayed empty. You had to leave the page and come back for the records to show up. 2. **The "Custom Domain" card stayed "Inactive"** even after the DNS records validated as "Success". Only a full page reload flipped it to "Active". ## Root cause **Issue 1 — stale closure.** In `useSettingsCustomDomain.handleSave`, the `updateWorkspace` `onCompleted` callback called `setCurrentWorkspace({ ...currentWorkspace, customDomain })` and then `checkCustomDomainRecords()`. But `checkCustomDomainRecords` guarded on the closed-over `currentWorkspace.customDomain`, which was still `null` at that render. The `setCurrentWorkspace` call doesn't synchronously update that captured value, so the guard returned early and the records were never fetched. Remounting the page (navigate away/back) ran the on-mount effect with a fresh workspace, which is why the trip "fixed" it. **Issue 2 — `isCustomDomainEnabled` never refreshed locally.** The Active/Inactive badge is driven by `currentWorkspace.isCustomDomainEnabled`. The backend flips this flag inside `checkCustomDomainValidRecords` (`custom-domain-manager.service.ts`), but the mutation didn't return it, so the local `currentWorkspaceState` stayed stale until a full reload re-ran the bootstrap query. The green "Success" DNS rows read from a different source (`record.status`), which is why the rows and the badge disagreed. ## Changes **Issue 1** - `checkCustomDomainRecords` now accepts the domain explicitly (defaulting to the workspace value), so the freshly-saved domain can be passed straight from `handleSave` instead of relying on the stale closure. No new `useEffect` introduced. - Fixed the Reload button so it no longer passes its click event as the domain argument. **Issue 2** - Added a nullable `isCustomDomainEnabled` field to the `DomainValidRecords` GraphQL type, populated only by the custom-domain check (the shared public-domain flow leaves it null, so it's backward compatible). - The frontend now writes that value back into `currentWorkspaceState` when the check completes, using a **functional** Jotai update so a concurrent `customDomain` update is never clobbered. The badge flips to "Active" as soon as validation passes — on mount, on Reload, and right after save. I deliberately kept this targeted rather than introducing real-time workspace sync: `isCustomDomainEnabled` only changes server-side during the on-demand DNS check (mount/Reload/cron), so returning it from that mutation is sufficient and far lower risk. ## Notes - `packages/twenty-front/src/generated-metadata/graphql.ts` was updated to match what `graphql:generate` produces for the new schema field (codegen requires a running backend, which isn't available in this environment). Worth re-running codegen in CI to confirm it's byte-identical. - No existing unit or integration tests reference these paths. ## Test plan - [ ] Set a custom domain → DNS records appear immediately (no navigation needed). - [ ] Once DNS validates, the "Custom Domain" card flips to "Active" without a reload. - [ ] Reload button still refreshes records. - [ ] Public domain validation flow is unaffected. https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj --- _Generated by [Claude Code](https://claude.ai/code/session_01BB6C6bpPZMUbMzKSCydEaj)_ Review in cubic --- .../src/generated-metadata/graphql.ts | 5 ++-- .../components/SettingsCustomDomain.tsx | 2 +- .../hooks/useCheckCustomDomainValidRecords.ts | 30 +++++++++++++++---- .../domains/hooks/useSettingsCustomDomain.ts | 2 +- .../queries/checkCustomDomainValidRecords.ts | 1 + .../dns-manager/dtos/domain-valid-records.ts | 3 ++ .../services/custom-domain-manager.service.ts | 5 +++- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0d542cf7dc..71b16415c1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1469,6 +1469,7 @@ export type DomainValidRecords = { __typename?: 'DomainValidRecords'; domain: Scalars['String']['output']; id: Scalars['UUID']['output']; + isCustomDomainEnabled?: Maybe; records: Array; }; @@ -8613,7 +8614,7 @@ export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorks export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>; -export type CheckCustomDomainValidRecordsMutation = { __typename?: 'Mutation', checkCustomDomainValidRecords?: { __typename?: 'DomainValidRecords', id: string, domain: string, records: Array<{ __typename?: 'DomainRecord', type: string, key: string, value: string, validationType: string, status: string }> } | null }; +export type CheckCustomDomainValidRecordsMutation = { __typename?: 'Mutation', checkCustomDomainValidRecords?: { __typename?: 'DomainValidRecords', id: string, domain: string, isCustomDomainEnabled?: boolean | null, records: Array<{ __typename?: 'DomainRecord', type: string, key: string, value: string, validationType: string, status: string }> } | null }; export type GetAiSystemPromptPreviewQueryVariables = Exact<{ [key: string]: never; }>; @@ -8953,7 +8954,7 @@ export const ActivateWorkspaceDocument = {"kind":"Document","definitions":[{"kin export const DeleteCurrentWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCurrentWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteCurrentWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkspaceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customDomain"}},{"kind":"Field","name":{"kind":"Name","value":"subdomain"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}},{"kind":"Field","name":{"kind":"Name","value":"isPublicInviteLinkEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isGoogleAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isMicrosoftAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isPasswordAuthEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"isTwoFactorAuthenticationEnforced"}},{"kind":"Field","name":{"kind":"Name","value":"isInternalMessagesImportEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRole"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RoleFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RoleFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Role"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"isEditable"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToUsers"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToAgents"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToApiKeys"}}]}}]} as unknown as DocumentNode; export const UploadWorkspaceLogoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceLogo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceLogo"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; -export const CheckCustomDomainValidRecordsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"validationType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CheckCustomDomainValidRecordsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"checkCustomDomainValidRecords"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"domain"}},{"kind":"Field","name":{"kind":"Name","value":"isCustomDomainEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"validationType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetAiSystemPromptPreviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAiSystemPromptPreview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAiSystemPromptPreview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTokenCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTokenCount"}}]}}]}}]} as unknown as DocumentNode; export const GetPublicWorkspaceDataByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPublicWorkspaceDataById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getPublicWorkspaceDataById"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}}]}}]}}]} as unknown as DocumentNode; export const GetWorkspaceFromInviteHashDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceFromInviteHash"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteHash"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findWorkspaceFromInviteHash"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteHash"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"allowImpersonation"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx index a8f584b86d..83edb0f91a 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingsCustomDomain.tsx @@ -110,7 +110,7 @@ export const SettingsCustomDomain = () => { Icon={IconReload} title={t`Reload`} variant="primary" - onClick={checkCustomDomainRecords} + onClick={() => checkCustomDomainRecords()} type="button" /> diff --git a/packages/twenty-front/src/modules/settings/domains/hooks/useCheckCustomDomainValidRecords.ts b/packages/twenty-front/src/modules/settings/domains/hooks/useCheckCustomDomainValidRecords.ts index 98ae922aa2..d8484ef234 100644 --- a/packages/twenty-front/src/modules/settings/domains/hooks/useCheckCustomDomainValidRecords.ts +++ b/packages/twenty-front/src/modules/settings/domains/hooks/useCheckCustomDomainValidRecords.ts @@ -5,21 +5,24 @@ import { isDefined } from 'twenty-shared/utils'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; export const useCheckCustomDomainValidRecords = () => { const [checkCustomDomainValidRecords] = useMutation( CheckCustomDomainValidRecordsDocument, ); const { enqueueErrorSnackBar } = useSnackBar(); - const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const [currentWorkspace, setCurrentWorkspace] = useAtomState( + currentWorkspaceState, + ); const [{ isLoading }, setCustomDomainRecords] = useAtomState( customDomainRecordsState, ); - const checkCustomDomainRecords = () => { - if (isLoading || !currentWorkspace?.customDomain) { + const checkCustomDomainRecords = ( + customDomain: string | null | undefined = currentWorkspace?.customDomain, + ) => { + if (isLoading || !customDomain) { return; } setCustomDomainRecords((currentState) => ({ @@ -28,13 +31,28 @@ export const useCheckCustomDomainValidRecords = () => { })); checkCustomDomainValidRecords({ onCompleted: (data) => { + const validRecords = data.checkCustomDomainValidRecords; + setCustomDomainRecords((currentState) => ({ ...currentState, isLoading: false, - ...(isDefined(data.checkCustomDomainValidRecords) - ? { customDomainRecords: data.checkCustomDomainValidRecords } + ...(isDefined(validRecords) + ? { customDomainRecords: validRecords } : {}), })); + + const nextIsCustomDomainEnabled = validRecords?.isCustomDomainEnabled; + + if (isDefined(nextIsCustomDomainEnabled)) { + setCurrentWorkspace((previousWorkspace) => + isDefined(previousWorkspace) + ? { + ...previousWorkspace, + isCustomDomainEnabled: nextIsCustomDomainEnabled, + } + : previousWorkspace, + ); + } }, onError: (error) => { enqueueErrorSnackBar({ apolloError: error }); diff --git a/packages/twenty-front/src/modules/settings/domains/hooks/useSettingsCustomDomain.ts b/packages/twenty-front/src/modules/settings/domains/hooks/useSettingsCustomDomain.ts index 4093167819..676aa5a86f 100644 --- a/packages/twenty-front/src/modules/settings/domains/hooks/useSettingsCustomDomain.ts +++ b/packages/twenty-front/src/modules/settings/domains/hooks/useSettingsCustomDomain.ts @@ -65,7 +65,7 @@ export const useSettingsCustomDomain = () => { }); enqueueSuccessSnackBar({ message: t`Custom domain updated` }); setIsSubmitting(false); - checkCustomDomainRecords(); + checkCustomDomainRecords(domainValue); }, onError: (mutationError) => { if ( diff --git a/packages/twenty-front/src/modules/workspace/graphql/queries/checkCustomDomainValidRecords.ts b/packages/twenty-front/src/modules/workspace/graphql/queries/checkCustomDomainValidRecords.ts index 4f85bd9387..172ad6aa31 100644 --- a/packages/twenty-front/src/modules/workspace/graphql/queries/checkCustomDomainValidRecords.ts +++ b/packages/twenty-front/src/modules/workspace/graphql/queries/checkCustomDomainValidRecords.ts @@ -5,6 +5,7 @@ export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS = gql` checkCustomDomainValidRecords { id domain + isCustomDomainEnabled records { type key diff --git a/packages/twenty-server/src/engine/core-modules/dns-manager/dtos/domain-valid-records.ts b/packages/twenty-server/src/engine/core-modules/dns-manager/dtos/domain-valid-records.ts index 7a65a463aa..c4e5a1a93c 100644 --- a/packages/twenty-server/src/engine/core-modules/dns-manager/dtos/domain-valid-records.ts +++ b/packages/twenty-server/src/engine/core-modules/dns-manager/dtos/domain-valid-records.ts @@ -30,4 +30,7 @@ export class DomainValidRecords { @Field(() => [DomainRecord]) records: Array; + + @Field(() => Boolean, { nullable: true }) + isCustomDomainEnabled?: boolean; } diff --git a/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts index 052f3fff4e..d9433a7d4d 100644 --- a/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts @@ -124,6 +124,9 @@ export class CustomDomainManagerService { ); } - return customDomainWithRecords; + return { + ...customDomainWithRecords, + isCustomDomainEnabled: workspace.isCustomDomainEnabled, + }; } }