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;