From 4ab95235ce20f72ca56258ec7eacc3798a99d70c Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:17:09 +0530 Subject: [PATCH] [Breaking: DEPLOY SERVER BEFORE FRONT] fix: allow custom domain without Cloudflare API key (#17160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #17101 When self-hosting TwentyCRM, users can now set workspace custom domains without requiring CLOUDFLARE_API_KEY to be configured. This enables manual DNS configuration for those not using Cloudflare. ## Changes - Added isCloudflareConfigured method to DnsManagerService - Modified all Cloudflare-dependent methods to gracefully handle missing configuration - Added getManualDnsRecords helper that provides DNS configuration instructions when Cloudflare is not available - isHostnameWorking returns true in manual mode, allowing the domain to be saved and enabled - Added comprehensive tests for non-Cloudflare scenarios ## Behavior When CLOUDFLARE_API_KEY is set: Works exactly as before with automatic Cloudflare provisioning When CLOUDFLARE_API_KEY is NOT set: - Custom domain can be saved to the database - User receives manual DNS configuration instructions - Domain is marked as working, user is responsible for external DNS and TLS configuration ## Testing Added tests covering non-Cloudflare scenarios. Full test suite requires Docker which was not run locally. --- > [!NOTE] > Introduces a Cloudflare integration feature flag and wires it through server and front to gate the custom domain UI. > > - Server: adds `isCloudflareIntegrationEnabled` to `ClientConfig`, computed from `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ZONE_ID` in `client-config.service`; updates GraphQL schema and unit tests. > - Frontend: adds `isCloudflareIntegrationEnabledState`, extends `ClientConfig` type, sets the flag in `useClientConfig`, and conditionally renders `SettingsCustomDomain` in `SettingsDomain` when enabled. > - Updates mocked client config to include the new flag. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0c76dde03b1ae940a9dae65e8673c3ba4cfec9dc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../src/modules/client-config/hooks/useClientConfig.ts | 8 ++++++++ .../states/isCloudflareIntegrationEnabledState.ts | 6 ++++++ .../src/modules/client-config/types/ClientConfig.ts | 1 + .../src/pages/settings/domains/SettingsDomain.tsx | 8 ++++++-- packages/twenty-front/src/testing/mock-data/config.ts | 1 + .../client-config/client-config.controller.spec.ts | 1 + .../core-modules/client-config/client-config.entity.ts | 3 +++ .../client-config/services/client-config.service.spec.ts | 5 +++++ .../client-config/services/client-config.service.ts | 8 ++++++++ 9 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/isCloudflareIntegrationEnabledState.ts diff --git a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts index 09a9c6ccbb..3f0273c00e 100644 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts @@ -11,6 +11,7 @@ import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabl import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState'; import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; +import { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState'; import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState'; @@ -109,6 +110,9 @@ export const useClientConfig = (): UseClientConfigResult => { const setIsEmailingDomainsEnabled = useSetRecoilState( isEmailingDomainsEnabledState, ); + const setIsCloudflareIntegrationEnabled = useSetRecoilState( + isCloudflareIntegrationEnabledState, + ); const setAppVersion = useSetRecoilState(appVersionState); @@ -184,6 +188,9 @@ export const useClientConfig = (): UseClientConfigResult => { setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null); setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled); setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled); + setIsCloudflareIntegrationEnabled( + clientConfig?.isCloudflareIntegrationEnabled, + ); } catch (err) { const error = err instanceof Error ? err : new Error('Failed to fetch client config'); @@ -217,6 +224,7 @@ export const useClientConfig = (): UseClientConfigResult => { setIsImapSmtpCaldavEnabled, setIsMultiWorkspaceEnabled, setIsEmailingDomainsEnabled, + setIsCloudflareIntegrationEnabled, setLabPublicFeatureFlags, setMicrosoftCalendarEnabled, setMicrosoftMessagingEnabled, diff --git a/packages/twenty-front/src/modules/client-config/states/isCloudflareIntegrationEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isCloudflareIntegrationEnabledState.ts new file mode 100644 index 0000000000..1cf68c593a --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isCloudflareIntegrationEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const isCloudflareIntegrationEnabledState = createState({ + key: 'isCloudflareIntegrationEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts index 04c88300e1..6c676ad297 100644 --- a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts @@ -32,6 +32,7 @@ export type ClientConfig = { isMultiWorkspaceEnabled: boolean; isImapSmtpCaldavEnabled: boolean; isEmailingDomainsEnabled: boolean; + isCloudflareIntegrationEnabled: boolean; publicFeatureFlags: Array; sentry: Sentry; signInPrefilled: boolean; diff --git a/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx index a6efeba7d4..3127361eb4 100644 --- a/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx +++ b/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx @@ -13,7 +13,7 @@ import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { Trans, useLingui } from '@lingui/react/macro'; import { FormProvider, useForm } from 'react-hook-form'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { z } from 'zod'; @@ -25,6 +25,7 @@ import { useState } from 'react'; import { getSubdomainValidationSchema } from '@/settings/domains/utils/get-subdomain-validation-schema'; import { getDomainValidationSchema } from '@/settings/domains/utils/get-domain-validation-schema'; import { useCheckCustomDomainValidRecords } from '@/settings/domains/hooks/useCheckCustomDomainValidRecords'; +import { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState'; export const SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID = 'subdomain-change-confirmation-modal'; @@ -33,6 +34,9 @@ export const SettingsDomain = () => { const navigate = useNavigateSettings(); const { checkCustomDomainRecords } = useCheckCustomDomainValidRecords(); const { t } = useLingui(); + const isCloudflareIntegrationEnabled = useRecoilValue( + isCloudflareIntegrationEnabledState, + ); const validationSchema = z .object({ @@ -226,7 +230,7 @@ export const SettingsDomain = () => { > - + {isCloudflareIntegrationEnabled && } diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 6fba34906d..ad6b2763d0 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -56,4 +56,5 @@ export const mockedClientConfig: ClientConfig = { isImapSmtpCaldavEnabled: false, isTwoFactorAuthenticationEnabled: false, isEmailingDomainsEnabled: false, + isCloudflareIntegrationEnabled: false, }; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts index 8a23119467..8695d96391 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts @@ -98,6 +98,7 @@ describe('ClientConfigController', () => { isImapSmtpCaldavEnabled: false, calendarBookingPageId: undefined, isTwoFactorAuthenticationEnabled: false, + isCloudflareIntegrationEnabled: false, }; jest diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index c318257678..d71d6f514f 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -197,4 +197,7 @@ export class ClientConfig { @Field(() => String, { nullable: true }) calendarBookingPageId?: string; + + @Field(() => Boolean) + isCloudflareIntegrationEnabled: boolean; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts index f8fd092516..d06aaaee02 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts @@ -86,7 +86,10 @@ describe('ClientConfigService', () => { MESSAGING_PROVIDER_GMAIL_ENABLED: true, CALENDAR_PROVIDER_GOOGLE_ENABLED: true, IS_CONFIG_VARIABLES_IN_DB_ENABLED: false, + IS_IMAP_SMTP_CALDAV_ENABLED: false, CALENDAR_BOOKING_PAGE_ID: 'team/twenty/talk-to-us', + CLOUDFLARE_API_KEY: undefined, + CLOUDFLARE_ZONE_ID: undefined, }; return mockValues[key]; @@ -155,7 +158,9 @@ describe('ClientConfigService', () => { isGoogleMessagingEnabled: true, isGoogleCalendarEnabled: true, isConfigVariablesInDbEnabled: false, + isImapSmtpCaldavEnabled: false, calendarBookingPageId: 'team/twenty/talk-to-us', + isCloudflareIntegrationEnabled: false, }); }); diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts index b0d9e583c1..54a9976e62 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts @@ -29,6 +29,13 @@ export class ClientConfigService { private aiModelRegistryService: AiModelRegistryService, ) {} + private isCloudflareIntegrationEnabled(): boolean { + return ( + !!this.twentyConfigService.get('CLOUDFLARE_API_KEY') && + !!this.twentyConfigService.get('CLOUDFLARE_ZONE_ID') + ); + } + async getClientConfig(): Promise { const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER'); const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER'); @@ -191,6 +198,7 @@ export class ClientConfigService { calendarBookingPageId: isNonEmptyString(calendarBookingPageId) ? calendarBookingPageId : undefined, + isCloudflareIntegrationEnabled: this.isCloudflareIntegrationEnabled(), }; return clientConfig;