diff --git a/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx b/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx index 3a8207915f..54151c3201 100644 --- a/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx +++ b/packages/twenty-docs/user-guide/settings/capabilities/domains-settings.mdx @@ -1,6 +1,6 @@ --- title: Domain Settings -description: Configure workspace domain, approved access domains, and public domains. +description: Configure workspace domain, approved access domains, and app custom domains. --- Domain settings live in three places, depending on what you're configuring. @@ -35,17 +35,16 @@ Once configured, anyone with an email address at that domain can join your works This is useful for allowing your entire team to self-register while keeping the workspace restricted to your organization. -## Public Domains +## App Custom Domains -Configure under **Settings → Apps → Developer**. +Configure under **Settings → Apps → your app → Settings → App URL**. -Provision a complete and secure hosting environment on these domains. A public domain can be bound to a specific app — when bound, only that app's HTTP-routed logic functions are reachable on the domain. Leave the binding empty to expose all workspace HTTP routes. +Apps that expose routes on the web are served from a dedicated URL, shown in the **App URL** section. You can serve an app from a domain you own instead. -### Add Public Domain -1. Click **Add Public Domain** -2. Enter the domain you want to use -3. Optionally bind it to an app -4. Configure DNS settings as instructed -5. Verify the domain +### Add Custom Domain +1. Click **Add Custom Domain** +2. Enter the domain you want to use (e.g., `app.yourcompany.com`) +3. Configure DNS settings as instructed +4. Verify the domain -SSL certificates are automatically provisioned for public domains. +Only that app's routes are reachable on the domain. SSL certificates are provisioned automatically. diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx index 16f16cfed9..bdd68aeee2 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx @@ -1,9 +1,6 @@ -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { FrontComponentRendererProvider } from '@/front-components/components/FrontComponentRendererProvider'; import { FrontComponentRendererWithSdkClient } from '@/front-components/components/FrontComponentRendererWithSdkClient'; -import { getFunctionsBaseUrl } from '@/settings/logic-functions/utils/getLogicFunctionHttpUrl'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useGetLogicFunctionHttpUrl } from '@/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl'; import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext'; import { useOnFrontComponentUpdated } from '@/front-components/hooks/useOnFrontComponentUpdated'; import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState'; @@ -33,8 +30,7 @@ export const FrontComponentRenderer = ({ const { colorScheme } = useContext(ThemeContext); const { enqueueErrorSnackBar } = useSnackBar(); - const { publicFunctionDomain } = useAtomStateValue(domainConfigurationState); - const currentWorkspace = useAtomStateValue(currentWorkspaceState); + const { functionsBaseUrl } = useGetLogicFunctionHttpUrl(); const setFrontComponentApplicationTokenPair = useSetAtomComponentState( frontComponentApplicationTokenPairComponentState, @@ -111,12 +107,6 @@ export const FrontComponentRenderer = ({ const applicationVariables = data.frontComponent.applicationVariables ?? undefined; - const functionsBaseUrl = - getFunctionsBaseUrl({ - publicFunctionDomain, - workspaceSubdomain: currentWorkspace?.subdomain, - }) ?? `${REACT_APP_SERVER_BASE_URL}/s`; - if (usesSdkClient) { return ( diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx index 9bdfc3edca..564581d204 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx @@ -14,7 +14,7 @@ type FrontComponentRendererWithSdkClientProps = { componentUrl: string; applicationAccessToken: string; applicationId: string; - functionsBaseUrl?: string; + functionsBaseUrl: string; executionContext: FrontComponentExecutionContext; frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi; applicationVariables?: Record; diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx index 1989127496..0080ae8725 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomain.tsx @@ -93,7 +93,7 @@ export const SettingPublicDomain = () => { variables: { domain: selectedPublicDomain.domain }, onCompleted: () => { enqueueSuccessSnackBar({ - message: t`Public domain successfully deleted`, + message: t`Custom domain successfully deleted`, }); navigate(SettingsPath.Applications); refetchPublicDomains(); @@ -132,7 +132,7 @@ export const SettingPublicDomain = () => { onCompleted: (data) => { setSelectedPublicDomain(data.createPublicDomain); enqueueSuccessSnackBar({ - message: t`Public domain created successfully`, + message: t`Custom domain successfully created`, }); }, onError: (error) => { @@ -146,7 +146,7 @@ export const SettingPublicDomain = () => { return ( Workspace, @@ -156,7 +156,7 @@ export const SettingPublicDomain = () => { children: Apps, href: getSettingsPath(SettingsPath.Applications), }, - { children: Public Domain }, + { children: Custom Domain }, ]} actionButton={ {
{isDefined(selectedPublicDomain) && ( { error={newPublicDomainError} type="text" disabled={isDefined(selectedPublicDomain)} - placeholder="crm.yourPublicDomain.com" + placeholder="app.yourdomain.com" fullWidth /> {isDefined(selectedPublicDomain) && ( diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomainRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomainRowDropdownMenu.tsx index 4e7fc1f666..700cd59a77 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomainRowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingPublicDomainRowDropdownMenu.tsx @@ -39,7 +39,7 @@ export const SettingPublicDomainRowDropdownMenu = ({ }, onCompleted: () => enqueueSuccessSnackBar({ - message: t`Public domain successfully deleted`, + message: t`Custom domain successfully deleted`, }), onError: (error) => enqueueErrorSnackBar({ apolloError: error }), }); diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingsPublicDomainsListCard.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingsPublicDomainsListCard.tsx index 5192e2e7db..0d6a68b7ac 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingsPublicDomainsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingsPublicDomainsListCard.tsx @@ -1,13 +1,14 @@ import { SettingsCard } from '@/settings/components/SettingsCard'; import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { SettingPublicDomainRowDropdownMenu } from '@/settings/domains/components/SettingPublicDomainRowDropdownMenu'; +import { useGetAddedRelativeDateDescription } from '@/settings/hooks/useGetAddedRelativeDateDescription'; import { selectedApplicationIdForPublicDomainState } from '@/settings/domains/states/selectedApplicationIdForPublicDomainState'; import { selectedPublicDomainState } from '@/settings/domains/states/selectedPublicDomainState'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useLingui } from '@lingui/react/macro'; import { SettingsPath } from 'twenty-shared/types'; import { Status } from 'twenty-ui/data-display'; -import { IconAt, IconMailCog } from 'twenty-ui/icon'; +import { IconWorld } from 'twenty-ui/icon'; import { useQuery } from '@apollo/client/react'; import { type PublicDomain, @@ -23,6 +24,8 @@ export const SettingsPublicDomainsListCard = ({ const navigate = useNavigateSettings(); const { t } = useLingui(); + const { getAddedRelativeDateDescription } = + useGetAddedRelativeDateDescription(); const setSelectedPublicDomain = useSetAtomState(selectedPublicDomainState); const setSelectedApplicationIdForPublicDomain = useSetAtomState( @@ -48,8 +51,8 @@ export const SettingsPublicDomainsListCard = ({ if (publicDomains.length === 0) { return ( } + title={t`Add Custom Domain`} + Icon={} onClick={navigateToCreate} /> ); @@ -59,8 +62,10 @@ export const SettingsPublicDomainsListCard = ({ domain} - getItemDescription={({ createdAt }) => createdAt} - RowIcon={IconAt} + getItemDescription={({ createdAt }) => + getAddedRelativeDateDescription(createdAt) + } + RowIcon={IconWorld} onRowClick={(publicDomain: PublicDomain) => { setSelectedPublicDomain(publicDomain); setSelectedApplicationIdForPublicDomain(applicationId); @@ -75,7 +80,7 @@ export const SettingsPublicDomainsListCard = ({ )} hasFooter - footerButtonLabel={t`Add Public Domain`} + footerButtonLabel={t`Add Custom Domain`} onFooterButtonClick={navigateToCreate} /> ); diff --git a/packages/twenty-front/src/modules/settings/hooks/useGetAddedRelativeDateDescription.ts b/packages/twenty-front/src/modules/settings/hooks/useGetAddedRelativeDateDescription.ts new file mode 100644 index 0000000000..3327caaf0c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/hooks/useGetAddedRelativeDateDescription.ts @@ -0,0 +1,19 @@ +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useLingui } from '@lingui/react/macro'; +import { dateLocaleState } from '~/localization/states/dateLocaleState'; +import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; + +export const useGetAddedRelativeDateDescription = () => { + const { t } = useLingui(); + const { localeCatalog } = useAtomStateValue(dateLocaleState); + + const getAddedRelativeDateDescription = (createdAt: string) => { + const beautifiedCreatedAt = beautifyPastDateRelativeToNow( + createdAt, + localeCatalog, + ); + return t`Added ${beautifiedCreatedAt}`; + }; + + return { getAddedRelativeDateDescription }; +}; diff --git a/packages/twenty-front/src/modules/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl.ts b/packages/twenty-front/src/modules/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl.ts index 33843b928b..84e2679bd3 100644 --- a/packages/twenty-front/src/modules/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl.ts +++ b/packages/twenty-front/src/modules/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl.ts @@ -2,7 +2,10 @@ import { useCallback } from 'react'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { getLogicFunctionHttpUrl } from '@/settings/logic-functions/utils/getLogicFunctionHttpUrl'; +import { + getFunctionsBaseUrl, + getLogicFunctionHttpUrl, +} from '@/settings/logic-functions/utils/getLogicFunctionHttpUrl'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -23,9 +26,14 @@ export const useGetLogicFunctionHttpUrl = () => { [publicFunctionDomain, workspaceSubdomain], ); - return { - getLogicFunctionHttpUrl: getHttpUrl, + const functionsBaseUrl = getFunctionsBaseUrl({ + serverBaseUrl: REACT_APP_SERVER_BASE_URL, publicFunctionDomain, workspaceSubdomain, + }); + + return { + getLogicFunctionHttpUrl: getHttpUrl, + functionsBaseUrl, }; }; diff --git a/packages/twenty-front/src/modules/settings/logic-functions/utils/__tests__/getLogicFunctionHttpUrl.test.ts b/packages/twenty-front/src/modules/settings/logic-functions/utils/__tests__/getLogicFunctionHttpUrl.test.ts index f72740aa6c..b93f1c4232 100644 --- a/packages/twenty-front/src/modules/settings/logic-functions/utils/__tests__/getLogicFunctionHttpUrl.test.ts +++ b/packages/twenty-front/src/modules/settings/logic-functions/utils/__tests__/getLogicFunctionHttpUrl.test.ts @@ -7,28 +7,31 @@ describe('getFunctionsBaseUrl', () => { it('builds the isolated base from subdomain + public domain', () => { expect( getFunctionsBaseUrl({ + serverBaseUrl: 'https://api.twenty.com', publicFunctionDomain: 'withtwenty.com', workspaceSubdomain: 'acme', }), ).toBe('https://acme.withtwenty.com'); }); - it('returns undefined when the public domain is missing', () => { + it('falls back to the /s server route when the public domain is missing', () => { expect( getFunctionsBaseUrl({ + serverBaseUrl: 'https://api.twenty.com', publicFunctionDomain: null, workspaceSubdomain: 'acme', }), - ).toBeUndefined(); + ).toBe('https://api.twenty.com/s'); }); - it('returns undefined when the subdomain is missing', () => { + it('falls back to the /s server route when the subdomain is missing', () => { expect( getFunctionsBaseUrl({ + serverBaseUrl: 'https://api.twenty.com', publicFunctionDomain: 'withtwenty.com', workspaceSubdomain: undefined, }), - ).toBeUndefined(); + ).toBe('https://api.twenty.com/s'); }); }); diff --git a/packages/twenty-front/src/modules/settings/logic-functions/utils/getLogicFunctionHttpUrl.ts b/packages/twenty-front/src/modules/settings/logic-functions/utils/getLogicFunctionHttpUrl.ts index d29d52a4fe..5a18bfe066 100644 --- a/packages/twenty-front/src/modules/settings/logic-functions/utils/getLogicFunctionHttpUrl.ts +++ b/packages/twenty-front/src/modules/settings/logic-functions/utils/getLogicFunctionHttpUrl.ts @@ -1,12 +1,14 @@ import { isNonEmptyString } from '@sniptt/guards'; export const getFunctionsBaseUrl = ({ + serverBaseUrl, publicFunctionDomain, workspaceSubdomain, }: { + serverBaseUrl: string; publicFunctionDomain?: string | null; workspaceSubdomain?: string | null; -}): string | undefined => { +}): string => { if ( isNonEmptyString(publicFunctionDomain) && isNonEmptyString(workspaceSubdomain) @@ -14,7 +16,7 @@ export const getFunctionsBaseUrl = ({ return `https://${workspaceSubdomain}.${publicFunctionDomain}`; } - return undefined; + return `${serverBaseUrl}/s`; }; export const getLogicFunctionHttpUrl = ({ @@ -31,13 +33,10 @@ export const getLogicFunctionHttpUrl = ({ const normalizedPath = path.startsWith('/') ? path : `/${path}`; const functionsBaseUrl = getFunctionsBaseUrl({ + serverBaseUrl, publicFunctionDomain, workspaceSubdomain, }); - if (isNonEmptyString(functionsBaseUrl)) { - return `${functionsBaseUrl}${normalizedPath}`; - } - - return `${serverBaseUrl}/s${normalizedPath}`; + return `${functionsBaseUrl}${normalizedPath}`; }; diff --git a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx index f757f8ab5b..0eda498501 100644 --- a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx @@ -4,6 +4,7 @@ import { SettingsPath } from 'twenty-shared/types'; import { SettingsCard } from '@/settings/components/SettingsCard'; import { SettingsListCard } from '@/settings/components/SettingsListCard'; +import { useGetAddedRelativeDateDescription } from '@/settings/hooks/useGetAddedRelativeDateDescription'; import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu'; import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect'; import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState'; @@ -12,14 +13,11 @@ import { styled } from '@linaria/react'; import { useEffect } from 'react'; import { useLingui } from '@lingui/react/macro'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { getSettingsPath } from 'twenty-shared/utils'; import { Status } from 'twenty-ui/data-display'; import { IconAt, IconMailCog } from 'twenty-ui/icon'; import { useQuery } from '@apollo/client/react'; import { GetApprovedAccessDomainsDocument } from '~/generated-metadata/graphql'; -import { dateLocaleState } from '~/localization/states/dateLocaleState'; -import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; const StyledLinkContainer = styled.div` > a { @@ -30,7 +28,8 @@ const StyledLinkContainer = styled.div` export const SettingsApprovedAccessDomainsListCard = () => { const navigate = useNavigate(); const { t } = useLingui(); - const { localeCatalog } = useAtomStateValue(dateLocaleState); + const { getAddedRelativeDateDescription } = + useGetAddedRelativeDateDescription(); const [approvedAccessDomains, setApprovedAccessDomains] = useAtomState( approvedAccessDomainsState, @@ -52,14 +51,6 @@ export const SettingsApprovedAccessDomainsListCard = () => { useSnackBarOnQueryError(domainsError); - const getItemDescription = (createdAt: string) => { - const beautifyPastDateRelative = beautifyPastDateRelativeToNow( - createdAt, - localeCatalog, - ); - return t`Added ${beautifyPastDateRelative}`; - }; - return loading || !approvedAccessDomains.length ? ( @@ -75,7 +66,9 @@ export const SettingsApprovedAccessDomainsListCard = () => { domain} - getItemDescription={({ createdAt }) => getItemDescription(createdAt)} + getItemDescription={({ createdAt }) => + getAddedRelativeDateDescription(createdAt) + } RowIcon={IconAt} RowRightComponent={({ item: approvedAccessDomain }) => ( <> diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection.tsx index c79a6304f1..e42d0a011f 100644 --- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection.tsx +++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection.tsx @@ -1,16 +1,21 @@ +import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { isNonEmptyString } from '@sniptt/guards'; import { SettingsPublicDomainsListCard } from '@/settings/domains/components/SettingsPublicDomainsListCard'; import { useGetLogicFunctionHttpUrl } from '@/settings/logic-functions/hooks/useGetLogicFunctionHttpUrl'; -import { getFunctionsBaseUrl } from '@/settings/logic-functions/utils/getLogicFunctionHttpUrl'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; -import { Info } from 'twenty-ui/feedback'; import { IconCopy } from 'twenty-ui/icon'; import { Section } from 'twenty-ui/layout'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; import { H2Title } from 'twenty-ui/typography'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; +const StyledContent = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[4]}; +`; + export const SettingsApplicationFunctionDomainSection = ({ applicationId, }: { @@ -18,46 +23,28 @@ export const SettingsApplicationFunctionDomainSection = ({ }) => { const { t } = useLingui(); const { copyToClipboard } = useCopyToClipboard(); - const { publicFunctionDomain, workspaceSubdomain } = - useGetLogicFunctionHttpUrl(); - - const baseUrl = getFunctionsBaseUrl({ - publicFunctionDomain, - workspaceSubdomain, - }); + const { functionsBaseUrl } = useGetLogicFunctionHttpUrl(); return ( - <> - {isNonEmptyString(baseUrl) && ( -
- - {}} - readOnly - fullWidth - RightIcon={IconCopy} - onRightIconClick={() => - copyToClipboard(baseUrl, t`URL copied to clipboard`) - } - /> - -
- )} -
- + + + {}} + readOnly + fullWidth + RightIcon={IconCopy} + onRightIconClick={() => + copyToClipboard(functionsBaseUrl, t`URL copied to clipboard`) + } /> -
- + +
); };