diff --git a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx index f4e4c6694d970..f3cdccc24d5c9 100644 --- a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx +++ b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx @@ -1,5 +1,5 @@ import { Mdx } from '@/components/mdx-components' -import { DocsPager } from '@/components/pager' +import { DocsPager, getBreadcrumbSegments } from '@/components/pager' import { SourcePanel } from '@/components/source-panel' import { DashboardTableOfContents } from '@/components/toc' import { siteConfig } from '@/config/site' @@ -12,6 +12,7 @@ import '@/styles/mdx.css' import { allDocs } from 'contentlayer/generated' import { ChevronRight } from 'lucide-react' import type { Metadata } from 'next' +import Link from 'next/link' import { notFound } from 'next/navigation' import Balancer from 'react-wrap-balancer' import { ScrollArea, Separator } from 'ui' @@ -83,15 +84,39 @@ export default async function DocPage(props: DocPageProps) { } const toc = await getTableOfContents(doc.body.raw) + const breadcrumbSegments = getBreadcrumbSegments(doc) return (
-
-
Docs
- -
{doc.title}
-
+

{doc.title}

{doc.description && ( diff --git a/apps/design-system/components/pager.tsx b/apps/design-system/components/pager.tsx index b5c6eb39a0dd7..b7b1620b8ea75 100644 --- a/apps/design-system/components/pager.tsx +++ b/apps/design-system/components/pager.tsx @@ -71,3 +71,27 @@ export function flatten(links: NavItemWithChildren[]): NavItem[] { }, []) .filter((link) => !link?.disabled) } + +export interface BreadcrumbSegment { + title: string + href?: string +} + +export function getBreadcrumbSegments(doc: Doc): BreadcrumbSegment[] { + const segments: BreadcrumbSegment[] = [{ title: 'Docs', href: '/docs' }] + + for (const section of docsConfig.sidebarNav) { + const flatItems = flatten(section.items ?? []) + const inSection = flatItems.some((item) => item.href === doc.slug) + if (!inSection || !section.title) continue + + const sectionLanding = flatItems.find((item) => item.priority) ?? flatItems[0] + const isOnLanding = sectionLanding?.href === doc.slug + if (sectionLanding?.href && !isOnLanding) { + segments.push({ title: section.title, href: sectionLanding.href }) + } + break + } + + return segments +} diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index daf0e8bbe13e4..594b16b7d37b6 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -1,11 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Check } from 'lucide-react' -import { useTheme } from 'next-themes' -import { useQueryState } from 'nuqs' -import { useEffect, useState } from 'react' -import ReactMarkdown from 'react-markdown' -import { toast } from 'sonner' - import { useParams } from 'common' import { Markdown } from 'components/interfaces/Markdown' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -13,17 +6,23 @@ import { DocsButton } from 'components/ui/DocsButton' import { ResourceItem } from 'components/ui/Resource/ResourceItem' import type { components } from 'data/api' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' +import { Check } from 'lucide-react' +import { useTheme } from 'next-themes' +import { useQueryState } from 'nuqs' +import { useEffect, useState } from 'react' +import ReactMarkdown from 'react-markdown' +import { toast } from 'sonner' import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui' import { Admonition } from 'ui-patterns' + import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' import { AuthAlert } from './AuthAlert' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' interface ProviderFormProps { config: components['schemas']['GoTrueConfigResponse'] @@ -42,6 +41,8 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) const [open, setOpen] = useState(false) const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation() + const { data: endpoint } = useProjectApiUrl({ projectRef }) + const { can: canUpdateConfig } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'custom_config_gotrue' @@ -67,12 +68,6 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) } const isFreePlan = organization?.plan.id === 'free' - const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - const apiUrl = `${protocol}://${endpoint}` - - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) const INITIAL_VALUES = (() => { const initialValues: { [x: string]: string | boolean } = {} @@ -247,11 +242,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) readOnly disabled label="Callback URL (for OAuth)" - value={ - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback` - : `${apiUrl}/auth/v1/callback` - } + value={`${endpoint}/auth/v1/callback`} descriptionText={ { @@ -201,8 +201,8 @@ export const CreateOrUpdateOAuthAppSheet = ({ updateOAuthApp({ projectRef, + clientEndpoint, clientId: appToEdit.client_id, - clientEndpoint: endpointData?.endpoint, ...payload, }) } else { @@ -216,7 +216,7 @@ export const CreateOrUpdateOAuthAppSheet = ({ createOAuthApp({ projectRef, - clientEndpoint: endpointData?.endpoint, + clientEndpoint, ...payload, }) } @@ -234,8 +234,8 @@ export const CreateOrUpdateOAuthAppSheet = ({ const handleConfirmRegenerate = () => { regenerateSecret({ projectRef, + clientEndpoint, clientId: appToEdit?.client_id, - clientEndpoint: endpointData?.endpoint, }) } diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx index df996225bd968..76a8818cb90ec 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx @@ -1,6 +1,6 @@ import type { OAuthClient } from '@supabase/supabase-js' import { useParams } from 'common' -import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' +import { useProjectApiUrl } from 'data/config/project-endpoint-query' import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -21,12 +21,13 @@ export const DeleteOAuthAppModal = ({ }: DeleteOAuthAppModalProps) => { const { ref: projectRef } = useParams() - const { data: endpointData } = useProjectEndpointQuery({ projectRef }) + const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef }) + const onConfirmDeleteApp = () => { onDelete({ projectRef, + clientEndpoint, clientId: selectedApp?.client_id, - clientEndpoint: endpointData?.endpoint, }) } diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index 817103165eb03..902b259d075af 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -4,7 +4,7 @@ import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' +import { useProjectApiUrl } from 'data/config/project-endpoint-query' import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation' import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query' @@ -75,7 +75,7 @@ export const OAuthAppsList = () => { const [filteredClientTypes, setFilteredClientTypes] = useState([]) const [filterString, setFilterString] = useState('') - const { data: endpointData } = useProjectEndpointQuery({ projectRef }) + const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef }) const { data, error, @@ -457,8 +457,8 @@ export const OAuthAppsList = () => { onConfirm={() => { regenerateSecret({ projectRef, + clientEndpoint, clientId: selectedApp?.client_id, - clientEndpoint: endpointData?.endpoint, }) setShowRegenerateDialog(false) }} diff --git a/apps/studio/components/interfaces/Connect/ApiKeysTabContent.tsx b/apps/studio/components/interfaces/Connect/ApiKeysTabContent.tsx index e1a486efbb84d..78232db1ca6ac 100644 --- a/apps/studio/components/interfaces/Connect/ApiKeysTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/ApiKeysTabContent.tsx @@ -1,12 +1,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' +import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { AlertCircle, ExternalLink, Loader2 } from 'lucide-react' import Link from 'next/link' import type { ReactNode } from 'react' - -import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' + import type { projectKeys } from './Connect.types' function KeyRow({ label, value }: { label: ReactNode; value: string }) { @@ -63,7 +63,6 @@ export function ApiKeysTabContent({ projectKeys }: { projectKeys: projectKeys }) value={projectKeys.anonKey ?? ''} /> - {/* Footer */}

For secret keys, see API settings.

diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index e2e91db087405..9ccd9c8d47d48 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -5,7 +5,6 @@ import { DatabaseConnectionString } from 'components/interfaces/Connect/Database import { McpTabContent } from 'components/interfaces/Connect/McpTabContent' import Panel from 'components/ui/Panel' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from 'lib/constants' @@ -35,6 +34,7 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C import { getContentFilePath, inferConnectTabFromParentKey } from './Connect.utils' import { ConnectDropdown } from './ConnectDropdown' import { ConnectTabContent } from './ConnectTabContent' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' export const Connect = () => { const router = useRouter() @@ -97,7 +97,8 @@ export const Connect = () => { ?.children.find((child) => child.key === selectedChild)?.children[0]?.key || '' ) - const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: open }) + const { data: resolvedEndpoint } = useProjectApiUrl({ projectRef }) + const { can: canReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.READ, 'service_api_keys' @@ -216,22 +217,12 @@ export const Connect = () => { : { anonKey: null, publishableKey: null } const projectKeys = useMemo(() => { - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : '' - return { - apiUrl: apiHost ?? null, + apiUrl: resolvedEndpoint ?? null, anonKey: anonKey?.api_key ?? null, publishableKey: publishableKey?.api_key ?? null, } - }, [ - settings?.app_config?.protocol, - settings?.app_config?.endpoint, - canReadAPIKeys, - anonKey?.api_key, - publishableKey?.api_key, - ]) + }, [resolvedEndpoint, anonKey?.api_key, publishableKey?.api_key]) const filePath = getContentFilePath({ connectionObject, diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx index 60d3d1f5e941a..02f605fc838ad 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx @@ -1,7 +1,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { useMemo } from 'react' @@ -11,9 +10,12 @@ import type { ProjectKeys } from './Connect.types' import { ConnectConfigSection, ModeSelector } from './ConnectConfigSection' import { ConnectStepsSection } from './ConnectStepsSection' import { useConnectState } from './useConnectState' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' export const ConnectSheet = () => { + const { ref: projectRef } = useParams() + const { projectConnectionShowAppFrameworks: showAppFrameworks, projectConnectionShowMobileFrameworks: showMobileFrameworks, @@ -40,9 +42,8 @@ export const ConnectSheet = () => { const { state, activeFields, resolvedSteps, schema, getFieldOptions, setMode, updateField } = useConnectState() - // Project keys for step components - const { ref: projectRef } = useParams() - const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: showConnect }) + const { data: endpoint = '' } = useProjectApiUrl({ projectRef }, { enabled: showConnect }) + const { can: canReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.READ, 'service_api_keys' @@ -53,22 +54,12 @@ export const ConnectSheet = () => { : { anonKey: null, publishableKey: null } const projectKeys: ProjectKeys = useMemo(() => { - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : '' - return { - apiUrl: apiHost ?? null, + apiUrl: endpoint, anonKey: anonKey?.api_key ?? null, publishableKey: publishableKey?.api_key ?? null, } - }, [ - settings?.app_config?.protocol, - settings?.app_config?.endpoint, - canReadAPIKeys, - anonKey?.api_key, - publishableKey?.api_key, - ]) + }, [endpoint, anonKey?.api_key, publishableKey?.api_key]) const availableModeIds = useMemo(() => { const modes: string[] = [] diff --git a/apps/studio/components/interfaces/Docs/ResourceContent.tsx b/apps/studio/components/interfaces/Docs/ResourceContent.tsx index f5f58076ab591..fd4093974e0cb 100644 --- a/apps/studio/components/interfaces/Docs/ResourceContent.tsx +++ b/apps/studio/components/interfaces/Docs/ResourceContent.tsx @@ -7,13 +7,12 @@ import Description from '@/components/interfaces/Docs/Description' import Param from '@/components/interfaces/Docs/Param' import Snippets from '@/components/interfaces/Docs/Snippets' import { InlineLink } from '@/components/ui/InlineLink' -import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { DOCS_URL } from '@/lib/constants' interface ResourceContentProps { - apiEndpoint: string resourceId: string resources: { [key: string]: { id: string; displayName: string; camelCase: string } } selectedLang: 'bash' | 'js' @@ -22,7 +21,6 @@ interface ResourceContentProps { } export const ResourceContent = ({ - apiEndpoint, resourceId, resources, selectedLang, @@ -30,16 +28,12 @@ export const ResourceContent = ({ refreshDocs, }: ResourceContentProps) => { const { ref } = useParams() - const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all']) const { data: jsonSchema } = useProjectJsonSchemaQuery({ projectRef: ref }) const { paths, definitions } = jsonSchema || {} - const endpoint = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain.hostname}` - : apiEndpoint + const { data: endpoint = '' } = useProjectApiUrl({ projectRef: ref }) const keyToShow = !!showApiKey ? showApiKey : 'SUPABASE_KEY' const resourcePaths = paths?.[`/${resourceId}`] diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 40f8fd83fa068..3da5c1a0adaed 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -9,29 +9,30 @@ import { useEffect, useMemo, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { + Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, CardFooter, + cn, CodeBlock, + copyToClipboard, CriticalIcon, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Form_Shadcn_, Switch, Tabs_Shadcn_ as Tabs, TabsContent_Shadcn_ as TabsContent, TabsList_Shadcn_ as TabsList, TabsTrigger_Shadcn_ as TabsTrigger, - cn, - copyToClipboard, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, @@ -40,7 +41,6 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' -import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import CommandRender from '../CommandRender' @@ -48,8 +48,7 @@ import { INVOCATION_TABS } from './EdgeFunctionDetails.constants' import { generateCLICommands } from './EdgeFunctionDetails.utils' import AlertError from '@/components/ui/AlertError' import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query' import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation' import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation' @@ -86,24 +85,18 @@ export const EdgeFunctionDetails = () => { const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') - const { data: apiKeys } = useAPIKeysQuery( - { - projectRef, - }, - { enabled: canReadAPIKeys } - ) - const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) + const { data: selectedFunction, error, isPending: isLoading, isError, isSuccess, - } = useEdgeFunctionQuery({ - projectRef, - slug: functionSlug, - }) + } = useEdgeFunctionQuery({ projectRef, slug: functionSlug }) + + const { data: endpoint } = useProjectApiUrl({ projectRef }) + const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}` const { mutate: updateEdgeFunction, isPending: isUpdating } = useEdgeFunctionUpdateMutation() const { mutate: deleteEdgeFunction, isPending: isDeleting } = useEdgeFunctionDeleteMutation({ @@ -121,12 +114,6 @@ export const EdgeFunctionDetails = () => { const { anonKey, publishableKey } = getKeys(apiKeys) const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]' - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const functionUrl = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain.hostname}/functions/v1/${selectedFunction?.slug}` - : `${protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}` const hasImportMap = useMemo( () => selectedFunction?.import_map || selectedFunction?.import_map_path, [selectedFunction] diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index 359948879a717..910d4f419488b 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -4,11 +4,10 @@ import dayjs from 'dayjs' import { Check, Copy } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' -import { TableCell, TableRow, copyToClipboard } from 'ui' +import { copyToClipboard, TableCell, TableRow } from 'ui' import { TimestampInfo } from 'ui-patterns' -import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import type { EdgeFunctionsResponse } from '@/data/edge-functions/edge-functions-query' import { createNavigationHandler } from '@/lib/navigation' @@ -21,15 +20,8 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP const { ref } = useParams() const [isCopied, setIsCopied] = useState(false) - const { data: settings } = useProjectSettingsV2Query({ projectRef: ref }) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref }) - - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const functionUrl = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain.hostname}/functions/v1/${item.slug}` - : `${protocol}://${endpoint}/functions/v1/${item.slug}` + const { data: endpoint } = useProjectApiUrl({ projectRef: ref }) + const functionUrl = `${endpoint}/functions/v1/${item.slug}` const handleNavigation = createNavigationHandler( `/project/${ref}/functions/${item.slug}${IS_PLATFORM ? '' : `/details`}`, diff --git a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx index 79e77e112b091..f4d82bb4c029b 100644 --- a/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/apps/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -1,24 +1,23 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react' -import { useRouter } from 'next/router' -import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'react' - import { useParams } from 'common' import CommandRender from 'components/interfaces/Functions/CommandRender' import { DocsButton } from 'components/ui/DocsButton' import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { DOCS_URL } from 'lib/constants' +import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react' +import { useRouter } from 'next/router' +import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'react' import { Button, + Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, - Collapsible_Shadcn_, } from 'ui' + import type { Commands } from './Functions.types' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' interface TerminalInstructionsProps extends ComponentPropsWithoutRef { closable?: boolean @@ -36,19 +35,13 @@ export const TerminalInstructions = forwardRef< const { data: tokens } = useAccessTokensQuery() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys }) - const { data: settings } = useProjectSettingsV2Query({ projectRef }) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + + const { data: endpoint } = useProjectApiUrl({ projectRef }) + const functionsEndpoint = `${endpoint}/functions/v1` const { anonKey, publishableKey } = getKeys(apiKeys) const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]' - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const functionsEndpoint = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain.hostname}/functions/v1` - : `${protocol}://${endpoint}/functions/v1` - // get the .co or .net TLD from the restUrl const restUrl = `https://${endpoint}` const restUrlTld = !!endpoint ? new URL(restUrl).hostname.split('.').pop() : 'co' diff --git a/apps/studio/components/interfaces/HomeNew/ProjectConnectionHoverCard.tsx b/apps/studio/components/interfaces/HomeNew/ProjectConnectionHoverCard.tsx index 91aa79824e5c3..82c8e4bdf8f38 100644 --- a/apps/studio/components/interfaces/HomeNew/ProjectConnectionHoverCard.tsx +++ b/apps/studio/components/interfaces/HomeNew/ProjectConnectionHoverCard.tsx @@ -2,7 +2,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Label } from '@ui/components/shadcn/ui/label' import { getConnectionStrings } from 'components/interfaces/Connect/DatabaseSettings.utils' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { pluckObjectFields } from 'lib/helpers' @@ -10,8 +9,11 @@ import { Plug } from 'lucide-react' import { parseAsBoolean, useQueryState } from 'nuqs' import { useMemo, useState, type ReactNode } from 'react' import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui' +import { ShimmeringLoader } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' + const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user'] as const const EMPTY_CONNECTION_INFO = { db_user: '', @@ -37,32 +39,23 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove const [open, setOpen] = useState(false) const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) - const { data: settings, isPending: isLoadingSettings } = useProjectSettingsV2Query( - { projectRef }, - { enabled: !!projectRef } - ) - - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - const projectUrl = endpoint ? `${protocol}://${endpoint}` : undefined - const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.READ, 'service_api_keys' ) + const { data: projectUrl, isPending: isLoadingApiUrl } = useProjectApiUrl({ projectRef }) + const { data: apiKeys, isLoading: isLoadingKeys } = useAPIKeysQuery( { projectRef }, { enabled: open && canReadAPIKeys } ) - const { publishableKey } = canReadAPIKeys ? getKeys(apiKeys) : { publishableKey: null } const { data: databases, isLoading: isLoadingDatabases } = useReadReplicasQuery( { projectRef }, { enabled: open && !!projectRef } ) - const primaryDatabase = databases?.find((db) => db.identifier === projectRef) const directConnectionString = useMemo(() => { @@ -81,9 +74,6 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove }).direct.uri }, [primaryDatabase, projectRef]) - const projectUrlLabel = - projectUrl ?? (isLoadingSettings ? 'Loading project URL...' : 'Project URL unavailable') - return ( @@ -91,9 +81,13 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove
- - {projectUrlLabel} - + {isLoadingApiUrl ? ( + + ) : ( + + {projectUrl ?? 'Project URL unavailable'} + + )}
diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts index 838bc0f72330c..f39388b27dc90 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts @@ -1,28 +1,10 @@ import { describe, expect, it } from 'vitest' -import { buildEntityMaps, getApiEndpoint, getProjectApiEndpoint } from './DataApi.utils' -import type { ProjectSettings } from '@/data/config/project-settings-v2-query' -import type { - CustomDomainResponse, - CustomDomainsData, -} from '@/data/custom-domains/custom-domains-query' +import { buildEntityMaps, getApiEndpoint } from './DataApi.utils' import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query' import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' import type { Database } from '@/data/read-replicas/replicas-query' -const makeCustomDomainData = (hostname: string): CustomDomainsData => ({ - customDomain: { - id: '', - ssl: {} as CustomDomainResponse['ssl'], - hostname, - status: 'active', - created_at: '', - custom_metadata: null, - custom_origin_server: '', - }, - status: '5_services_reconfigured', -}) - const makeDatabase = ( identifier: string, restUrl: string @@ -30,71 +12,13 @@ const makeDatabase = ( const makeLoadBalancer = (endpoint: string): Pick => ({ endpoint }) -describe('getProjectApiEndpoint', () => { - it('returns custom domain URL when custom domain is active', () => { - expect( - getProjectApiEndpoint({ - settings: undefined, - customDomainData: makeCustomDomainData('api.example.com'), - }) - ).toBe('https://api.example.com') - }) - - it('returns settings-based URL when no custom domain', () => { - expect( - getProjectApiEndpoint({ - settings: { - app_config: { protocol: 'https', endpoint: 'abc.supabase.co' }, - } as ProjectSettings, - customDomainData: undefined, - }) - ).toBe('https://abc.supabase.co') - }) - - it('respects protocol from settings', () => { - expect( - getProjectApiEndpoint({ - settings: { - app_config: { protocol: 'http', endpoint: 'localhost:54321' }, - } as ProjectSettings, - customDomainData: undefined, - }) - ).toBe('http://localhost:54321') - }) - - it('returns placeholder when settings are undefined', () => { - expect( - getProjectApiEndpoint({ - settings: undefined, - customDomainData: undefined, - }) - ).toBe('https://-') - }) - - it('ignores inactive custom domain', () => { - const inactiveCustomDomain: CustomDomainsData = { - customDomain: null, - status: '0_no_hostname_configured', - } - - expect( - getProjectApiEndpoint({ - settings: { - app_config: { protocol: 'https', endpoint: 'abc.supabase.co' }, - } as ProjectSettings, - customDomainData: inactiveCustomDomain, - }) - ).toBe('https://abc.supabase.co') - }) -}) - describe('getApiEndpoint', () => { it('returns custom domain URL when custom domain is active and primary database is selected', () => { expect( getApiEndpoint({ selectedDatabaseId: 'project-ref', projectRef: 'project-ref', - customDomainData: makeCustomDomainData('api.example.com'), + resolvedEndpoint: 'https://api.example.com', loadBalancers: undefined, selectedDatabase: makeDatabase( 'project-ref', @@ -109,7 +33,7 @@ describe('getApiEndpoint', () => { getApiEndpoint({ selectedDatabaseId: 'replica-1', projectRef: 'project-ref', - customDomainData: makeCustomDomainData('api.example.com'), + resolvedEndpoint: 'https://api.example.com', loadBalancers: undefined, selectedDatabase: makeDatabase( 'replica-1', @@ -124,7 +48,7 @@ describe('getApiEndpoint', () => { getApiEndpoint({ selectedDatabaseId: 'load-balancer', projectRef: 'project-ref', - customDomainData: undefined, + resolvedEndpoint: 'https://project-ref.supabase.co', loadBalancers: [makeLoadBalancer('https://lb.supabase.co') as LoadBalancer], selectedDatabase: undefined, }) @@ -136,46 +60,26 @@ describe('getApiEndpoint', () => { getApiEndpoint({ selectedDatabaseId: 'load-balancer', projectRef: 'project-ref', - customDomainData: undefined, + resolvedEndpoint: 'https://project-ref.supabase.co', loadBalancers: undefined, selectedDatabase: undefined, }) ).toBe('') }) - it('returns database restUrl for a normal database selection', () => { + it('returns database restUrl for a replica database selection', () => { expect( getApiEndpoint({ - selectedDatabaseId: 'project-ref', + selectedDatabaseId: 'replica-2', projectRef: 'project-ref', - customDomainData: undefined, + resolvedEndpoint: 'https://project-ref.supabase.co', loadBalancers: undefined, selectedDatabase: makeDatabase( - 'project-ref', - 'https://project-ref.supabase.co/rest/v1' - ) as Database, - }) - ).toBe('https://project-ref.supabase.co/rest/v1') - }) - - it('ignores custom domain when it is not active', () => { - const inactiveCustomDomain: CustomDomainsData = { - customDomain: null, - status: '0_no_hostname_configured', - } - - expect( - getApiEndpoint({ - selectedDatabaseId: 'project-ref', - projectRef: 'project-ref', - customDomainData: inactiveCustomDomain, - loadBalancers: undefined, - selectedDatabase: makeDatabase( - 'project-ref', - 'https://project-ref.supabase.co/rest/v1' + 'replica-2', + 'https://replica-2.supabase.co/rest/v1' ) as Database, }) - ).toBe('https://project-ref.supabase.co/rest/v1') + ).toBe('https://replica-2.supabase.co/rest/v1') }) }) diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts index 61990329ea510..da0fd5d6be101 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts @@ -1,29 +1,8 @@ -import type { ProjectSettings } from '@/data/config/project-settings-v2-query' -import type { CustomDomainsData } from '@/data/custom-domains/custom-domains-query' import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query' import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query' import type { Database } from '@/data/read-replicas/replicas-query' import { snakeToCamel } from '@/lib/helpers' -/** - * Resolves the primary project API endpoint, respecting custom domains. - */ -export function getProjectApiEndpoint({ - settings, - customDomainData, -}: { - settings: ProjectSettings | undefined - customDomainData: CustomDomainsData | undefined -}): string { - if (customDomainData?.customDomain?.status === 'active') { - return `https://${customDomainData.customDomain.hostname}` - } - - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - return `${protocol}://${endpoint ?? '-'}` -} - /** * Resolves the API endpoint URL based on the selected database, custom domain * status, and load balancer configuration. @@ -31,21 +10,20 @@ export function getProjectApiEndpoint({ export function getApiEndpoint({ selectedDatabaseId, projectRef, - customDomainData, + resolvedEndpoint, loadBalancers, selectedDatabase, }: { selectedDatabaseId: string | undefined projectRef: string | undefined - customDomainData: CustomDomainsData | undefined + resolvedEndpoint: string | undefined loadBalancers: Array | undefined selectedDatabase: Database | undefined }): string { - const isCustomDomainActive = customDomainData?.customDomain?.status === 'active' const loadBalancerSelected = selectedDatabaseId === 'load-balancer' - if (isCustomDomainActive && selectedDatabaseId === projectRef) { - return `https://${customDomainData.customDomain.hostname}` + if (selectedDatabaseId === projectRef && !!resolvedEndpoint) { + return resolvedEndpoint } if (loadBalancerSelected) { diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx b/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx index 7316245f00e49..8950db2a349ce 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx +++ b/apps/studio/components/interfaces/Integrations/DataApi/DocView.tsx @@ -4,14 +4,10 @@ import type { ShowApiKey } from '../../Docs/Docs.types' import { GeneralContent } from '@/components/interfaces/Docs/GeneralContent' import { ResourceContent } from '@/components/interfaces/Docs/ResourceContent' import { RpcContent } from '@/components/interfaces/Docs/RpcContent' -import { - buildEntityMaps, - getProjectApiEndpoint, -} from '@/components/interfaces/Integrations/DataApi/DataApi.utils' +import { buildEntityMaps } from '@/components/interfaces/Integrations/DataApi/DataApi.utils' import { DocViewError } from '@/components/interfaces/Integrations/DataApi/DocViewError' import { DocViewLoading } from '@/components/interfaces/Integrations/DataApi/DocViewLoading' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query' interface DocViewProps { @@ -29,9 +25,6 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => { isPending: isLoading, refetch, } = useProjectJsonSchemaQuery({ projectRef }) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) - - const endpoint = getProjectApiEndpoint({ settings, customDomainData }) const { paths } = jsonSchema || {} const PAGE_KEY = resource || rpc || page || 'index' @@ -51,7 +44,6 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
{resource ? ( { if (isLogsNotAvailableBasedOnPlan) { return ( - + { return ( <> - +
{showFilters && ( diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx index 54194192c4446..dd76e2e0d4deb 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx @@ -1,10 +1,12 @@ import { NoProjectsOnPaidOrgInfo } from 'components/interfaces/Billing/NoProjectsOnPaidOrgInfo' -import { - ScaffoldContainer, - ScaffoldSection, - ScaffoldSectionTitle, -} from 'components/layouts/Scaffold' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { + PageSection, + PageSectionContent, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} from 'ui-patterns/PageSection' import { OrganizationDeletePanel } from './OrganizationDeletePanel' import { DataPrivacyForm } from './DataPrivacyForm' @@ -14,20 +16,43 @@ export const GeneralSettings = () => { const organizationDeletionEnabled = useIsFeatureEnabled('organizations:delete') return ( - + <> - - Organization Details - - + + + + Organization details + + + + + + - - Data Privacy - - + + + + Data privacy + + + + + + - {organizationDeletionEnabled && } - + {organizationDeletionEnabled && ( + + + + Danger zone + + + + + + + )} + ) } diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx index d484b76993bea..a9ddf0d076b2f 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OrganizationDeletePanel.tsx @@ -1,4 +1,3 @@ -import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold' import PartnerManagedResource from 'components/ui/PartnerManagedResource' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { MANAGED_BY } from 'lib/constants/infrastructure' @@ -8,28 +7,23 @@ import { DeleteOrganizationButton } from './DeleteOrganizationButton' export const OrganizationDeletePanel = () => { const { data: selectedOrganization } = useSelectedOrganizationQuery() - return ( - - Danger Zone - {selectedOrganization?.managed_by !== 'vercel-marketplace' ? ( - - - - ) : ( - - )} - + return selectedOrganization?.managed_by !== 'vercel-marketplace' ? ( + + + + ) : ( + ) } diff --git a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx index 17312e0b462d5..c340127cf3c99 100644 --- a/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx +++ b/apps/studio/components/interfaces/Organization/OAuthApps/OAuthApps.tsx @@ -68,7 +68,7 @@ export const OAuthApps = () => { return ( <> - +
diff --git a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx index 753af528fcc2d..f6a454b40ed3b 100644 --- a/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx +++ b/apps/studio/components/interfaces/Organization/SSO/SSOConfig.tsx @@ -153,7 +153,7 @@ export const SSOConfig = () => { }, [ssoConfig, form]) return ( - + {isLoadingEntitlement || (hasAccessToSso && isLoadingSSOConfig) ? ( diff --git a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx index 50162239359a9..51af92d27de89 100644 --- a/apps/studio/components/interfaces/Organization/SecuritySettings.tsx +++ b/apps/studio/components/interfaces/Organization/SecuritySettings.tsx @@ -105,7 +105,7 @@ export const SecuritySettings = () => { } return ( - + {!isPaidPlan ? ( { { projectRef: ref }, { enabled: snap.showProjectApiDocs && canReadAPIKeys } ) - const { data: settings } = useProjectSettingsV2Query( - { projectRef: ref }, - { enabled: snap.showProjectApiDocs } - ) - const { data: customDomainData } = useCustomDomainsQuery( + + const { data: endpoint } = useProjectApiUrl( { projectRef: ref }, { enabled: snap.showProjectApiDocs } ) @@ -63,12 +59,6 @@ export const ProjectAPIDocs = () => { const apikey = showKeys ? anonKey?.api_key ?? 'SUPABASE_CLIENT_ANON_KEY' : 'SUPABASE_CLIENT_ANON_KEY' - const protocol = settings?.app_config?.protocol ?? 'https' - const hostEndpoint = settings?.app_config?.endpoint - const endpoint = - customDomainData?.customDomain?.status === 'active' - ? `https://${customDomainData.customDomain?.hostname}` - : `${protocol}://${hostEndpoint ?? ''}` return ( { const { isPending: isLoading } = useSelectedProjectQuery() @@ -31,7 +31,7 @@ export const DataApiProjectUrlCard = () => { const [querySource, setQuerySource] = useQueryState('source', parseAsString) - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + const { data: resolvedEndpoint } = useProjectApiUrl({ projectRef }) const { data: databases, isError, @@ -55,7 +55,7 @@ export const DataApiProjectUrlCard = () => { const endpoint = getApiEndpoint({ selectedDatabaseId: state.selectedDatabaseId, projectRef, - customDomainData, + resolvedEndpoint, loadBalancers, selectedDatabase, }) diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 68dca08580a08..66bcbd2b07af8 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -418,7 +418,9 @@ export const Addons = () => { !isProjectActive || projectUpdateDisabled || !(canUpdateIPv4 || ipv4) } > - Change dedicated IPv4 address + {!!ipv4 + ? 'Toggle dedicated IPv4 address' + : 'Enable dedicated IPv4 address'}
@@ -528,7 +530,7 @@ export const Addons = () => { }, }} > - Change point in time recovery + Enable point in time recovery ) : ( { hasHipaaAddon } > - Change point in time recovery + {!!pitr ? 'Change recovery duration' : 'Enable point in time recovery'} )} @@ -608,19 +610,28 @@ export const Addons = () => { ? 'Custom domain is enabled' : 'Custom domain is not enabled'}

- - - + + + {!!customDomain && ( + + )} +
diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx index 7ae4948710253..0b35de9131628 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainActivate.tsx @@ -1,6 +1,3 @@ -import { useState } from 'react' -import { toast } from 'sonner' - import { DocsButton } from 'components/ui/DocsButton' import Panel from 'components/ui/Panel' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' @@ -9,16 +6,18 @@ import { useCustomDomainActivateMutation } from 'data/custom-domains/custom-doma import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' import { DOCS_URL } from 'lib/constants' +import { useState } from 'react' +import { toast } from 'sonner' import { Button } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { Admonition } from 'ui-patterns/admonition' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' export type CustomDomainActivateProps = { projectRef?: string customDomain: CustomDomainResponse } -const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => { +export const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => { const [isActivateConfirmModalVisible, setIsActivateConfirmModalVisible] = useState(false) const { data: settings } = useProjectSettingsV2Query({ projectRef }) @@ -134,5 +133,3 @@ const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivate ) } - -export default CustomDomainActivate diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index bf3c82fc730b4..5edd3a16d38ed 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -1,5 +1,3 @@ -import { AlertCircle } from 'lucide-react' - import { SupportCategories } from '@supabase/shared-types/out/constants' import { useFlag, useParams } from 'common' import { SupportLink } from 'components/interfaces/Support/SupportLink' @@ -11,6 +9,7 @@ import { } from 'data/custom-domains/custom-domains-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { AlertCircle } from 'lucide-react' import { Card, CardContent } from 'ui' import { PageSection, @@ -20,11 +19,12 @@ import { PageSectionSummary, PageSectionTitle, } from 'ui-patterns/PageSection' -import CustomDomainActivate from './CustomDomainActivate' -import CustomDomainDelete from './CustomDomainDelete' -import CustomDomainVerify from './CustomDomainVerify' -import CustomDomainsConfigureHostname from './CustomDomainsConfigureHostname' -import CustomDomainsShimmerLoader from './CustomDomainsShimmerLoader' + +import { CustomDomainActivate } from './CustomDomainActivate' +import { CustomDomainDelete } from './CustomDomainDelete' +import { CustomDomainsConfigureHostname } from './CustomDomainsConfigureHostname' +import { CustomDomainsShimmerLoader } from './CustomDomainsShimmerLoader' +import { CustomDomainVerify } from './CustomDomainVerify' export const CustomDomainConfig = () => { const { ref } = useParams() diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx index c0e128e9fd44c..da82b6dc8284c 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainDelete.tsx @@ -1,12 +1,11 @@ -import { useState } from 'react' -import { toast } from 'sonner' - import { DocsButton } from 'components/ui/DocsButton' import Panel from 'components/ui/Panel' import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation' import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query' import { DOCS_URL } from 'lib/constants' import { Trash } from 'lucide-react' +import { useState } from 'react' +import { toast } from 'sonner' import { Button } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -15,7 +14,7 @@ export type CustomDomainDeleteProps = { customDomain: CustomDomainResponse } -const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProps) => { +export const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProps) => { const [isDeleteConfirmModalVisible, setIsDeleteConfirmModalVisible] = useState(false) const { mutate: deleteCustomDomain, isPending: isDeletingCustomDomain } = useCustomDomainDeleteMutation({ @@ -85,5 +84,3 @@ const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProp ) } - -export default CustomDomainDelete diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx index 943ac726035f2..0303780c2752a 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx @@ -1,6 +1,3 @@ -import { AlertCircle, RefreshCw } from 'lucide-react' -import { toast } from 'sonner' - import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import { InlineLink } from 'components/ui/InlineLink' @@ -10,19 +7,22 @@ import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domain import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useCustomDomainReverifyQuery } from 'data/custom-domains/custom-domains-reverify-query' import { DOCS_URL } from 'lib/constants' +import { AlertCircle, RefreshCw } from 'lucide-react' import { useEffect } from 'react' +import { toast } from 'sonner' import { + Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, - Alert_Shadcn_, Button, WarningIcon, } from 'ui' import { Admonition } from 'ui-patterns/admonition' + import DNSRecord from './DNSRecord' import { DNSTableHeaders } from './DNSTableHeaders' -const CustomDomainVerify = () => { +export const CustomDomainVerify = () => { const { ref: projectRef } = useParams() const { data: settings } = useProjectSettingsV2Query({ projectRef }) @@ -208,5 +208,3 @@ const CustomDomainVerify = () => { ) } - -export default CustomDomainVerify diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx index cd0ad8a982d90..5555efc79f6c4 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsConfigureHostname.tsx @@ -1,8 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useForm } from 'react-hook-form' -import { z } from 'zod' - import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' @@ -11,6 +8,7 @@ import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domain import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { useForm } from 'react-hook-form' import { Button, Card, @@ -18,19 +16,19 @@ import { CardFooter, CardHeader, CardTitle, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - FormMessage_Shadcn_, - Form_Shadcn_, Input_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' const schema = z.object({ domain: z.string().trim().min(1, 'A value for your custom domain is required'), }) -const CustomDomainsConfigureHostname = () => { +export const CustomDomainsConfigureHostname = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() @@ -102,7 +100,6 @@ const CustomDomainsConfigureHostname = () => { autoComplete="off" /> - )} /> @@ -154,5 +151,3 @@ const CustomDomainsConfigureHostname = () => { ) } - -export default CustomDomainsConfigureHostname diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx index c4f5af2aeb3dd..c942e8e48f9c3 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx @@ -1,4 +1,4 @@ -const CustomDomainsShimmerLoader = () => { +export const CustomDomainsShimmerLoader = () => { return (
@@ -11,5 +11,3 @@ const CustomDomainsShimmerLoader = () => {
) } - -export default CustomDomainsShimmerLoader diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index d5686415597bc..ef2229ea81ac9 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -373,6 +373,14 @@ const OrganizationLinks = () => { const showBilling = useIsFeatureEnabled('billing:all') const activeRoute = router.pathname.split('/')[3] + const organizationSettingsRoutes = new Set([ + 'general', + 'security', + 'sso', + 'apps', + 'audit', + 'documents', + ]) const navMenuItems = [ { @@ -410,7 +418,7 @@ const OrganizationLinks = () => { ] : []), { - label: 'Organization settings', + label: 'Organization Settings', href: `/org/${organizationSlug}/general`, key: 'settings', icon: , @@ -429,11 +437,7 @@ const OrganizationLinks = () => { i === 0 ? activeRoute === undefined : item.key === 'settings' - ? router.pathname.includes('/general') || - router.pathname.includes('/apps') || - router.pathname.includes('/audit') || - router.pathname.includes('/documents') || - router.pathname.includes('/security') + ? organizationSettingsRoutes.has(activeRoute ?? '') : activeRoute === item.key } route={{ diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/useCopyUrl.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/useCopyUrl.tsx index ed94cfbddcc7d..c6c70036a0160 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/useCopyUrl.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/useCopyUrl.tsx @@ -1,22 +1,18 @@ import { useCallback } from 'react' import { toast } from 'sonner' - -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { copyToClipboard } from 'ui' + import { URL_EXPIRY_DURATION } from '../Storage.constants' import { getPathAlongOpenedFolders } from './StorageExplorer.utils' import { fetchFileUrl } from './useFetchFileUrlQuery' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' export const useCopyUrl = () => { const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot() - const { data: customDomainData } = useCustomDomainsQuery({ projectRef: projectRef }) - const { data: settings } = useProjectSettingsV2Query({ projectRef: projectRef }) - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - const apiUrl = `${protocol}://${endpoint ?? '-'}` + const { hostEndpoint, customEndpoint } = useProjectApiUrl({ projectRef }) + const isCustomDomainActive = !!customEndpoint const getFileUrl = useCallback( (fileName: string, expiresIn?: URL_EXPIRY_DURATION) => { @@ -37,8 +33,8 @@ export const useCopyUrl = () => { const onCopyUrl = useCallback( (name: string, expiresIn?: URL_EXPIRY_DURATION) => { const formattedUrl = getFileUrl(name, expiresIn).then((url) => { - return customDomainData?.customDomain?.status === 'active' - ? url.replace(apiUrl, `https://${customDomainData.customDomain.hostname}`) + return isCustomDomainActive && hostEndpoint + ? url.replace(hostEndpoint, customEndpoint) : url }) @@ -46,12 +42,7 @@ export const useCopyUrl = () => { toast.success(`Copied URL for ${name} to clipboard.`) }) }, - [ - apiUrl, - customDomainData?.customDomain?.hostname, - customDomainData?.customDomain?.status, - getFileUrl, - ] + [customEndpoint, getFileUrl, hostEndpoint, isCustomDomainActive] ) return { onCopyUrl } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx index 5083475f8328d..3866556071227 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx @@ -1,3 +1,8 @@ +import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import { useIsSchemaExposed } from 'hooks/misc/useIsSchemaExposed' +import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Settings } from 'lucide-react' import Link from 'next/link' import { @@ -10,7 +15,19 @@ import { type SetStateAction, } from 'react' import { usePreviousDistinct } from 'react-use' +import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Switch } from 'ui' +import { Admonition } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { + MultiSelector, + MultiSelectorContent, + MultiSelectorItem, + MultiSelectorList, + MultiSelectorTrigger, +} from 'ui-patterns/multi-select' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' import { @@ -25,23 +42,6 @@ import { } from '@/lib/data-api-types' import type { DeepReadonly, Prettify } from '@/lib/type-helpers' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' -import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' -import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query' -import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useIsSchemaExposed } from 'hooks/misc/useIsSchemaExposed' -import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Switch } from 'ui' -import { Admonition } from 'ui-patterns' -import { Input } from 'ui-patterns/DataInputs/Input' -import { InfoTooltip } from 'ui-patterns/info-tooltip' -import { - MultiSelector, - MultiSelectorContent, - MultiSelectorItem, - MultiSelectorList, - MultiSelectorTrigger, -} from 'ui-patterns/multi-select' const ROLE_LABELS: Record = { anon: 'Anonymous (anon)', @@ -390,6 +390,7 @@ export const ApiAccessToggle = ({
+ { const { selectedDatabaseId } = useDatabaseSelectorStateSnapshot() - const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) + const { data: endpoint } = useProjectApiUrl({ projectRef }) const { data: loadBalancers } = useLoadBalancersQuery({ projectRef }) const { data: databases } = useReadReplicasQuery({ projectRef }) const apiEndpoint = useMemo(() => { - const isCustomDomainActive = customDomainData?.customDomain?.status === 'active' - if (isCustomDomainActive && selectedDatabaseId === projectRef) { - return `https://${customDomainData.customDomain.hostname}` + if (selectedDatabaseId === projectRef) { + return endpoint } const loadBalancerSelected = selectedDatabaseId === 'load-balancer' @@ -439,14 +439,8 @@ const SchemaExposureOptions = ({ const selectedDatabase = databases?.find((db) => db.identifier === selectedDatabaseId) return selectedDatabase?.restUrl - }, [ - projectRef, - databases, - selectedDatabaseId, - customDomainData?.customDomain?.status, - customDomainData?.customDomain?.hostname, - loadBalancers, - ]) + }, [selectedDatabaseId, projectRef, databases, endpoint, loadBalancers]) + const apiBaseUrl = useMemo(() => { if (!apiEndpoint) return undefined return apiEndpoint.endsWith('/') ? apiEndpoint.slice(0, -1) : apiEndpoint diff --git a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts new file mode 100644 index 0000000000000..23a4bfdf06c21 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' + +import { + generateOrganizationSettingsSections, + getOrganizationSettingsDocumentTitle, + normalizeOrganizationSettingsPath, +} from './OrganizationSettingsLayout' + +describe('OrganizationSettingsLayout helpers', () => { + it('returns expected organization settings sections and links', () => { + const sections = generateOrganizationSettingsSections({ + slug: 'my-org', + currentPath: '/org/my-org/general', + showSecuritySettings: true, + showSsoSettings: true, + showLegalDocuments: true, + }) + + expect(sections.map((section) => section.heading)).toEqual([ + 'Configuration', + 'Connections', + 'Compliance', + ]) + expect(sections.flatMap((section) => section.links.map((item) => item.label))).toEqual([ + 'General', + 'Security', + 'SSO', + 'OAuth Apps', + 'Audit Logs', + 'Legal Documents', + ]) + expect( + sections.flatMap((section) => section.links).find((item) => item.label === 'General') + ?.isActive + ).toBe(true) + }) + + it('hides feature-flagged items when flags are disabled', () => { + const sections = generateOrganizationSettingsSections({ + slug: 'my-org', + currentPath: '/org/my-org/general', + showSecuritySettings: false, + showSsoSettings: false, + showLegalDocuments: false, + }) + + expect(sections.map((section) => section.heading)).toEqual([ + 'Configuration', + 'Connections', + 'Compliance', + ]) + expect(sections.flatMap((section) => section.links.map((item) => item.label))).toEqual([ + 'General', + 'OAuth Apps', + 'Audit Logs', + ]) + }) + + it('normalizes hash paths for active state checks', () => { + const currentPath = normalizeOrganizationSettingsPath('/org/my-org/security#sso') + const sections = generateOrganizationSettingsSections({ + slug: 'my-org', + currentPath, + showSecuritySettings: true, + showSsoSettings: true, + showLegalDocuments: true, + }) + + expect( + sections.flatMap((section) => section.links).find((item) => item.label === 'Security') + ?.isActive + ).toBe(true) + }) + + it('uses settings as default document title when page title is not provided', () => { + expect(getOrganizationSettingsDocumentTitle(undefined, 'Supabase')).toBe('Settings | Supabase') + }) + + it('uses page title for document title when provided', () => { + expect(getOrganizationSettingsDocumentTitle('General', 'Supabase')).toBe('General | Supabase') + }) +}) diff --git a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx index b9f974dc65446..a7c7aa109306c 100644 --- a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx @@ -1,61 +1,83 @@ -import Link from 'next/link' +import Head from 'next/head' import { PropsWithChildren } from 'react' import { useParams } from 'common' +import type { SidebarSection } from 'components/layouts/AccountLayout/AccountLayout.types' +import { WithSidebar } from 'components/layouts/AccountLayout/WithSidebar' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useCurrentPath } from 'hooks/misc/useCurrentPath' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { NavMenu, NavMenuItem } from 'ui' -import { ScaffoldContainerLegacy, ScaffoldTitle } from '../Scaffold' -function OrganizationSettingsLayout({ children }: PropsWithChildren) { - const { slug } = useParams() - // Get the path without any hash values - const fullCurrentPath = useCurrentPath() - const [currentPath] = fullCurrentPath.split('#') +interface OrganizationSettingsSectionsProps { + slug?: string + currentPath: string + showSecuritySettings?: boolean + showSsoSettings?: boolean + showLegalDocuments?: boolean +} - const { - organizationShowSsoSettings: showSsoSettings, - organizationShowSecuritySettings: showSecuritySettings, - organizationShowLegalDocuments: showLegalDocuments, - } = useIsFeatureEnabled([ - 'organization:show_sso_settings', - 'organization:show_security_settings', - 'organization:show_legal_documents', - ]) +interface OrganizationSettingsLayoutProps { + pageTitle?: string +} + +export const normalizeOrganizationSettingsPath = (path: string) => path.split('#')[0] +export const getOrganizationSettingsPageTitle = (pageTitle?: string) => pageTitle ?? 'Settings' +export const getOrganizationSettingsDocumentTitle = ( + pageTitle: string | undefined, + title: string +) => `${getOrganizationSettingsPageTitle(pageTitle)} | ${title}` - const navMenuItems = [ +export const generateOrganizationSettingsSections = ({ + slug, + currentPath, + showSecuritySettings = true, + showSsoSettings = true, + showLegalDocuments = true, +}: OrganizationSettingsSectionsProps): SidebarSection[] => { + const configurationLinks = [ { + key: 'general', label: 'General', href: `/org/${slug}/general`, }, ...(showSecuritySettings ? [ { + key: 'security', label: 'Security', href: `/org/${slug}/security`, }, ] : []), - { - label: 'OAuth Apps', - href: `/org/${slug}/apps`, - }, ...(showSsoSettings ? [ { + key: 'sso', label: 'SSO', href: `/org/${slug}/sso`, }, ] : []), + ] + + const connectionsLinks = [ + { + key: 'apps', + label: 'OAuth Apps', + href: `/org/${slug}/apps`, + }, + ] + const complianceLinks = [ { + key: 'audit', label: 'Audit Logs', href: `/org/${slug}/audit`, }, ...(showLegalDocuments ? [ { + key: 'documents', label: 'Legal Documents', href: `/org/${slug}/documents`, }, @@ -63,19 +85,80 @@ function OrganizationSettingsLayout({ children }: PropsWithChildren) { : []), ] + return [ + { + key: 'configuration', + heading: 'Configuration', + links: configurationLinks.map((item) => ({ + ...item, + isActive: currentPath === item.href, + })), + }, + { + key: 'connections', + heading: 'Connections', + links: connectionsLinks.map((item) => ({ + ...item, + isActive: currentPath === item.href, + })), + }, + { + key: 'compliance', + heading: 'Compliance', + links: complianceLinks.map((item) => ({ + ...item, + isActive: currentPath === item.href, + })), + }, + ] +} + +function OrganizationSettingsLayout({ + children, + pageTitle, +}: PropsWithChildren) { + const { slug } = useParams() + const fullCurrentPath = useCurrentPath() + const currentPath = normalizeOrganizationSettingsPath(fullCurrentPath) + const { appTitle } = useCustomContent(['app:title']) + const titleSuffix = appTitle || 'Supabase' + + const { + organizationShowSsoSettings: showSsoSettings, + organizationShowSecuritySettings: showSecuritySettings, + organizationShowLegalDocuments: showLegalDocuments, + } = useIsFeatureEnabled([ + 'organization:show_sso_settings', + 'organization:show_security_settings', + 'organization:show_legal_documents', + ]) + + const sections = generateOrganizationSettingsSections({ + slug, + currentPath, + showSecuritySettings, + showSsoSettings, + showLegalDocuments, + }) + return ( <> - - Organization Settings - - {(navMenuItems.filter(Boolean) as { label: string; href: string }[]).map((item) => ( - - {item.label} - - ))} - - -
{children}
+ + {getOrganizationSettingsDocumentTitle(pageTitle, titleSuffix)} + + + +

Settings

+
+ } + > + {children} + ) } diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index 17320fc9d33ef..f92c0bea15464 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -22,7 +22,7 @@ export const generateSettingsMenu = ( if (!IS_PLATFORM) { return [ { - title: 'Project Settings', + title: 'Configuration', items: [ { name: `Log Drains`, @@ -42,7 +42,7 @@ export const generateSettingsMenu = ( return [ { - title: 'Project Settings', + title: 'Configuration', items: [ { name: 'General', @@ -106,7 +106,7 @@ export const generateSettingsMenu = ( ], }, { - title: 'Configuration', + title: 'Integrations', items: [ { name: 'Data API', diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx index 30fe5a09a3c31..3e6acc6378f36 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx @@ -1,6 +1,15 @@ -import { Plus, Settings, X } from 'lucide-react' +import { Clipboard, Ellipsis, Plus, Settings, X } from 'lucide-react' import { useState } from 'react' -import { AiIconAnimation, Button } from 'ui' +import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { + AiIconAnimation, + Button, + copyToClipboard, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' import { Admonition } from 'ui-patterns' import { ButtonTooltip } from '../ButtonTooltip' @@ -26,6 +35,7 @@ export const AIAssistantHeader = ({ isHipaaProjectDisallowed, aiOptInLevel, }: AIAssistantHeaderProps) => { + const snap = useAiAssistantStateSnapshot() const [isOptInModalOpen, setIsOptInModalOpen] = useState(false) return (
@@ -70,6 +80,26 @@ export const AIAssistantHeader = ({ content: { side: 'bottom', text: 'Permission settings' }, }} /> + + + } + className="h-7 w-7 p-0" + tooltip={{ content: { side: 'bottom', text: 'More options' } }} + /> + + + copyToClipboard(snap.activeChatId ?? '')} + > + + Copy chat ID + + + { - return useProjectSettingsV2Query( - { projectRef }, - { - select: (data) => { - const protocol = data?.app_config?.protocol ?? 'https' - const endpoint = data?.app_config?.endpoint - const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}` - const storageEndpoint = data?.app_config?.storage_endpoint - ? `${IS_PLATFORM ? 'https' : protocol}://${data?.app_config?.storage_endpoint}` - : undefined +export const useProjectApiUrl = ( + { projectRef }: { projectRef?: string }, + { enabled = true }: { enabled?: boolean } = {} +) => { + const { data } = useProjectAddonsQuery({ projectRef }) + const hasCustomDomainsAddon = !!data?.selected_addons.find((x) => x.type === 'custom_domain') - return { endpoint: clientEndpoint, storageEndpoint } - }, - } - ) + const { + data: customDomainData, + error: customDomainsError, + isPending: isLoadingCustomDomains, + isSuccess: isSuccessCustomDomains, + isError: isErrorCustomDomains, + } = useCustomDomainsQuery({ projectRef }, { enabled }) + const isCustomDomainsActive = customDomainData?.customDomain?.status === 'active' + const customEndpoint = + hasCustomDomainsAddon && isCustomDomainsActive + ? `https://${customDomainData?.customDomain?.hostname}` + : undefined + + const { + data: settings, + error: projectSettingsError, + isPending: isLoadingProjectSettings, + isSuccess: isSuccessProjectSettings, + isError: isErrorProjectSettings, + } = useProjectSettingsV2Query({ projectRef }, { enabled }) + const protocol = settings?.app_config?.protocol ?? 'https' + const endpoint = settings?.app_config?.endpoint + + const hostEndpoint = isSuccessProjectSettings ? `${protocol}://${endpoint}` : undefined + const resolvedEndpoint = isCustomDomainsActive ? customEndpoint : hostEndpoint + const storageEndpoint = settings?.app_config?.storage_endpoint + ? `${IS_PLATFORM ? 'https' : protocol}://${settings?.app_config?.storage_endpoint}` + : undefined + + return { + data: resolvedEndpoint, + customEndpoint, + hostEndpoint, + storageEndpoint, + error: projectSettingsError || (hasCustomDomainsAddon ? customDomainsError : undefined), + isPending: isLoadingProjectSettings || (hasCustomDomainsAddon && isLoadingCustomDomains), + isSuccess: isSuccessProjectSettings && (!hasCustomDomainsAddon || isSuccessCustomDomains), + isError: isErrorProjectSettings || (hasCustomDomainsAddon && isErrorCustomDomains), + } } diff --git a/apps/studio/data/custom-domains/custom-domains-query.ts b/apps/studio/data/custom-domains/custom-domains-query.ts index db502694a7a96..bbfbb4eb76efc 100644 --- a/apps/studio/data/custom-domains/custom-domains-query.ts +++ b/apps/studio/data/custom-domains/custom-domains-query.ts @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query' - import { get, handleError } from 'data/fetchers' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { IS_PLATFORM } from 'lib/constants' import type { ResponseError, UseCustomQueryOptions } from 'types' + import { customDomainKeys } from './keys' export type CustomDomainsVariables = { diff --git a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts index 83e1962b335c4..20d75cdb45761 100644 --- a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts +++ b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query' - import { components } from 'api-types' -import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' +import { useProjectApiUrl } from 'data/config/project-endpoint-query' import { handleError } from 'data/fetchers' import { createProjectSupabaseClient } from 'lib/project-supabase-client' import type { ResponseError, UseCustomQueryOptions } from 'types' + import { useAuthConfigQuery } from '../auth/auth-config-query' import { oauthServerAppKeys } from './keys' @@ -46,9 +46,7 @@ export const useOAuthServerAppsQuery = ( ...options }: UseCustomQueryOptions = {} ) => { - const { data: endpointData } = useProjectEndpointQuery({ projectRef }) - const clientEndpoint = endpointData?.endpoint - + const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef }) const { data: authConfig, isSuccess: isSuccessConfig } = useAuthConfigQuery({ projectRef }) const isOAuthServerEnabled = !!authConfig?.OAUTH_SERVER_ENABLED diff --git a/apps/studio/pages/org/[slug]/apps.tsx b/apps/studio/pages/org/[slug]/apps.tsx index bbbfad68bf625..2d478cb13c394 100644 --- a/apps/studio/pages/org/[slug]/apps.tsx +++ b/apps/studio/pages/org/[slug]/apps.tsx @@ -3,15 +3,36 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' import type { NextPageWithLayout } from 'types' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' const OrgOAuthApps: NextPageWithLayout = () => { - return + return ( + <> + + + + OAuth Apps + + Published and authorized OAuth applications + + + + + + + ) } OrgOAuthApps.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/org/[slug]/audit.tsx b/apps/studio/pages/org/[slug]/audit.tsx index 7a8393212bb9a..1d4277ab1bc6c 100644 --- a/apps/studio/pages/org/[slug]/audit.tsx +++ b/apps/studio/pages/org/[slug]/audit.tsx @@ -5,6 +5,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from 'types' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' import { LogoLoader } from 'ui' const OrgAuditLogs: NextPageWithLayout = () => { @@ -13,6 +20,16 @@ const OrgAuditLogs: NextPageWithLayout = () => { return ( <> + + + + Audit Logs + + Organization-level activity history and security event records + + + + {selectedOrganization === undefined && isLoadingPermissions ? : } ) @@ -21,7 +38,7 @@ const OrgAuditLogs: NextPageWithLayout = () => { OrgAuditLogs.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/org/[slug]/documents.tsx b/apps/studio/pages/org/[slug]/documents.tsx index c9e97ece7b6fc..60ec3dddd7f15 100644 --- a/apps/studio/pages/org/[slug]/documents.tsx +++ b/apps/studio/pages/org/[slug]/documents.tsx @@ -3,6 +3,13 @@ import { Documents } from 'components/interfaces/Organization/Documents/Document import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { + ScaffoldContainer, + ScaffoldDescription, + ScaffoldDivider, + ScaffoldHeader, + ScaffoldTitle, +} from 'components/layouts/Scaffold' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' @@ -16,13 +23,24 @@ const OrgDocuments: NextPageWithLayout = () => { return } - return + return ( + <> + + + Legal documents + Compliance documentation and legal agreements + + + + + + ) } OrgDocuments.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/org/[slug]/general.tsx b/apps/studio/pages/org/[slug]/general.tsx index 5638f123a259b..045d9ee48a83d 100644 --- a/apps/studio/pages/org/[slug]/general.tsx +++ b/apps/studio/pages/org/[slug]/general.tsx @@ -5,6 +5,14 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from 'types' +import { PageContainer } from 'ui-patterns/PageContainer' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' import { LogoLoader } from 'ui' const OrgGeneralSettings: NextPageWithLayout = () => { @@ -13,11 +21,23 @@ const OrgGeneralSettings: NextPageWithLayout = () => { return ( <> - {selectedOrganization === undefined && isLoadingPermissions ? ( - - ) : ( - - )} + + + + Organization Settings + + General configuration, privacy, and lifecycle controls + + + + + + {selectedOrganization === undefined && isLoadingPermissions ? ( + + ) : ( + + )} + ) } @@ -25,7 +45,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => { OrgGeneralSettings.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/org/[slug]/security.tsx b/apps/studio/pages/org/[slug]/security.tsx index a4b867621b067..a75bff211d552 100644 --- a/apps/studio/pages/org/[slug]/security.tsx +++ b/apps/studio/pages/org/[slug]/security.tsx @@ -6,6 +6,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' const OrgSecuritySettings: NextPageWithLayout = () => { const { slug } = useParams() @@ -15,13 +22,27 @@ const OrgSecuritySettings: NextPageWithLayout = () => { return } - return + return ( + <> + + + + Security + + Organization-wide security controls and MFA enforcement + + + + + + + ) } OrgSecuritySettings.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/org/[slug]/sso.tsx b/apps/studio/pages/org/[slug]/sso.tsx index 899b1cf7c7689..d5f0d56cae428 100644 --- a/apps/studio/pages/org/[slug]/sso.tsx +++ b/apps/studio/pages/org/[slug]/sso.tsx @@ -6,6 +6,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' +import { + PageHeader, + PageHeaderDescription, + PageHeaderMeta, + PageHeaderSummary, + PageHeaderTitle, +} from 'ui-patterns/PageHeader' const OrgSSO: NextPageWithLayout = () => { const { slug } = useParams() @@ -15,13 +22,27 @@ const OrgSSO: NextPageWithLayout = () => { return } - return + return ( + <> + + + + Single Sign-On + + SAML SSO configuration and domain access controls + + + + + + + ) } OrgSSO.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index e4fcede7007a1..807585de24657 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -29,7 +29,6 @@ import { formatTime, getFilesDataTransferItems, getPathAlongFoldersToIndex, - getPathAlongOpenedFolders, sanitizeNameForDuplicateInColumn, validateFolderName, } from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils' @@ -37,7 +36,7 @@ import { convertFromBytes } from '@/components/interfaces/Storage/StorageSetting import { InlineLink } from '@/components/ui/InlineLink' import { getOrRefreshTemporaryApiKey } from '@/data/api-keys/temp-api-keys-utils' import { configKeys } from '@/data/config/keys' -import { useProjectEndpointQuery } from '@/data/config/project-endpoint-query' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import type { ProjectStorageConfigResponse } from '@/data/config/project-storage-config-query' import { getQueryClient } from '@/data/query-client' import { deleteBucketObject } from '@/data/storage/bucket-object-delete-mutation' @@ -1870,9 +1869,11 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG)) const stateRef = useLatest(state) - const { data: endpointData, isSuccess: isSuccessSettings } = useProjectEndpointQuery({ - projectRef: project?.ref, - }) + const { + storageEndpoint, + hostEndpoint, + isSuccess: isSuccessSettings, + } = useProjectApiUrl({ projectRef: project?.ref }) // [Joshen] JFYI opting with the useEffect here as the storage explorer state was being loaded // before the project details were ready, hence the store kept returning project ref as undefined @@ -1884,7 +1885,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild const storeAlreadyLoaded = state.projectRef === project?.ref if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) { - const clientEndpoint = endpointData.storageEndpoint ?? endpointData.endpoint + const clientEndpoint = storageEndpoint ?? hostEndpoint ?? '' const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable` setState( createStorageExplorerState({ @@ -1901,8 +1902,8 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild project?.connectionString, stateRef, isPaused, - endpointData?.endpoint, - endpointData?.storageEndpoint, + hostEndpoint, + storageEndpoint, isSuccessSettings, ]) diff --git a/apps/www/_go/index.tsx b/apps/www/_go/index.tsx index c9d67a30d985c..5c020232551c3 100644 --- a/apps/www/_go/index.tsx +++ b/apps/www/_go/index.tsx @@ -1,5 +1,6 @@ import type { GoPageInput } from 'marketing' +import byocEarlyAccess from './pre-release/byoc-early-access' import exampleLeadGen from './lead-gen/example-lead-gen' import exampleLegal from './legal/example-legal' import exampleThankYou from './thank-you/example-thank-you' @@ -7,6 +8,7 @@ import boltWebinar from './webinar/bolt-webinar' import boltWebinarThankYou from './webinar/bolt-webinar-thank-you' const pages: GoPageInput[] = [ + byocEarlyAccess, exampleLeadGen, exampleThankYou, exampleLegal, diff --git a/apps/www/_go/pre-release/byoc-early-access.tsx b/apps/www/_go/pre-release/byoc-early-access.tsx new file mode 100644 index 0000000000000..bc50147740cc0 --- /dev/null +++ b/apps/www/_go/pre-release/byoc-early-access.tsx @@ -0,0 +1,127 @@ +import type { GoPageInput } from 'marketing' + +const page: GoPageInput = { + template: 'lead-gen', + slug: 'byoc-early-access', + metadata: { + title: 'Bring Your Own Cloud (BYOC) for Supabase — Early Access', + description: + 'Deploy Supabase in your own AWS account. Meet strict data residency and compliance requirements while Supabase handles operations, upgrades and monitoring.', + ogImage: '/images/landing-pages/byoc-early-access/og.png', + }, + hero: { + title: 'Bring Your Own Cloud (BYOC) for Supabase', + subtitle: 'Early Access', + description: + 'Deploy Supabase in your own AWS account. Meet strict data residency and compliance requirements while Supabase handles operations, upgrades and monitoring.', + ctas: [ + { + label: 'Request Early Access', + href: '#form', + variant: 'primary', + }, + ], + }, + sections: [ + { + type: 'feature-grid', + title: 'Your cloud, operated by Supabase', + description: 'Get the full power of Supabase deployed inside your own infrastructure.', + items: [ + { + title: 'Control where your data goes', + description: + 'Meet data residency and compliance requirements. Your data stays in your infrastructure and in your region.', + }, + { + title: 'Deploy the infrastructure you want', + description: + 'Choose instance sizes and volumes for your use case. No project size constraints.', + }, + { + title: 'Leverage your cloud costs', + description: + 'Apply pre-negotiated discounts and cloud credits to your Supabase deployment.', + }, + { + title: 'Let Supabase manage operations', + description: + 'Supabase handles deployments, upgrades, monitoring and support. No Ops overhead.', + }, + ], + }, + { + type: 'form', + id: 'form', + title: 'Early Access Request Form', + description: + "If you are interested in participating in BYOC early access when it becomes available later in 2026, please fill out the form below. A member of the Supabase team will reach out if you've been selected.", + fields: [ + { + type: 'text', + name: 'first_name', + label: 'First Name', + placeholder: 'First Name', + required: true, + half: true, + }, + { + type: 'text', + name: 'last_name', + label: 'Last Name', + placeholder: 'Last Name', + required: true, + half: true, + }, + { + type: 'email', + name: 'email', + label: 'Email Address', + placeholder: 'Work email', + required: true, + }, + { + type: 'text', + name: 'company_name', + label: 'Company Name', + placeholder: 'Company name', + required: true, + }, + { + type: 'text', + name: 'supabase_org_name', + label: 'Supabase Organization Name', + placeholder: 'Organization name (if applicable)', + required: false, + }, + ], + submitLabel: 'Request Early Access', + disclaimer: + 'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).', + crm: { + hubspot: { + formGuid: 'c09ead14-e1b7-4031-9f77-ba6b2ad96364', + fieldMap: { + first_name: 'firstname', + last_name: 'lastname', + email: 'email', + company_name: 'company', + supabase_org_name: 'what_is_your_supabase_org_slug', + }, + consent: + 'By submitting this form, I confirm that I have read and understood the Privacy Policy.', + }, + customerio: { + event: 'early_access_requested', + profileMap: { + email: 'email', + first_name: 'first_name', + last_name: 'last_name', + }, + }, + }, + }, + ], +} + +export default page diff --git a/apps/www/components/Nav/SolutionsDropdown.tsx b/apps/www/components/Nav/SolutionsDropdown.tsx index 59bc3c5f01bd0..202e36110eb79 100644 --- a/apps/www/components/Nav/SolutionsDropdown.tsx +++ b/apps/www/components/Nav/SolutionsDropdown.tsx @@ -16,9 +16,9 @@ type LinkProps = { export const SolutionsDropdown = () => (
-
- {/* two first columns */} - {DevelopersData['navigation'].slice(0, 2).map((column) => ( +
+ {/* Skill Level, Who it's for, App Type */} + {DevelopersData['navigation'].slice(0, 3).map((column) => ( ))}
@@ -26,11 +26,11 @@ export const SolutionsDropdown = () => (
- {DevelopersData['navigation'][2].links.map((link) => ( + {DevelopersData['navigation'][3].links.map((link) => ( ))}
diff --git a/apps/www/components/Solutions/CustomerEvidenceSection.tsx b/apps/www/components/Solutions/CustomerEvidenceSection.tsx new file mode 100644 index 0000000000000..b72be6e036035 --- /dev/null +++ b/apps/www/components/Solutions/CustomerEvidenceSection.tsx @@ -0,0 +1,72 @@ +import Link from 'next/link' +import { ArrowRight, Check } from 'lucide-react' +import SectionContainer from 'components/Layouts/SectionContainer' + +export interface CustomerEvidenceCustomer { + name: string + logo?: string + highlights: string[] + cta?: { label: string; href: string } +} + +export interface CustomerEvidenceSectionProps { + id: string + heading: React.ReactNode + customers: CustomerEvidenceCustomer[] + className?: string +} + +const CustomerEvidenceSection = ({ + id, + heading, + customers, + className = '', +}: CustomerEvidenceSectionProps) => ( + +
+

{heading}

+
+
+ {customers.map((customer) => ( +
+ {customer.logo && ( +
+ )} +

{customer.name}

+
    + {customer.highlights.map((highlight, i) => ( +
  • + + {highlight} +
  • + ))} +
+ {customer.cta && ( + + {customer.cta.label} + + + )} +
+ ))} +
+ +) + +export default CustomerEvidenceSection diff --git a/apps/www/components/Solutions/WhatItTakesSection.tsx b/apps/www/components/Solutions/WhatItTakesSection.tsx new file mode 100644 index 0000000000000..398605970d0e9 --- /dev/null +++ b/apps/www/components/Solutions/WhatItTakesSection.tsx @@ -0,0 +1,41 @@ +import Link from 'next/link' +import { ArrowUpRight } from 'lucide-react' +import { Button } from 'ui' +import SectionContainer from 'components/Layouts/SectionContainer' + +export interface WhatItTakesItem { + id: string + description: React.ReactNode + url: string + linkLabel?: string +} + +export interface WhatItTakesSectionProps { + id: string + heading: React.ReactNode + items: WhatItTakesItem[] + className?: string +} + +const WhatItTakesSection = ({ id, heading, items, className = '' }: WhatItTakesSectionProps) => ( + +
+

{heading}

+
+ {items.map((item) => ( +
+

{item.description}

+ +
+ ))} +
+
+
+) + +export default WhatItTakesSection diff --git a/apps/www/components/SolutionsStickyNav.tsx b/apps/www/components/SolutionsStickyNav.tsx index 3702336cf4fb7..f99bd5c1c1295 100644 --- a/apps/www/components/SolutionsStickyNav.tsx +++ b/apps/www/components/SolutionsStickyNav.tsx @@ -14,15 +14,16 @@ import { import SectionContainer from 'components/Layouts/SectionContainer' import { SolutionTypes, + appTypeSolutions, + migrationSolutions, skillBasedSolutions, useCaseSolutions, - migrationSolutions, } from 'data/Solutions' interface Props { activeItem: SolutionTypes className?: string - type?: 'skill-based' | 'use-case' | 'migration' + type?: 'skill-based' | 'use-case' | 'migration' | 'app-type' } function SolutionsStickyNav({ type, activeItem, className }: Props) { @@ -32,7 +33,9 @@ function SolutionsStickyNav({ type, activeItem, className }: Props) { ? skillBasedSolutions.solutions : type === 'use-case' ? useCaseSolutions.solutions - : migrationSolutions.solutions + : type === 'app-type' + ? appTypeSolutions.solutions + : migrationSolutions.solutions const items = solutions.map((solution: any) => ({ id: solution.id, name: solution.text, diff --git a/apps/www/data/Solutions.tsx b/apps/www/data/Solutions.tsx index d3f4eafdbe2db..281ea1c336301 100644 --- a/apps/www/data/Solutions.tsx +++ b/apps/www/data/Solutions.tsx @@ -3,9 +3,11 @@ import { Building2Icon, Code2Icon, HammerIcon, + Heart, LightbulbIcon, PointerIcon, PuzzleIcon, + ShieldCheck, TrendingUpIcon, UsersIcon, ZapIcon, @@ -27,6 +29,10 @@ export enum Solutions { hackathon = 'hackathon', innovationTeams = 'innovation-teams', vibeCoders = 'vibe-coders', + b2bSaaS = 'b2b-saas', + finserv = 'finserv', + healthcare = 'healthcare', + agents = 'agents', } export const skillBasedSolutions = { @@ -145,6 +151,40 @@ export const useCaseSolutions = { ], } +export const appTypeSolutions = { + label: 'Solutions', + solutions: [ + { + id: Solutions.b2bSaaS, + text: 'B2B SaaS', + description: '', + url: '/solutions/b2b-saas', + icon: Building2Icon, + }, + { + id: Solutions.finserv, + text: 'FinServ', + description: '', + url: '/solutions/finserv', + icon: ShieldCheck, + }, + { + id: Solutions.healthcare, + text: 'Healthcare', + description: '', + url: '/solutions/healthcare', + icon: Heart, + }, + { + id: Solutions.agents, + text: 'Agents', + description: '', + url: '/solutions/agents', + icon: BotIcon, + }, + ], +} + export const migrationSolutions = { label: 'Solutions', solutions: [ @@ -241,6 +281,16 @@ export const navData = { })), ], }, + { + label: 'App Type', + links: [ + ...appTypeSolutions.solutions.map((solution) => ({ + text: solution.text, + url: solution.url, + icon: solution.icon, + })), + ], + }, { label: 'Migration', links: [ diff --git a/apps/www/data/solutions/agencies.tsx b/apps/www/data/solutions/agencies.tsx index fae83fc019896..3586e2b0de972 100644 --- a/apps/www/data/solutions/agencies.tsx +++ b/apps/www/data/solutions/agencies.tsx @@ -545,8 +545,9 @@ const data: { icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M21.7152 12.8129C21.7152 11.4644 21.4115 10.1867 20.8688 9.0447M12.925 21.6032C14.2829 21.6032 15.5689 21.2952 16.717 20.7454M16.717 20.7454C17.2587 21.5257 18.1612 22.0366 19.1831 22.0366C20.84 22.0366 22.1831 20.6935 22.1831 19.0366C22.1831 17.3798 20.84 16.0366 19.1831 16.0366C17.5263 16.0366 16.1831 17.3798 16.1831 19.0366C16.1831 19.6716 16.3804 20.2605 16.717 20.7454ZM4.96506 16.5471C4.16552 17.086 3.63965 17.9999 3.63965 19.0366C3.63965 20.6935 4.98279 22.0366 6.63965 22.0366C8.2965 22.0366 9.63965 20.6935 9.63965 19.0366C9.63965 17.3798 8.2965 16.0366 6.63965 16.0366C6.01951 16.0366 5.44333 16.2248 4.96506 16.5471ZM9.12614 4.88371C8.58687 4.08666 7.67444 3.56274 6.63965 3.56274C4.98279 3.56274 3.63965 4.90589 3.63965 6.56274C3.63965 8.2196 4.98279 9.56274 6.63965 9.56274C8.2965 9.56274 9.63965 8.2196 9.63965 6.56274C9.63965 5.94069 9.45032 5.36285 9.12614 4.88371ZM20.8688 9.0447C21.6621 8.50486 22.1831 7.59464 22.1831 6.56274C22.1831 4.90589 20.84 3.56274 19.1831 3.56274C17.5263 3.56274 16.1831 4.90589 16.1831 6.56274C16.1831 8.2196 17.5263 9.56274 19.1831 9.56274C19.8081 9.56274 20.3884 9.37165 20.8688 9.0447Z', subheading: ( <> - Connect Supabase to Redshift, BigQuery, MySQL{' '} - and external APIs for seamless integrations with your clients’ existing systems. + Connect Supabase to{' '} + BigQuery, Snowflake, ClickHouse and external + APIs for seamless integrations with your clients’ existing systems. ), image: ( diff --git a/apps/www/data/solutions/agents.tsx b/apps/www/data/solutions/agents.tsx new file mode 100644 index 0000000000000..3ea7233ad2689 --- /dev/null +++ b/apps/www/data/solutions/agents.tsx @@ -0,0 +1,349 @@ +import { + Check, + ClipboardCheck, + FolderLock, + HeartPulse, + Lightbulb, + List, + Lock, + ShieldAlert, + ShieldCheck, + Users, + UserX, +} from 'lucide-react' +import { CubeIcon } from '@heroicons/react/outline' + +import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection' +import { frameworks } from 'components/Hero/HeroFrameworks' + +import type { FeatureGridProps } from 'components/Solutions/FeatureGrid' +import type { PlatformSectionProps } from 'components/Solutions/PlatformSection' +import type { ResultsSectionProps } from 'components/Solutions/ResultsSection' +import type { SecuritySectionProps } from 'components/Enterprise/Security' +import { + FrameworkLink, + type FeaturesSection, + type HeroSection, + type Metadata, +} from './solutions.utils' +import { getSharedSections } from './shared-sections' +import { Image } from 'ui' + +import { useSendTelemetryEvent } from 'lib/telemetry' + +const data: () => { + metadata: Metadata + heroSection: HeroSection + singleQuote: { + id: string + quote: { + text: string + author: string + role: string + logo?: React.ReactElement + link?: string + } + } + why: FeaturesSection + platform: PlatformSectionProps + developerExperience: ReturnType['developerExperience'] + resultsSection: ResultsSectionProps + featureGrid: FeatureGridProps + securitySection: SecuritySectionProps + ecosystemSection: FeaturesSection + platformStarterSection: TwoColumnsSectionProps + customerEvidence: { + id: string + heading: React.ReactNode + customers: Array<{ + name: string + logo?: string + highlights: string[] + cta?: { label: string; href: string } + }> + } +} = () => { + const shared = getSharedSections() + const sendTelemetryEvent = useSendTelemetryEvent() + + return { + metadata: { + metaTitle: 'Supabase for Agents', + metaDescription: + 'One platform for your agents. Memory, tools, and data in one place. Stop stitching together separate services.', + }, + heroSection: { + id: 'hero', + title: 'Supabase for Agents', + h1: ( + <> + One platform for your agents. +
+ Memory, tools, and data in one place. + + ), + subheader: [ + <> + Stop stitching together separate services for memory, vectors, auth, file storage, and + APIs. Supabase gives your agents a complete Postgres backend with everything they need, + from one dashboard, one connection string, one bill. + , + ], + image: undefined, + ctas: [ + { + label: 'Start your project', + href: 'https://supabase.com/dashboard', + type: 'primary' as any, + onClick: () => + sendTelemetryEvent({ + action: 'start_project_button_clicked', + properties: { buttonLocation: 'Solutions: Agents page hero' }, + }), + }, + { + label: 'Request a demo', + href: 'https://supabase.com/contact/sales', + type: 'default' as any, + onClick: () => + sendTelemetryEvent({ + action: 'request_demo_button_clicked', + properties: { buttonLocation: 'Solutions: Agents page hero' }, + }), + }, + ], + }, + singleQuote: { + id: 'social-proof', + quote: { + text: '"Supabase is great because it has everything. I don\'t need a different solution for authentication, a different solution for database, or a different solution for storage."', + author: 'Yasser Elsaid', + role: 'Founder, Chatbase', + link: '/customers/chatbase', + logo: ( + <> + Chatbase + Chatbase + + ), + }, + }, + why: { + id: 'why-supabase', + label: '', + heading: ( + <> + Why agent builders choose Supabase + + ), + subheading: + 'You need infrastructure that keeps up with agents that plan, act, and learn autonomously. Supabase is the complete Postgres developer platform built for agentic workloads.', + features: [ + { + id: 'one-platform', + icon: CubeIcon, + heading: 'One platform, not five.', + subheading: + 'Most agent stacks require a vector database, an auth provider, a file store, an API layer, and a separate Postgres instance. Supabase replaces all of them. One connection string, one dashboard, one bill.', + }, + { + id: 'security-at-agent-speed', + icon: Lock, + heading: 'Security at agent speed.', + subheading: + 'Agents make autonomous decisions with no human in the loop. Row Level Security ensures every database call respects tenant boundaries. Combined with SOC 2 Type II certification and database audit logs, your agent workloads stay secure at production scale.', + }, + { + id: 'native-mcp', + icon: (props: any) => ( + + + + ), + heading: 'Native MCP from day one.', + subheading: + 'Supabase includes an MCP server that supports both read and write operations. Your agents can query schemas, insert records, and manage data through a standardized protocol.', + }, + ], + }, + platform: { + ...shared.platform, + title: ( + <> + Supabase is the Postgres platform{' '} + your agents control + + ), + subheading: 'Everything your agents need to connect, query, and act on your data.', + }, + developerExperience: shared.developerExperience, + resultsSection: shared.resultsSection, + featureGrid: shared.featureGrid, + securitySection: { + id: 'security', + label: 'Security', + heading: ( + <> + Trusted for agent workloads in production + + ), + subheading: + 'Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.', + features: [ + { icon: ShieldCheck, heading: 'SOC 2 Type II certified' }, + { icon: HeartPulse, heading: 'HIPAA compliant' }, + { icon: ShieldAlert, heading: 'DDoS Protection' }, + { icon: Lock, heading: 'Multi-factor Authentication' }, + { icon: ClipboardCheck, heading: 'Vulnerability Management' }, + { icon: Users, heading: 'Role-based access control' }, + { icon: List, heading: 'Database Audit Logs' }, + { icon: Lightbulb, heading: 'Security Advisors' }, + { icon: FolderLock, heading: 'Encrypted Storage' }, + { icon: UserX, heading: 'Network restrictions' }, + ], + cta: { + label: 'Learn about security', + url: '/security', + }, + }, + ecosystemSection: { + id: 'ecosystem', + label: '', + heading: ( + <> + Works with every agent framework + + ), + subheading: 'Supabase integrates with the tools and frameworks agent builders already use.', + features: [ + { + id: 'agent-frameworks', + icon: CubeIcon, + heading: 'Agent frameworks.', + subheading: + 'LangChain and any framework that speaks SQL, REST, or MCP—including CrewAI, AutoGen, and others.', + }, + { + id: 'ai-providers', + icon: Check, + heading: 'AI providers.', + subheading: + 'OpenAI, Anthropic, Google, Mistral, and any model provider. Supabase is model-agnostic.', + }, + { + id: 'development-tools', + icon: Check, + heading: 'Development tools.', + subheading: + 'Cursor, Windsurf, VS Code Copilot, Claude Code, and every editor that supports MCP.', + }, + ], + }, + platformStarterSection: { + id: 'quickstarts', + heading: ( + <> + Get started + + ), + subheading: + 'Copy these prompts into your AI editor to scaffold agent infrastructure on Supabase:', + docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts', + leftFooter: ( +
+ {frameworks.map((framework) => ( + + ))} +
+ ), + aiPrompts: [ + { + id: 'agent-memory', + title: 'Build an agent memory store', + code: 'Create a Supabase schema for persistent agent memory with session tracking, JSONB state storage, and vector embeddings for semantic recall.', + language: 'markdown', + copyable: true, + }, + { + id: 'rag-pipeline', + title: 'Set up a RAG pipeline', + code: 'Build a RAG pipeline on Supabase with document upload to Storage, text extraction via Edge Functions, and pgvector embeddings with hybrid search.', + language: 'markdown', + copyable: true, + }, + { + id: 'multi-tenant', + title: 'Configure multi-tenant agent access', + code: "Design a multi-tenant Supabase schema with Row Level Security policies that restrict each agent to its own tenant's data.", + language: 'markdown', + copyable: true, + }, + { + id: 'tool-endpoint', + title: 'Deploy a tool-calling endpoint', + code: 'Create a Supabase Edge Function that serves as a tool endpoint for an AI agent, accepting structured input, querying the database, and returning formatted results.', + language: 'markdown', + copyable: true, + }, + ], + }, + customerEvidence: { + id: 'case-studies', + heading: ( + <> + Customer stories + + ), + customers: [ + { + name: 'Humata', + logo: '/images/customers/logos/humata.png', + highlights: ['AI-powered document analysis at scale'], + cta: { label: 'Read the case study', href: '/customers/humata' }, + }, + { + name: 'Chatbase', + logo: '/images/customers/logos/chatbase.png', + highlights: [ + 'One of the most successful single-founder AI products', + 'Built entirely on Supabase', + ], + cta: { label: 'Read the case study', href: '/customers/chatbase' }, + }, + { + name: 'Markprompt', + logo: '/images/customers/logos/markprompt.png', + highlights: ['GDPR-compliant AI chatbots on Supabase'], + cta: { label: 'Read the case study', href: '/customers/markprompt' }, + }, + ], + }, + } +} + +export default data diff --git a/apps/www/data/solutions/b2b-saas.tsx b/apps/www/data/solutions/b2b-saas.tsx new file mode 100644 index 0000000000000..0001ad1f4cb57 --- /dev/null +++ b/apps/www/data/solutions/b2b-saas.tsx @@ -0,0 +1,306 @@ +import dynamic from 'next/dynamic' +import { + Check, + ClipboardCheck, + FolderLock, + HeartPulse, + Lightbulb, + List, + Lock, + ShieldAlert, + ShieldCheck, + Users, + UserX, +} from 'lucide-react' +import { CubeIcon } from '@heroicons/react/outline' +import { Image } from 'ui' + +import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection' +import { frameworks } from 'components/Hero/HeroFrameworks' + +import type { FeatureGridProps } from 'components/Solutions/FeatureGrid' +import type { PlatformSectionProps } from 'components/Solutions/PlatformSection' +import type { ResultsSectionProps } from 'components/Solutions/ResultsSection' +import type { SecuritySectionProps } from 'components/Enterprise/Security' +import { + FrameworkLink, + type FeaturesSection, + type HeroSection, + type Metadata, +} from './solutions.utils' +import { getSharedSections } from './shared-sections' + +import { useBreakpoint } from 'common' +import { useSendTelemetryEvent } from 'lib/telemetry' + +const data: () => { + metadata: Metadata + heroSection: HeroSection + singleQuote: { + id: string + quote: { + text: string + author: string + role: string + link?: string + logo?: React.ReactElement + } + } + why: FeaturesSection + platform: PlatformSectionProps + developerExperience: ReturnType['developerExperience'] + resultsSection: ResultsSectionProps + featureGrid: FeatureGridProps + securitySection: SecuritySectionProps + platformStarterSection: TwoColumnsSectionProps + customerEvidence: { + id: string + heading: React.ReactNode + customers: Array<{ + name: string + logo?: string + highlights: string[] + cta?: { label: string; href: string } + }> + } +} = () => { + const isXs = useBreakpoint(640) + const shared = getSharedSections() + const sendTelemetryEvent = useSendTelemetryEvent() + + return { + metadata: { + metaTitle: 'Supabase for B2B SaaS', + metaDescription: + 'Ship faster. Scale smarter. Own your backend. Supabase gives B2B SaaS teams the tools to build, launch, and scale modern applications.', + }, + heroSection: { + id: 'hero', + title: 'Supabase for B2B SaaS', + h1: ( + <> + Ship faster. Scale smarter. + Own your backend. + + ), + subheader: [ + <> + Supabase gives B2B SaaS teams the tools to build, launch, and scale modern applications + without backend complexity. Focus on product velocity, multi-tenant architecture, and zero + backend boilerplate. + , + ], + image: undefined, + ctas: [ + { + label: 'Start your project', + href: 'https://supabase.com/dashboard', + type: 'primary' as any, + onClick: () => + sendTelemetryEvent({ + action: 'start_project_button_clicked', + properties: { buttonLocation: 'Solutions: B2B SaaS page hero' }, + }), + }, + { + label: 'Request a demo', + href: 'https://supabase.com/contact/sales', + type: 'default' as any, + onClick: () => + sendTelemetryEvent({ + action: 'request_demo_button_clicked', + properties: { buttonLocation: 'Solutions: B2B SaaS page hero' }, + }), + }, + ], + }, + singleQuote: { + id: 'social-proof', + quote: { + text: '"Supabase enabled us to focus on building the best email infrastructure for developers, without worrying about backend complexity."', + author: 'Zeno Rocha', + role: 'CEO, Resend', + link: '/customers/resend', + logo: ( + <> + Resend + Resend + + ), + }, + }, + why: { + id: 'why-supabase', + label: '', + heading: ( + <> + Why B2B SaaS companies choose Supabase + + ), + subheading: + 'Build high-velocity SaaS products on a modern backend platform that balances developer speed with architectural integrity.', + features: [ + { + id: 'multi-tenant', + icon: CubeIcon, + heading: 'Ship multi-tenant apps without the plumbing.', + subheading: + 'RLS enforces tenant isolation at the database layer. RBAC controls what each user role can access. No custom middleware, no tenant-routing code, no data leakage risk.', + }, + { + id: 'own-your-data', + icon: Lock, + heading: 'Own your data layer, avoid lock-in.', + subheading: + 'Supabase is just Postgres. No proprietary query language, no vendor-specific APIs. Export your data, self-host if you need to, and keep full control of your architecture.', + }, + { + id: 'scale', + icon: Check, + heading: 'Scale from first customer to enterprise tier.', + subheading: + 'Start free, scale to millions of rows with read replicas, connection pooling, and HA failover. Same platform from prototype through SOC 2 audit.', + }, + ], + }, + platform: { + ...shared.platform, + title: ( + <> + Supabase is the Postgres platform you control + + ), + subheading: + 'Supabase includes everything you need to deliver robust, scalable, and dependable software as a service.', + }, + developerExperience: shared.developerExperience, + resultsSection: shared.resultsSection, + featureGrid: shared.featureGrid, + securitySection: { + id: 'security', + label: 'Security', + heading: ( + <> + Trusted for B2B SaaS solutions at every stage + + ), + subheading: + "Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.", + features: [ + { icon: ShieldCheck, heading: 'SOC 2 Type II certified' }, + { icon: HeartPulse, heading: 'HIPAA compliant' }, + { icon: ShieldAlert, heading: 'DDoS Protection' }, + { icon: Lock, heading: 'Multi-factor Authentication' }, + { icon: ClipboardCheck, heading: 'Vulnerability Management' }, + { icon: Users, heading: 'Role-based access control' }, + { icon: List, heading: 'Database Audit Logs' }, + { icon: Lightbulb, heading: 'Security Advisors' }, + { icon: FolderLock, heading: 'Encrypted Storage' }, + { icon: UserX, heading: 'Network restrictions' }, + ], + cta: { + label: 'Learn about security', + url: '/security', + }, + }, + platformStarterSection: { + id: 'quickstarts', + heading: ( + <> + Choose your platform to start building in seconds + + ), + headingRight: ( + <> + Or, start with Supabase AI Prompts + + ), + docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts', + leftFooter: ( +
+ {frameworks.map((framework) => ( + + ))} +
+ ), + aiPrompts: [ + { + id: 'auth-setup', + title: 'Bootstrap Next.js app with Supabase Auth', + code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.', + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth', + }, + { + id: 'rls-policies', + title: 'Create RLS policies', + code: "Generate Row Level Security policies for tenant isolation. Retrieve your schema, then write policies that restrict users to their tenant's data using the auth.uid() or custom claims.", + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies', + }, + ], + }, + customerEvidence: { + id: 'case-studies', + heading: ( + <> + Customer stories + + ), + customers: [ + { + name: 'Resend', + logo: '/images/customers/logos/resend.png', + highlights: [ + 'Focus on building the best email infrastructure for developers', + 'Backend complexity handled by Supabase', + ], + cta: { label: 'Read the case study', href: '/customers/resend' }, + }, + { + name: 'Mobbin', + logo: '/images/customers/logos/mobbin.png', + highlights: [ + 'Migrated 200,000 users from Firebase', + 'Better authentication experience at scale', + ], + cta: { label: 'Read the case study', href: '/customers/mobbin' }, + }, + { + name: 'Shotgun', + logo: '/images/customers/logos/shotgun.png', + highlights: [ + '83% reduction in data infrastructure costs', + 'Remarkable database efficiency through migration', + ], + cta: { label: 'Read the case study', href: '/customers/shotgun' }, + }, + { + name: 'Quilia', + logo: '/images/customers/logos/quilia.png', + highlights: [ + '75% reduction in development time', + '50% lower costs with enhanced security for sensitive client data', + ], + cta: { label: 'Read the case study', href: '/customers/quilia' }, + }, + ], + }, + } +} + +export default data diff --git a/apps/www/data/solutions/developers.tsx b/apps/www/data/solutions/developers.tsx index 04bc52223dc07..87cea84390503 100644 --- a/apps/www/data/solutions/developers.tsx +++ b/apps/www/data/solutions/developers.tsx @@ -497,8 +497,9 @@ const data: () => { icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: ( <> - Connect Supabase to Redshift, BigQuery, MySQL - , and external APIs for seamless integrations. + Connect Supabase to{' '} + BigQuery, Snowflake, ClickHouse, and external + APIs for seamless integrations. ), image: ( @@ -633,10 +634,10 @@ const data: () => { }, { id: 'multi-region', - title: 'Multi-region Deployments', + title: 'Multi-region Options', description: ( <> - Deploy databases across multiple regions for{' '} + Deploy in your chosen region with optional read replicas in other regions for{' '} global availability. ), @@ -649,8 +650,8 @@ const data: () => { description: ( <> Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + failover and redundancy for mission-critical + applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/data/solutions/finserv.tsx b/apps/www/data/solutions/finserv.tsx new file mode 100644 index 0000000000000..da5907f94456a --- /dev/null +++ b/apps/www/data/solutions/finserv.tsx @@ -0,0 +1,311 @@ +import { + Check, + ClipboardCheck, + FolderLock, + HeartPulse, + Lightbulb, + List, + Lock, + ShieldAlert, + ShieldCheck, + Users, + UserX, +} from 'lucide-react' + +import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection' +import { frameworks } from 'components/Hero/HeroFrameworks' + +import type { FeatureGridProps } from 'components/Solutions/FeatureGrid' +import type { PlatformSectionProps } from 'components/Solutions/PlatformSection' +import type { ResultsSectionProps } from 'components/Solutions/ResultsSection' +import type { SecuritySectionProps } from 'components/Enterprise/Security' +import { + FrameworkLink, + type FeaturesSection, + type HeroSection, + type Metadata, +} from './solutions.utils' +import { getSharedSections } from './shared-sections' +import MainProducts from '../MainProducts' +import { Image } from 'ui' +import { PRODUCT_SHORTNAMES } from 'shared-data/products' + +import { useSendTelemetryEvent } from 'lib/telemetry' + +const data: () => { + metadata: Metadata + heroSection: HeroSection + singleQuote: { + id: string + quote: { + text: string + author: string + role: string + link?: string + logo?: React.ReactElement + } + } + why: FeaturesSection + platform: PlatformSectionProps + developerExperience: ReturnType['developerExperience'] + resultsSection: ResultsSectionProps + featureGrid: FeatureGridProps + securitySection: SecuritySectionProps + platformStarterSection: TwoColumnsSectionProps + customerEvidence: { + id: string + heading: React.ReactNode + customers: Array<{ + name: string + logo?: string + highlights: string[] + cta?: { label: string; href: string } + }> + } +} = () => { + const shared = getSharedSections() + const sendTelemetryEvent = useSendTelemetryEvent() + + return { + metadata: { + metaTitle: 'Supabase for Financial Services', + metaDescription: + 'Secure, compliant financial applications without the complexity. SOC 2, ACID transactions, real-time data, and audit trails built in.', + }, + heroSection: { + id: 'hero', + title: 'Supabase for Financial Services', + h1: ( + <> + Secure, compliant financial applications + without the complexity. + + ), + subheader: [ + <> + Supabase is a Postgres development platform with SOC 2 certification, ACID transactions, + real-time data, and audit trails built in. Build for trading, payments, lending, and + embedded finance from one platform. + , + ], + image: undefined, + ctas: [ + { + label: 'Start your project', + href: 'https://supabase.com/dashboard', + type: 'primary' as any, + onClick: () => + sendTelemetryEvent({ + action: 'start_project_button_clicked', + properties: { buttonLocation: 'Solutions: FinServ page hero' }, + }), + }, + { + label: 'Request a demo', + href: 'https://supabase.com/contact/sales', + type: 'default' as any, + onClick: () => + sendTelemetryEvent({ + action: 'request_demo_button_clicked', + properties: { buttonLocation: 'Solutions: FinServ page hero' }, + }), + }, + ], + }, + singleQuote: { + id: 'social-proof', + quote: { + text: '"We wanted a backend that could accelerate our development while maintaining security and scalability. Supabase stood out due to its automation, integrations, and ecosystem."', + author: 'Raunak Kathuria', + role: 'VP of Engineering, Deriv', + link: '/customers/deriv', + logo: ( + <> + Deriv + Deriv + + ), + }, + }, + why: { + id: 'why-supabase', + label: '', + heading: ( + <> + Why financial services companies choose Supabase + + ), + subheading: 'Build secure, scalable financial applications using a trusted data platform.', + features: [ + { + id: 'compliance', + icon: ShieldCheck, + heading: 'Compliance you can prove, not just claim.', + subheading: + 'SOC 2 Type II certified with comprehensive audit logs, encryption at rest and in transit, and database-level access controls. Meet regulatory requirements with infrastructure that documents every action automatically.', + }, + { + id: 'transactional-integrity', + icon: Check, + heading: 'Transactional integrity at every layer.', + subheading: + 'Postgres provides full ACID compliance for settlements, reversals, and multi-step financial operations. No partial writes, no eventual consistency surprises. Your ledger stays correct.', + }, + { + id: 'realtime-data', + icon: MainProducts[PRODUCT_SHORTNAMES.REALTIME].icon, + heading: 'Real-time data for real-time decisions.', + subheading: + 'Process transactions, detect fraud signals, and push updates to dashboards as they happen. Realtime subscriptions and Edge Functions give your applications the speed financial workloads demand.', + }, + ], + }, + platform: { + ...shared.platform, + title: ( + <> + Supabase is the SOC 2-compliant Postgres platform{' '} + you control + + ), + subheading: + 'Supabase includes everything you need to build secure, compliant financial applications.', + }, + developerExperience: shared.developerExperience, + resultsSection: shared.resultsSection, + featureGrid: shared.featureGrid, + securitySection: { + id: 'security', + label: 'Security', + heading: ( + <> + Trusted for{' '} + financial solutions and transactions of all types + + ), + subheading: + "Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.", + features: [ + { icon: ShieldCheck, heading: 'SOC 2 Type II certified' }, + { icon: HeartPulse, heading: 'HIPAA compliant' }, + { icon: ShieldAlert, heading: 'DDoS Protection' }, + { icon: Lock, heading: 'Multi-factor Authentication' }, + { icon: ClipboardCheck, heading: 'Vulnerability Management' }, + { icon: Users, heading: 'Role-based access control' }, + { icon: List, heading: 'Database Audit Logs' }, + { icon: Lightbulb, heading: 'Security Advisors' }, + { icon: FolderLock, heading: 'Encrypted Storage' }, + { icon: UserX, heading: 'Network restrictions' }, + ], + cta: { + label: 'Learn about security', + url: '/security', + }, + }, + platformStarterSection: { + id: 'quickstarts', + heading: ( + <> + Choose your platform to start building in seconds + + ), + headingRight: ( + <> + Or, start with Supabase AI Prompts + + ), + docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts', + leftFooter: ( +
+ {frameworks.map((framework) => ( + + ))} +
+ ), + aiPrompts: [ + { + id: 'auth-setup', + title: 'Bootstrap Next.js app with Supabase Auth', + code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.', + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth', + }, + { + id: 'rls-policies', + title: 'Create RLS policies', + code: 'Generate Row Level Security policies for secure access control. Retrieve your schema, then write policies that restrict access to sensitive data based on user roles and auth.uid().', + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies', + }, + ], + }, + customerEvidence: { + id: 'case-studies', + heading: ( + <> + Customer stories + + ), + customers: [ + { + name: 'Deriv', + logo: '/images/customers/logos/deriv.png', + highlights: [ + 'Accelerating online trading with scalable Postgres', + 'Automation, integrations, and ecosystem', + ], + cta: { label: 'Read the case study', href: '/customers/deriv' }, + }, + { + name: 'Bree', + logo: '/images/customers/logos/bree.png', + highlights: [ + '10X performance gains when switching from Fauna', + 'Greater developer velocity and AI-ready foundation', + ], + cta: { label: 'Read the case study', href: '/customers/bree' }, + }, + { + name: 'Next Door Lending', + logo: '/images/customers/logos/next-door-lending.png', + highlights: [ + 'Top 10 mortgage broker with Supabase', + 'Leveraged Postgres for lending workflows', + ], + cta: { label: 'Read the case study', href: '/customers/next-door-lending' }, + }, + { + name: 'Rally', + logo: '/images/customers/logos/rally.png', + highlights: ['Building financial applications on Supabase'], + cta: { label: 'Read the case study', href: '/customers/rally' }, + }, + { + name: 'Xendit', + logo: '/images/customers/logos/xendit.png', + highlights: [ + 'Full solution shipped to production in less than one week', + 'Payment processor with transaction verification', + ], + cta: { label: 'Read the case study', href: '/customers/xendit' }, + }, + ], + }, + } +} + +export default data diff --git a/apps/www/data/solutions/firebase.tsx b/apps/www/data/solutions/firebase.tsx index d0d6d89c84a82..dd05519e2bc9c 100644 --- a/apps/www/data/solutions/firebase.tsx +++ b/apps/www/data/solutions/firebase.tsx @@ -456,7 +456,7 @@ const data = { title: 'Foreign Data Wrappers', icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: - 'Connect Supabase to Redshift, BigQuery, MySQL, and external APIs for seamless integrations.', + 'Connect Supabase to BigQuery, Snowflake, ClickHouse, S3, Stripe, Firebase, and external APIs for seamless integrations.', image: ( - Deploy databases across multiple regions for{' '} + Deploy in your chosen region with optional read replicas in other regions for{' '} global availability. ), @@ -600,9 +600,8 @@ const data = { title: 'High Availability Architecture', description: ( <> - Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + Enterprise plans offer failover and redundancy{' '} + for mission-critical applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/data/solutions/hackathon.tsx b/apps/www/data/solutions/hackathon.tsx index d9af1e19526e3..17a34e4469f4f 100644 --- a/apps/www/data/solutions/hackathon.tsx +++ b/apps/www/data/solutions/hackathon.tsx @@ -148,7 +148,7 @@ const data: () => { icon: Sparkles, heading: 'Speed and AI-powered innovation', subheading: - 'Supabase is just Postgres, so it's easy to scale your Hackathon project into a real product. Connect to your favorite AI tools and use built-in Vectors to store embeddings in Postgres. Use Foreign Data Wrappers to connect to Google Sheets, MySQL, BigQuery, and more.', + 'Supabase is just Postgres, so it's easy to scale your Hackathon project into a real product. Connect to your favorite AI tools and use built-in Vectors to store embeddings in Postgres. Use Foreign Data Wrappers to connect to BigQuery, Snowflake, S3, Stripe, and more.', }, ], }, @@ -486,8 +486,9 @@ const data: () => { icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: ( <> - Connect Supabase to Redshift, BigQuery, MySQL - , and external APIs for seamless integrations. + Connect Supabase to{' '} + BigQuery, Snowflake, ClickHouse, and external + APIs for seamless integrations. ), image: ( diff --git a/apps/www/data/solutions/healthcare.tsx b/apps/www/data/solutions/healthcare.tsx new file mode 100644 index 0000000000000..2caaa8ea4313f --- /dev/null +++ b/apps/www/data/solutions/healthcare.tsx @@ -0,0 +1,301 @@ +import { + ClipboardCheck, + FolderLock, + HeartPulse, + Lightbulb, + List, + Lock, + ShieldAlert, + ShieldCheck, + Users, + UserX, +} from 'lucide-react' + +import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection' +import { frameworks } from 'components/Hero/HeroFrameworks' + +import type { FeatureGridProps } from 'components/Solutions/FeatureGrid' +import type { PlatformSectionProps } from 'components/Solutions/PlatformSection' +import type { ResultsSectionProps } from 'components/Solutions/ResultsSection' +import type { SecuritySectionProps } from 'components/Enterprise/Security' +import type { WhatItTakesSectionProps } from 'components/Solutions/WhatItTakesSection' +import { + FrameworkLink, + type FeaturesSection, + type HeroSection, + type Metadata, +} from './solutions.utils' +import { getSharedSections } from './shared-sections' +import { Image } from 'ui' + +import { useSendTelemetryEvent } from 'lib/telemetry' + +const data: () => { + metadata: Metadata + heroSection: HeroSection + singleQuote: { + id: string + quote: { + text: string + author: string + role: string + link?: string + logo?: React.ReactElement + } + } + why: FeaturesSection + platform: PlatformSectionProps + developerExperience: ReturnType['developerExperience'] + resultsSection: ResultsSectionProps + featureGrid: FeatureGridProps + securitySection: SecuritySectionProps + platformStarterSection: TwoColumnsSectionProps + customerEvidence: { + id: string + heading: React.ReactNode + customers: Array<{ + name: string + logo?: string + highlights: string[] + cta?: { label: string; href: string } + }> + } + whatItTakes: WhatItTakesSectionProps +} = () => { + const shared = getSharedSections() + const sendTelemetryEvent = useSendTelemetryEvent() + + return { + metadata: { + metaTitle: 'Supabase for Healthcare', + metaDescription: + 'HIPAA-compliant from day one. Build patient-facing applications, clinical tools, and health data platforms with compliance built in.', + }, + heroSection: { + id: 'hero', + title: 'Supabase for Healthcare', + h1: ( + <> + HIPAA-compliant from day one. + Build with confidence. + + ), + subheader: [ + <> + Supabase provides a fully managed, HIPAA-compliant Postgres platform with PHI protection, + a signed BAA, and audit-ready infrastructure. Build patient-facing applications, clinical + tools, and health data platforms with compliance built in. + , + ], + image: undefined, + ctas: [ + { + label: 'Start your project', + href: 'https://supabase.com/dashboard', + type: 'primary' as any, + onClick: () => + sendTelemetryEvent({ + action: 'start_project_button_clicked', + properties: { buttonLocation: 'Solutions: Healthcare page hero' }, + }), + }, + { + label: 'Request a demo', + href: 'https://supabase.com/contact/sales', + type: 'default' as any, + onClick: () => + sendTelemetryEvent({ + action: 'request_demo_button_clicked', + properties: { buttonLocation: 'Solutions: Healthcare page hero' }, + }), + }, + ], + }, + singleQuote: { + id: 'social-proof', + quote: { + text: '"For me, the biggest benefit of Supabase is developer experience. My expertise doesn\'t lie in databases and infrastructure."', + author: 'Nick Farrant', + role: 'Founding Engineer, Juniver', + link: '/customers/juniver', + logo: ( + <> + Juniver + Juniver + + ), + }, + }, + why: { + id: 'why-supabase', + label: '', + heading: ( + <> + Why healthcare companies choose Supabase + + ), + subheading: + 'Build secure, scalable healthcare applications using a trusted, HIPAA-compliant data platform.', + features: [ + { + id: 'hipaa-baa', + icon: HeartPulse, + heading: 'HIPAA-compliant with a signed BAA.', + subheading: + 'Enable the HIPAA add-on, sign a Business Associate Agreement, and store Protected Health Information on infrastructure that meets the Security Rule. Not a checkbox exercise. Real technical and administrative safeguards.', + }, + { + id: 'audit-trails', + icon: List, + heading: 'Audit trails that satisfy regulators.', + subheading: + 'Every data access, every modification, every login is logged. Database audit logs and Row Level Security give you the documentation regulators expect during compliance reviews.', + }, + { + id: 'patient-data', + icon: Lock, + heading: 'Patient data stays where it belongs.', + subheading: + 'Encrypt at rest and in transit, enforce role-based access controls, and use RLS to guarantee that each provider, clinic, or patient only sees their own data. Multi-region deployment options support data residency requirements.', + }, + ], + }, + platform: { + ...shared.platform, + title: ( + <> + Supabase is the HIPAA-compliant Postgres platform{' '} + you control + + ), + subheading: + 'Supabase includes everything you need to build HIPAA-compliant healthcare applications.', + }, + developerExperience: shared.developerExperience, + resultsSection: shared.resultsSection, + featureGrid: shared.featureGrid, + securitySection: { + id: 'security', + label: 'Security', + heading: ( + <> + Trusted for{' '} + medical records and health data of all types + + ), + subheading: + "Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.", + features: [ + { icon: ShieldCheck, heading: 'SOC 2 Type II certified' }, + { icon: HeartPulse, heading: 'HIPAA compliant' }, + { icon: ShieldAlert, heading: 'DDoS Protection' }, + { icon: Lock, heading: 'Multi-factor Authentication' }, + { icon: ClipboardCheck, heading: 'Vulnerability Management' }, + { icon: Users, heading: 'Role-based access control' }, + { icon: List, heading: 'Database Audit Logs' }, + { icon: Lightbulb, heading: 'Security Advisors' }, + { icon: FolderLock, heading: 'Encrypted Storage' }, + { icon: UserX, heading: 'Network restrictions' }, + ], + cta: { + label: 'Learn about security', + url: '/security', + }, + }, + platformStarterSection: { + id: 'quickstarts', + heading: ( + <> + Choose your platform to start building in seconds + + ), + headingRight: ( + <> + Or, start with Supabase AI Prompts + + ), + docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts', + leftFooter: ( +
+ {frameworks.map((framework) => ( + + ))} +
+ ), + aiPrompts: [ + { + id: 'auth-setup', + title: 'Bootstrap Next.js app with Supabase Auth', + code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.', + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth', + }, + { + id: 'rls-policies', + title: 'Create RLS policies', + code: 'Generate Row Level Security policies for healthcare data. Retrieve your schema, then write policies that restrict access to PHI based on user roles, auth.uid(), and organizational boundaries.', + language: 'markdown', + docsUrl: + 'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies', + }, + ], + }, + customerEvidence: { + id: 'case-studies', + heading: ( + <> + Customer stories + + ), + customers: [ + { + name: 'Juniver', + logo: '/images/customers/logos/juniver.png', + highlights: [ + 'Automated B2B workflows with Edge Functions and RLS', + 'Improved developer experience and performance', + ], + cta: { label: 'Read the case study', href: '/customers/juniver' }, + }, + ], + }, + whatItTakes: { + id: 'what-it-takes', + heading: ( + <> + What it takes to be HIPAA-compliant on Supabase + + ), + items: [ + { + id: 'hipaa-guide', + description: + 'Read our guide on HIPAA and learn more about our shared responsibilities in delivering compliant solutions.', + url: '/docs/guides/security/hipaa-compliance', + linkLabel: 'Read more', + }, + { + id: 'enable-hipaa', + description: 'Enable HIPAA compliance in your project.', + url: '/docs/guides/platform/hipaa-projects', + linkLabel: 'Read more', + }, + ], + }, + } +} + +export default data diff --git a/apps/www/data/solutions/innovation-teams.tsx b/apps/www/data/solutions/innovation-teams.tsx index 5e59d34608bd1..5d1b6e707aec8 100644 --- a/apps/www/data/solutions/innovation-teams.tsx +++ b/apps/www/data/solutions/innovation-teams.tsx @@ -695,10 +695,10 @@ const data: () => { }, { id: 'multi-region', - title: 'Multi-region Deployments', + title: 'Multi-region Options', description: ( <> - Deploy databases across multiple regions for{' '} + Deploy in your chosen region with optional read replicas in other regions for{' '} global availability. ), @@ -711,8 +711,8 @@ const data: () => { description: ( <> Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + failover and redundancy for mission-critical + applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/data/solutions/neon.tsx b/apps/www/data/solutions/neon.tsx index 95c99d95ae61e..7c9c791b1cb04 100644 --- a/apps/www/data/solutions/neon.tsx +++ b/apps/www/data/solutions/neon.tsx @@ -448,7 +448,7 @@ const data = { title: 'Foreign Data Wrappers', icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: - 'Connect Supabase to Redshift, BigQuery, MySQL, and external APIs for seamless integrations.', + 'Connect Supabase to BigQuery, Snowflake, ClickHouse, S3, Stripe, Firebase, and external APIs for seamless integrations.', image: ( - Deploy databases across multiple regions for{' '} + Deploy in your chosen region with optional read replicas in other regions for{' '} global availability. ), @@ -592,9 +592,8 @@ const data = { title: 'High Availability Architecture', description: ( <> - Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + Enterprise plans offer failover and redundancy{' '} + for mission-critical applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/data/solutions/postgres-developers.tsx b/apps/www/data/solutions/postgres-developers.tsx index 1b5dd0145dd24..a924451a079f7 100644 --- a/apps/www/data/solutions/postgres-developers.tsx +++ b/apps/www/data/solutions/postgres-developers.tsx @@ -597,8 +597,9 @@ const data: () => { icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: ( <> - Connect Supabase to Redshift, BigQuery, MySQL - , and external APIs for seamless integrations. + Connect Supabase to{' '} + BigQuery, Snowflake, ClickHouse, S3, Stripe, + Firebase, and external APIs for seamless integrations. ), image: ( @@ -733,11 +734,12 @@ const data: () => { }, { id: 'multi-region', - title: 'Multi-region Deployments', + title: 'Multi-region Options', description: ( <> - Deploy databases across multiple regions for{' '} - global availability. + Deploy in your chosen region with optional{' '} + read replicas in other regions for global + availability. ), icon: 'M8.48462 3.05339C6.79298 3.58819 5.33457 4.64831 4.30037 6.0436C3.4029 7.25444 2.82613 8.71636 2.69516 10.306H6.77142C6.83771 8.01994 7.22916 5.93809 7.84745 4.36313C8.03485 3.88578 8.24723 3.44433 8.48462 3.05339ZM10.9999 1.27832C7.79633 1.27832 4.95467 2.82842 3.18457 5.21656C1.98658 6.83284 1.2778 8.83471 1.2778 11.0001C1.2778 13.1781 1.99476 15.1906 3.20527 16.8117C4.97675 19.1842 7.80877 20.7225 10.9999 20.7225C14.191 20.7225 17.023 19.1841 18.7944 16.8117C20.005 15.1906 20.722 13.1781 20.722 11.0001C20.722 8.83471 20.0132 6.83284 18.8152 5.21656L18.7944 5.18864C17.0229 2.81635 14.1909 1.27832 10.9999 1.27832ZM10.9999 2.66721C10.768 2.66721 10.4732 2.78413 10.1294 3.15462C9.78466 3.52602 9.44227 4.10142 9.14028 4.87067C8.596 6.2571 8.22699 8.16013 8.16092 10.306H13.8389C13.7728 8.16013 13.4038 6.2571 12.8595 4.87067C12.5575 4.10142 12.2151 3.52602 11.8704 3.15462C11.5265 2.78413 11.2318 2.66721 10.9999 2.66721ZM15.2284 10.306C15.1621 8.01994 14.7706 5.93809 14.1523 4.36313C13.9649 3.88578 13.7525 3.44433 13.5152 3.05339C15.1971 3.58512 16.6485 4.63618 17.6816 6.01966L17.6994 6.0436C18.5969 7.25443 19.1737 8.71636 19.3046 10.306H15.2284ZM13.8389 11.6949H8.16092C8.22699 13.8407 8.596 15.7437 9.14028 17.1301C9.44227 17.8994 9.78466 18.4748 10.1294 18.8462C10.4732 19.2167 10.768 19.3336 10.9999 19.3336C11.2318 19.3336 11.5265 19.2167 11.8704 18.8462C12.2151 18.4748 12.5575 17.8994 12.8595 17.1301C13.4038 15.7437 13.7728 13.8407 13.8389 11.6949ZM13.5152 18.9473C13.7526 18.5564 13.965 18.115 14.1523 17.6377C14.7706 16.0627 15.1621 13.9809 15.2284 11.6949H19.3046C19.1727 13.2947 18.5892 14.7653 17.6816 15.9807C16.6485 17.3643 15.1971 18.4155 13.5152 18.9473ZM8.48458 18.9474C8.24721 18.5564 8.03484 18.115 7.84745 17.6377C7.22916 16.0627 6.83771 13.9809 6.77142 11.6949H2.6952C2.82712 13.2947 3.41061 14.7653 4.31815 15.9808C5.35126 17.3644 6.80264 18.4156 8.48458 18.9474Z', @@ -748,9 +750,9 @@ const data: () => { title: 'High Availability Architecture', description: ( <> - Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + Enterprise plans include{' '} + failover and redundancy for mission-critical + applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/data/solutions/shared-sections.tsx b/apps/www/data/solutions/shared-sections.tsx new file mode 100644 index 0000000000000..9104ef6ee01c7 --- /dev/null +++ b/apps/www/data/solutions/shared-sections.tsx @@ -0,0 +1,15 @@ +/** + * Shared sections reused from postgres-developers across app-type solution pages + * (B2B SaaS, FinServ, Healthcare, Agents) + */ +import getPostgresDevelopersContent from './postgres-developers' + +export const getSharedSections = () => { + const pg = getPostgresDevelopersContent() + return { + platform: pg.platform, + developerExperience: pg.developerExperience, + resultsSection: pg.resultsSection, + featureGrid: pg.featureGrid, + } +} diff --git a/apps/www/data/solutions/startups.tsx b/apps/www/data/solutions/startups.tsx index 801aa0d04eca7..61493118c502b 100644 --- a/apps/www/data/solutions/startups.tsx +++ b/apps/www/data/solutions/startups.tsx @@ -486,8 +486,9 @@ const data: () => { icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z', subheading: ( <> - Connect Supabase to Redshift, BigQuery, MySQL - , and external APIs for seamless integrations. + Connect Supabase to{' '} + BigQuery, Snowflake, ClickHouse, and external + APIs for seamless integrations. ), image: ( @@ -622,10 +623,10 @@ const data: () => { }, { id: 'multi-region', - title: 'Multi-region Deployments', + title: 'Multi-region Options', description: ( <> - Deploy databases across multiple regions for{' '} + Deploy in your chosen region with optional read replicas in other regions for{' '} global availability. ), @@ -638,8 +639,8 @@ const data: () => { description: ( <> Enterprise plans offer{' '} - automatic failover and redundancy for - mission-critical applications. + failover and redundancy for mission-critical + applications. ), icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557', diff --git a/apps/www/pages/solutions/agents.tsx b/apps/www/pages/solutions/agents.tsx new file mode 100644 index 0000000000000..464f2abf97e59 --- /dev/null +++ b/apps/www/pages/solutions/agents.tsx @@ -0,0 +1,88 @@ +import { NextPage } from 'next' +import dynamic from 'next/dynamic' +import { NextSeo } from 'next-seo' + +import Layout from 'components/Layouts/Default' +import ProductHeader from 'components/Sections/ProductHeader2' +import SolutionsStickyNav from 'components/SolutionsStickyNav' + +import getContent from 'data/solutions/agents' +import { Solutions } from 'data/Solutions' + +const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection')) +const SingleQuote = dynamic(() => import('components/Sections/SingleQuote')) +const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection')) +const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection')) +const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection')) +const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid')) +const Security = dynamic(() => import('components/Enterprise/Security')) +const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection')) +const CustomerEvidenceSection = dynamic( + () => import('components/Solutions/CustomerEvidenceSection') +) + +const AgentsPage: NextPage = () => { + const content = getContent() + + return ( + <> + + + + + + + + + + + + + + + + + ) +} + +export default AgentsPage diff --git a/apps/www/pages/solutions/b2b-saas.tsx b/apps/www/pages/solutions/b2b-saas.tsx new file mode 100644 index 0000000000000..769bd852d2bb2 --- /dev/null +++ b/apps/www/pages/solutions/b2b-saas.tsx @@ -0,0 +1,87 @@ +import { NextPage } from 'next' +import dynamic from 'next/dynamic' +import { NextSeo } from 'next-seo' + +import Layout from 'components/Layouts/Default' +import ProductHeader from 'components/Sections/ProductHeader2' +import SolutionsStickyNav from 'components/SolutionsStickyNav' + +import getContent from 'data/solutions/b2b-saas' +import { Solutions } from 'data/Solutions' + +const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection')) +const SingleQuote = dynamic(() => import('components/Sections/SingleQuote')) +const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection')) +const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection')) +const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection')) +const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid')) +const Security = dynamic(() => import('components/Enterprise/Security')) +const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection')) +const CustomerEvidenceSection = dynamic( + () => import('components/Solutions/CustomerEvidenceSection') +) + +const B2BSaaSPage: NextPage = () => { + const content = getContent() + + return ( + <> + + + + + + + + + + + + + + + + ) +} + +export default B2BSaaSPage diff --git a/apps/www/pages/solutions/finserv.tsx b/apps/www/pages/solutions/finserv.tsx new file mode 100644 index 0000000000000..e94c291e6965e --- /dev/null +++ b/apps/www/pages/solutions/finserv.tsx @@ -0,0 +1,87 @@ +import { NextPage } from 'next' +import dynamic from 'next/dynamic' +import { NextSeo } from 'next-seo' + +import Layout from 'components/Layouts/Default' +import ProductHeader from 'components/Sections/ProductHeader2' +import SolutionsStickyNav from 'components/SolutionsStickyNav' + +import getContent from 'data/solutions/finserv' +import { Solutions } from 'data/Solutions' + +const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection')) +const SingleQuote = dynamic(() => import('components/Sections/SingleQuote')) +const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection')) +const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection')) +const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection')) +const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid')) +const Security = dynamic(() => import('components/Enterprise/Security')) +const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection')) +const CustomerEvidenceSection = dynamic( + () => import('components/Solutions/CustomerEvidenceSection') +) + +const FinServPage: NextPage = () => { + const content = getContent() + + return ( + <> + + + + + + + + + + + + + + + + ) +} + +export default FinServPage diff --git a/apps/www/pages/solutions/healthcare.tsx b/apps/www/pages/solutions/healthcare.tsx new file mode 100644 index 0000000000000..aa427f8003b39 --- /dev/null +++ b/apps/www/pages/solutions/healthcare.tsx @@ -0,0 +1,89 @@ +import { NextPage } from 'next' +import dynamic from 'next/dynamic' +import { NextSeo } from 'next-seo' + +import Layout from 'components/Layouts/Default' +import ProductHeader from 'components/Sections/ProductHeader2' +import SolutionsStickyNav from 'components/SolutionsStickyNav' + +import getContent from 'data/solutions/healthcare' +import { Solutions } from 'data/Solutions' + +const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection')) +const SingleQuote = dynamic(() => import('components/Sections/SingleQuote')) +const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection')) +const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection')) +const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection')) +const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid')) +const Security = dynamic(() => import('components/Enterprise/Security')) +const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection')) +const CustomerEvidenceSection = dynamic( + () => import('components/Solutions/CustomerEvidenceSection') +) +const WhatItTakesSection = dynamic(() => import('components/Solutions/WhatItTakesSection')) + +const HealthcarePage: NextPage = () => { + const content = getContent() + + return ( + <> + + + + + + + + + + + + + + + + + ) +} + +export default HealthcarePage diff --git a/apps/www/pages/solutions/innovation-teams.tsx b/apps/www/pages/solutions/innovation-teams.tsx index 5e5731aa532c8..ab1b0cec54592 100644 --- a/apps/www/pages/solutions/innovation-teams.tsx +++ b/apps/www/pages/solutions/innovation-teams.tsx @@ -2,7 +2,7 @@ import { NextPage } from 'next' import dynamic from 'next/dynamic' import { NextSeo } from 'next-seo' import Link from 'next/link' -import { Check, ArrowRight } from 'lucide-react' +import { Check } from 'lucide-react' import Layout from 'components/Layouts/Default' import SolutionsStickyNav from 'components/SolutionsStickyNav' @@ -19,6 +19,9 @@ const DXSection = dynamic(() => import('components/Solutions/DeveloperExperience const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection')) const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid')) const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection')) +const CustomerEvidenceSection = dynamic( + () => import('components/Solutions/CustomerEvidenceSection') +) const MPCSection = dynamic(() => import('components/Solutions/MPCSection')) const SecuritySection = dynamic(() => import('components/Enterprise/Security')) @@ -193,66 +196,7 @@ const InnovationTeams: NextPage = () => { subheading={data.why.subheading} features={data.why.features} /> - {/* Customer Evidence Section */} - {data.customerEvidence && ( - -
-

- {data.customerEvidence.heading} -

-
-
- {data.customerEvidence.customers.map( - (customer: { - name: string - logo?: string - highlights: string[] - cta?: { label: string; href: string } - }) => ( -
- {customer.logo && ( -
- )} -

{customer.name}

-
    - {customer.highlights.map((highlight: string, i: number) => ( -
  • - - {highlight} -
  • - ))} -
- {customer.cta && ( - - {customer.cta.label} - - - )} -
- ) - )} -
- - )} + {data.customerEvidence && }