From 57dde08672b1f95405b2f666f2804e64c00755ae Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:40:35 +1100 Subject: [PATCH 01/19] chore(design-system): better breadcrumbs (#42310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Minor update ## What is the current behavior? Our design system’s breadcrumbs: 1. Are not interactive 2. ~~Repeat the page title, which is directly below~~ ## What is the new behavior? These breadcrumbs: - Are hyperlinked - Show all parents, if nested - ~~Don’t repeat the page title~~ | Before | After | | --- | --- | | Forms-FBDF180F-F101-407C-A8EB-A06BBDB3509A | Forms-95348190-C8B2-4A3A-AE55-A4A5A16CD4B4 | ## Additional context I wonder if we should either drop the `/docs/` subdirectory or rename it. The latter would make sense if we split, say, `/product/` from `/icons/` and/or `/brand`. [Primer](https://primer.style/octicons/design-guidelines/) does this well. --- .../app/(app)/docs/[[...slug]]/page.tsx | 37 ++++++++++++++++--- apps/design-system/components/pager.tsx | 24 ++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx index f4e4c6694d9..f3cdccc24d5 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 b5c6eb39a0d..b7b1620b8ea 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 +} From 706207436fe27a6632028b226f3b62a2fa0243db Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:15:40 +1100 Subject: [PATCH 02/19] chore(studio): move organization settings nav to sidebar layout (#43228) ## What kind of change does this PR introduce? UI change ## What is the current behavior? Organization settings are currently spread across a NavMenu. This is getting unweildy as the amount of children within Organization settings grows. ## What is the new behavior? Organization settings are now on a sidebar, just like Project Settings and most other surfaces. | Before | After | | --- | --- | | Supabase | General Supabase | ## Additional context @fsansalvadore and the design team have have other efforts going on to reduce interface clutter, e.g. nested sidebars. --- .../Organization/AuditLogs/AuditLogs.tsx | 4 +- .../GeneralSettings/GeneralSettings.tsx | 57 +++-- .../OrganizationDeletePanel.tsx | 42 ++-- .../Organization/OAuthApps/OAuthApps.tsx | 2 +- .../interfaces/Organization/SSO/SSOConfig.tsx | 2 +- .../Organization/SecuritySettings.tsx | 2 +- apps/studio/components/interfaces/Sidebar.tsx | 16 +- .../OrganizationSettingsLayout.test.ts | 82 ++++++++ .../OrganizationSettingsLayout.tsx | 195 +++++++++++++----- .../SettingsMenu.utils.tsx | 6 +- apps/studio/pages/org/[slug]/apps.tsx | 25 ++- apps/studio/pages/org/[slug]/audit.tsx | 19 +- apps/studio/pages/org/[slug]/documents.tsx | 22 +- apps/studio/pages/org/[slug]/general.tsx | 32 ++- apps/studio/pages/org/[slug]/security.tsx | 25 ++- apps/studio/pages/org/[slug]/sso.tsx | 25 ++- 16 files changed, 431 insertions(+), 125 deletions(-) create mode 100644 apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index f2887ca1f4c..ff36e39e6b9 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -163,7 +163,7 @@ export const AuditLogs = () => { 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 54194192c44..dd76e2e0d4d 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 d484b76993b..a9ddf0d076b 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 17312e0b462..c340127cf3c 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 753af528fcc..f6a454b40ed 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 50162239359..51af92d27de 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 ? ( { 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/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.test.ts new file mode 100644 index 00000000000..23a4bfdf06c --- /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 b9f974dc654..a7c7aa10930 100644 --- a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx @@ -1,17 +1,127 @@ -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) { +interface OrganizationSettingsSectionsProps { + slug?: string + currentPath: string + showSecuritySettings?: boolean + showSsoSettings?: boolean + showLegalDocuments?: boolean +} + +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}` + +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`, + }, + ] + : []), + ...(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`, + }, + ] + : []), + ] + + 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() - // Get the path without any hash values const fullCurrentPath = useCurrentPath() - const [currentPath] = fullCurrentPath.split('#') + const currentPath = normalizeOrganizationSettingsPath(fullCurrentPath) + const { appTitle } = useCustomContent(['app:title']) + const titleSuffix = appTitle || 'Supabase' const { organizationShowSsoSettings: showSsoSettings, @@ -23,59 +133,32 @@ function OrganizationSettingsLayout({ children }: PropsWithChildren) { 'organization:show_legal_documents', ]) - const navMenuItems = [ - { - label: 'General', - href: `/org/${slug}/general`, - }, - ...(showSecuritySettings - ? [ - { - label: 'Security', - href: `/org/${slug}/security`, - }, - ] - : []), - { - label: 'OAuth Apps', - href: `/org/${slug}/apps`, - }, - ...(showSsoSettings - ? [ - { - label: 'SSO', - href: `/org/${slug}/sso`, - }, - ] - : []), - - { - label: 'Audit Logs', - href: `/org/${slug}/audit`, - }, - ...(showLegalDocuments - ? [ - { - label: 'Legal Documents', - href: `/org/${slug}/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 17320fc9d33..f92c0bea154 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/pages/org/[slug]/apps.tsx b/apps/studio/pages/org/[slug]/apps.tsx index bbbfad68bf6..2d478cb13c3 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 7a8393212bb..1d4277ab1bc 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 c9e97ece7b6..60ec3dddd7f 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 5638f123a25..045d9ee48a8 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 a4b867621b0..a75bff211d5 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 899b1cf7c76..d5f0d56cae4 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} ) From e8d68242854c6561d7af17514a44c3d68675133d Mon Sep 17 00:00:00 2001 From: Prashant Sridharan <914007+CoolAssPuppy@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:28:29 +0000 Subject: [PATCH 03/19] Added four new solution pages (#43242) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? I added four new solution pages for marketing purposes: - B2B SaaS - Healthcare - FinServ - Agents I followed the data model and components of all existing solution pages. I also altered the mega menu to include a new category. --------- Co-authored-by: Alan Daniel --- apps/www/components/Nav/SolutionsDropdown.tsx | 10 +- .../Solutions/CustomerEvidenceSection.tsx | 72 ++++ .../Solutions/WhatItTakesSection.tsx | 41 ++ apps/www/components/SolutionsStickyNav.tsx | 9 +- apps/www/data/Solutions.tsx | 50 +++ apps/www/data/solutions/agencies.tsx | 5 +- apps/www/data/solutions/agents.tsx | 349 ++++++++++++++++++ apps/www/data/solutions/b2b-saas.tsx | 306 +++++++++++++++ apps/www/data/solutions/developers.tsx | 13 +- apps/www/data/solutions/finserv.tsx | 311 ++++++++++++++++ apps/www/data/solutions/firebase.tsx | 11 +- apps/www/data/solutions/hackathon.tsx | 7 +- apps/www/data/solutions/healthcare.tsx | 301 +++++++++++++++ apps/www/data/solutions/innovation-teams.tsx | 8 +- apps/www/data/solutions/neon.tsx | 11 +- .../data/solutions/postgres-developers.tsx | 18 +- apps/www/data/solutions/shared-sections.tsx | 15 + apps/www/data/solutions/startups.tsx | 13 +- apps/www/pages/solutions/agents.tsx | 88 +++++ apps/www/pages/solutions/b2b-saas.tsx | 87 +++++ apps/www/pages/solutions/finserv.tsx | 87 +++++ apps/www/pages/solutions/healthcare.tsx | 89 +++++ apps/www/pages/solutions/innovation-teams.tsx | 66 +--- 23 files changed, 1857 insertions(+), 110 deletions(-) create mode 100644 apps/www/components/Solutions/CustomerEvidenceSection.tsx create mode 100644 apps/www/components/Solutions/WhatItTakesSection.tsx create mode 100644 apps/www/data/solutions/agents.tsx create mode 100644 apps/www/data/solutions/b2b-saas.tsx create mode 100644 apps/www/data/solutions/finserv.tsx create mode 100644 apps/www/data/solutions/healthcare.tsx create mode 100644 apps/www/data/solutions/shared-sections.tsx create mode 100644 apps/www/pages/solutions/agents.tsx create mode 100644 apps/www/pages/solutions/b2b-saas.tsx create mode 100644 apps/www/pages/solutions/finserv.tsx create mode 100644 apps/www/pages/solutions/healthcare.tsx diff --git a/apps/www/components/Nav/SolutionsDropdown.tsx b/apps/www/components/Nav/SolutionsDropdown.tsx index 59bc3c5f01b..202e36110eb 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 00000000000..b72be6e0360 --- /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 00000000000..398605970d0 --- /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 3702336cf4f..f99bd5c1c12 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 d3f4eafdbe2..281ea1c3363 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 fae83fc0198..3586e2b0de9 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 00000000000..3ea7233ad26 --- /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 00000000000..0001ad1f4cb --- /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 04bc52223dc..87cea843905 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 00000000000..da5907f9445 --- /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 d0d6d89c84a..dd05519e2bc 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 d9af1e19526..17a34e4469f 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 00000000000..2caaa8ea431 --- /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 5e59d34608b..5d1b6e707ae 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 95c99d95ae6..7c9c791b1cb 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 1b5dd0145dd..a924451a079 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 00000000000..9104ef6ee01 --- /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 801aa0d04ec..61493118c50 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 00000000000..464f2abf97e --- /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 00000000000..769bd852d2b --- /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 00000000000..e94c291e696 --- /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 00000000000..aa427f8003b --- /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 5e5731aa532..ab1b0cec545 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 && } Date: Mon, 2 Mar 2026 19:06:55 -0500 Subject: [PATCH 04/19] feat: byoc early access page (#43315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Adds a dedicated landing page for BYOC Early Access ## What is the current behavior? ## What is the new behavior? Adds /go/byoc-early-access using the _go lead gen page system. The page includes: - Hero section with "Request Early Access" CTA - Feature grid with the 4 BYOC value props (data residency, custom infrastructure, cloud cost optimization, managed operations) - Early access request form with First Name, Last Name, Email, Company Name, and Supabase Organization Name fields - Dual CRM integration: HubSpot and Customer.io ## Additional context Screenshot 2026-03-02 at 5 00
18 PM Screenshot 2026-03-02 at 5 00
30 PM --------- Co-authored-by: Ana Mogul --- apps/www/_go/index.tsx | 2 + .../www/_go/pre-release/byoc-early-access.tsx | 127 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 apps/www/_go/pre-release/byoc-early-access.tsx diff --git a/apps/www/_go/index.tsx b/apps/www/_go/index.tsx index c9d67a30d98..5c020232551 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 00000000000..bc50147740c --- /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 From 18ce0659c7f2d91aa1b59d29da9d258639f45805 Mon Sep 17 00:00:00 2001 From: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:34:00 -0500 Subject: [PATCH 05/19] feat(assistant): add overflow menu with Copy chat ID action (#43303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an overflow `…` menu to the AI assistant header with a **Copy chat ID** action. Clicking it copies the active chat's UUID to the clipboard, useful for reproduction/debugging. CleanShot 2026-03-02 at 14 55 31@2x This facilitates filtering Logs like so: CleanShot 2026-03-02 at 14 57 01@2x Closes AI-441 --- .../ui/AIAssistantPanel/AIAssistantHeader.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx index 30fe5a09a3c..3e6acc6378f 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 + + + Date: Tue, 3 Mar 2026 11:37:08 +0800 Subject: [PATCH 06/19] Joshen/fe 2621 show custom domain on dashboard and connect modal (#43233) ## Context Main fix is to adjust the new home page + connect dialog (and connect sheet) to render the project's custom domain if available image image ## Changes involved - Created a `useProjectApiUrl` hook that will return the API URL depending if custom domains is available, otherwise default to default project API URL - Refactored all the other places that were manually deriving the project's endpoint - Storage Explorer -> copy URL - Edge Functions - Integrations -> Data API + API Docs - Auth Providers -> Callback URL - Also updated the copy CTA for the addons page - Instead of just "Change xxx", make it a bit more actionable - For add ons with binary states (Custom domains, IPv4) - If not enabled yet, "Enable xxx", otherwise "Toggle xxx" - For PITR - If not enabled yet, "Enable PITR", otherwise "Change recovery duration" - Also added "Edit custom domain" CTA if enabled image ## To test - [ ] Verify that for a project with custom domain set up, all the affected UI mentioned in the above section look correct --- .../Auth/AuthProvidersForm/ProviderForm.tsx | 31 ++--- .../OAuthApps/CreateOrUpdateOAuthAppSheet.tsx | 10 +- .../Auth/OAuthApps/DeleteOAuthAppModal.tsx | 7 +- .../Auth/OAuthApps/OAuthAppsList.tsx | 6 +- .../interfaces/Connect/ApiKeysTabContent.tsx | 5 +- .../components/interfaces/Connect/Connect.tsx | 19 +-- .../interfaces/ConnectSheet/ConnectSheet.tsx | 23 ++-- .../interfaces/Docs/ResourceContent.tsx | 10 +- .../EdgeFunctionDetails.tsx | 37 ++---- .../Functions/EdgeFunctionsListItem.tsx | 16 +-- .../Functions/TerminalInstructions.tsx | 25 ++-- .../HomeNew/ProjectConnectionHoverCard.tsx | 30 ++--- .../DataApi/DataApi.utils.test.ts | 118 ++---------------- .../Integrations/DataApi/DataApi.utils.ts | 30 +---- .../Integrations/DataApi/DocView.tsx | 10 +- .../ProjectAPIDocs/ProjectAPIDocs.tsx | 22 +--- .../Settings/API/DataApiProjectUrlCard.tsx | 8 +- .../interfaces/Settings/Addons/Addons.tsx | 41 +++--- .../CustomDomainActivate.tsx | 11 +- .../CustomDomainConfig/CustomDomainConfig.tsx | 14 +-- .../CustomDomainConfig/CustomDomainDelete.tsx | 9 +- .../CustomDomainConfig/CustomDomainVerify.tsx | 12 +- .../CustomDomainsConfigureHostname.tsx | 13 +- .../CustomDomainsShimmerLoader.tsx | 4 +- .../Storage/StorageExplorer/useCopyUrl.tsx | 23 ++-- .../TableEditor/ApiAccessToggle.tsx | 52 ++++---- .../data/config/project-endpoint-query.ts | 64 +++++++--- .../custom-domains/custom-domains-query.ts | 2 +- .../oauth-server-apps-query.ts | 8 +- apps/studio/state/storage-explorer.tsx | 17 +-- 30 files changed, 243 insertions(+), 434 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index daf0e8bbe13..594b16b7d37 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 df996225bd9..76a8818cb90 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 817103165eb..902b259d075 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 e1a486efbb8..78232db1ca6 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 e2e91db0874..9ccd9c8d47d 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 60d3d1f5e94..02f605fc838 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 f5f58076ab5..fd4093974e0 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 40f8fd83fa0..3da5c1a0ada 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 359948879a7..910d4f41948 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 79e77e112b0..f4d82bb4c02 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 91aa79824e5..82c8e4bdf8f 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 838bc0f7233..f39388b27dc 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' + 'replica-2', + 'https://replica-2.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' - ) 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 61990329ea5..da0fd5d6be1 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 7316245f00e..8950db2a349 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 ? ( { { 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 68dca08580a..66bcbd2b07a 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 7ae49487102..0b35de91316 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 bf3c82fc730..5edd3a16d38 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 c0e128e9fd4..da82b6dc828 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 943ac726035..0303780c275 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 cd0ad8a982d..5555efc79f6 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 c4f5af2aeb3..c942e8e48f9 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/Storage/StorageExplorer/useCopyUrl.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/useCopyUrl.tsx index ed94cfbddcc..c6c70036a01 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 5083475f832..38665560712 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/data/config/project-endpoint-query.ts b/apps/studio/data/config/project-endpoint-query.ts index 4f4445e5213..d1a7c1bd028 100644 --- a/apps/studio/data/config/project-endpoint-query.ts +++ b/apps/studio/data/config/project-endpoint-query.ts @@ -1,21 +1,53 @@ import { IS_PLATFORM } from 'common' -import { ProjectSettingsVariables, useProjectSettingsV2Query } from './project-settings-v2-query' +import { useProjectSettingsV2Query } from './project-settings-v2-query' +import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query' +import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' -export const useProjectEndpointQuery = ({ projectRef }: ProjectSettingsVariables) => { - 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 db502694a7a..bbfbb4eb76e 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 83e1962b335..20d75cdb457 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/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index e4fcede7007..807585de246 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, ]) From 74ef2825b06111c4a050b23e051f91a286f65505 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:02:45 -0500 Subject: [PATCH 07/19] feat: filter incidents with impact none out of banner (#43311) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Enhancement ## What is the current behavior? Incidents are displayed regardless of impact. ## What is the new behavior? Filters out incidents with impact = none ## Additional context Resolves FE-2649 --- .../layouts/AppLayout/useStatusPageBannerVisibility.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts b/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts index 4bce0648080..2a37187faf3 100644 --- a/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts +++ b/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts @@ -19,7 +19,8 @@ export function useStatusPageBannerVisibility(): StatusPageBannerData | null { useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true' const { data: allStatusPageEvents } = useIncidentStatusQuery() - const { incidents = [] } = allStatusPageEvents ?? {} + const { incidents: allIncidents = [] } = allStatusPageEvents ?? {} + const incidents = allIncidents.filter((i) => i.impact !== 'none') const hasActiveIncidents = incidents.length > 0 From 277441334f8d0e6163d27f58b528548f2fb58169 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 3 Mar 2026 02:22:55 -0500 Subject: [PATCH 08/19] feat(studio): Display region-specific incidents in RegionSelector (#43308) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature ## What is the current behavior? Region-specific incidents are only shown in the global StatusPageBanner, which doesn't clearly indicate that an incident only affects specific regions during project creation. ## What is the new behavior? Region-specific incidents are now displayed inline in the RegionSelector with smart region matching to show which regions are affected. The StatusPageBanner logic is updated to avoid duplicate incident notices for region-specific incidents when creating projects. ## Additional context CleanShot 2026-03-02 at 16 32 34@2x Resolves FE-2652 --------- Co-authored-by: Joshen Lim --- .../ProjectCreation/RegionSelector.tsx | 343 ++++++++++-------- .../AppLayout/StatusPageBanner.utils.test.ts | 8 +- .../AppLayout/StatusPageBanner.utils.ts | 3 +- 3 files changed, 204 insertions(+), 150 deletions(-) diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 696fdf86ae4..cb6aa8227e2 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -1,16 +1,11 @@ -import { UseFormReturn } from 'react-hook-form' - import { useFlag, useParams } from 'common' -import AlertError from 'components/ui/AlertError' -import Panel from 'components/ui/Panel' -import { useDefaultRegionQuery } from 'data/misc/get-default-region-query' -import { useOrganizationAvailableRegionsQuery } from 'data/organizations/organization-available-regions-query' -import type { DesiredInstanceSize } from 'data/projects/new-project.constants' -import { BASE_PATH, PROVIDERS } from 'lib/constants' +import { UseFormReturn } from 'react-hook-form' import type { CloudProvider } from 'shared-data' import { Badge, + cn, FormField_Shadcn_, + Select_Shadcn_, SelectContent_Shadcn_, SelectGroup_Shadcn_, SelectItem_Shadcn_, @@ -18,15 +13,23 @@ import { SelectSeparator_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, - Select_Shadcn_, Tooltip, TooltipContent, TooltipTrigger, - cn, } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + import { CreateProjectForm } from './ProjectCreation.schema' import { getAvailableRegions } from './ProjectCreation.utils' +import AlertError from '@/components/ui/AlertError' +import { InlineLink } from '@/components/ui/InlineLink' +import Panel from '@/components/ui/Panel' +import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query' +import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query' +import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' +import type { DesiredInstanceSize } from '@/data/projects/new-project.constants' +import { BASE_PATH, PROVIDERS } from '@/lib/constants' interface RegionSelectorProps { form: UseFormReturn @@ -38,6 +41,18 @@ interface RegionSelectorProps { // I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render // ^ can try again next time +// Maps smart region group codes to the specific-region code prefixes they contain. +// Used to check whether an incident affecting specific regions also affects a smart region selection. +const SMART_REGION_PREFIXES: Record> = { + americas: ['us-', 'ca-', 'sa-'], + emea: ['eu-', 'me-', 'af-'], + apac: ['ap-'], +} + +function smartRegionMatchesSpecific(smartCode: string, specificCode: string): boolean { + return (SMART_REGION_PREFIXES[smartCode] ?? []).some((prefix) => specificCode.startsWith(prefix)) +} + // Map backend region names to user-friendly display names const getDisplayNameForSmartRegion = (name: string): string => { if (name === 'APAC') { @@ -56,6 +71,9 @@ export const RegionSelector = ({ const smartRegionEnabled = useFlag('enableSmartRegion') + const { data: statusData } = useIncidentStatusQuery() + const { incidents = [] } = statusData ?? {} + const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery( { cloudProvider }, { enabled: !smartRegionEnabled } @@ -114,150 +132,185 @@ export const RegionSelector = ({ return !!region.name && region.name === field.value }) + const affectingIncidents = incidents.filter((incident) => { + const affectedRegions = incident.cache?.affected_regions ?? [] + if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false + + // Specific region: direct code match + if (affectedRegions.includes(selectedRegion.code)) return true + + // Smart region: match if any affected region falls within the smart group + return affectedRegions.some((specificCode) => + smartRegionMatchesSpecific(selectedRegion.code, specificCode) + ) + }) + return ( - -

Select the region closest to your users for the best performance.

- {showNonProdFields && ( -
-

Only these regions are supported for local/staging projects:

-
    -
  • East US (North Virginia)
  • -
  • Central EU (Frankfurt)
  • -
  • Southeast Asia (Singapore)
  • -
-
- )} - - } - > - - - - {field.value !== undefined && ( -
- {selectedRegion?.code && ( - region icon - )} - - {selectedRegion?.name - ? getDisplayNameForSmartRegion(selectedRegion.name) - : field.value} - + <> + +

Select the region closest to your users for the best performance.

+ {showNonProdFields && ( +
+

Only these regions are supported for local/staging projects:

+
    +
  • East US (North Virginia)
  • +
  • Central EU (Frankfurt)
  • +
  • Southeast Asia (Singapore)
  • +
)} - - - - {smartRegionEnabled && ( - <> - - General regions - {smartRegions.map((value) => { - return ( - -
-
- region icon - - {getDisplayNameForSmartRegion(value.name)} + + } + > + + + + {field.value !== undefined && ( +
+ {selectedRegion?.code && ( + region icon + )} + + {selectedRegion?.name + ? getDisplayNameForSmartRegion(selectedRegion.name) + : field.value} + +
+ )} +
+
+ + {smartRegionEnabled && ( + <> + + General regions + {smartRegions.map((value) => { + return ( + +
+
+ region icon + + {getDisplayNameForSmartRegion(value.name)} + +
+ +
+ {recommendedSmartRegions.has(value.code) && ( + + Recommended + + )} +
+
+
+ ) + })} +
+ + + )} + + + Specific regions + {regionOptions.map((value) => { + return ( + :nth-child(2)]:w-full', + value.status !== undefined && '!pointer-events-auto' + )} + disabled={value.status !== undefined} + > +
+
+ region icon +
+ {value.name} + + {value.code}
+
-
- {recommendedSmartRegions.has(value.code) && ( - - Recommended + {recommendedSpecificRegions.has(value.code) && ( + + Recommended + + )} + {value.status !== undefined && value.status === 'capacity' && ( + + + + Unavailable - )} -
-
-
- ) - })} -
- - - )} - - - Specific regions - {regionOptions.map((value) => { - return ( - :nth-child(2)]:w-full', - value.status !== undefined && '!pointer-events-auto' - )} - disabled={value.status !== undefined} - > -
-
- region icon -
- {value.name} - - {value.code} - -
+ + + Temporarily unavailable due to this region being at capacity. + + + )}
+ + ) + })} + + + + - {recommendedSpecificRegions.has(value.code) && ( - - Recommended - - )} - {value.status !== undefined && value.status === 'capacity' && ( - - - - Unavailable - - - - Temporarily unavailable due to this region being at capacity. - - - )} -
-
- ) - })} -
-
-
- + {affectingIncidents.length > 0 && ( + + + We're currently investigating an issue that may impact projects in this + region. Follow updates on{' '} + + status.supabase.com + + . + + } + className="mt-3" + /> + + )} + ) }} /> diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts index acb5f11c609..4c7e687c97b 100644 --- a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts @@ -56,14 +56,14 @@ describe('shouldShowBanner', () => { ).toBe(true) }) - it('shows when affects_project_creation is true even with a region restriction', () => { + it('does not show when affects_project_creation is true but there is a region restriction', () => { expect( shouldShowBanner({ incidents: [usEast1AndCreation], hasProjects: false, userRegions: new Set(), }) - ).toBe(true) + ).toBe(false) }) }) @@ -221,7 +221,7 @@ describe('shouldShowBanner', () => { ).toBe(false) }) - it('still shows for affects_project_creation with no projects even when regions are unknown', () => { + it('does not show for affects_project_creation with no projects when there is a region restriction, even when regions are unknown', () => { expect( shouldShowBanner({ incidents: [usEast1AndCreation], @@ -229,7 +229,7 @@ describe('shouldShowBanner', () => { userRegions: new Set(), hasUnknownRegions: true, }) - ).toBe(true) + ).toBe(false) }) }) }) diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts index ccd3fa98275..b1423bfcc4e 100644 --- a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts @@ -30,7 +30,8 @@ export function shouldShowBanner({ const affectsProjectCreation = incident.cache?.affects_project_creation ?? false // Users with no projects only see the banner if the incident affects project creation - if (!hasProjects) return affectsProjectCreation + // and has no specific region targeting (inline notice in RegionSelector handles region-specific incidents) + if (!hasProjects) return affectsProjectCreation && affectedRegions.length === 0 // User has projects: if no region restriction, always show if (affectedRegions.length === 0) return true From 43bccc52a3ee91f7ecea8caa93dc7a5b1dc4e6c5 Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Tue, 3 Mar 2026 08:44:57 +0100 Subject: [PATCH 09/19] feat(etl): Add new flags per org (#43292) This PR adds new feature flags for controlling etl destinations visibility based on the org slug. --------- Co-authored-by: Joshen Lim --- .../DestinationForm/index.tsx | 10 +- .../DestinationPanel/DestinationPanel.tsx | 2 +- .../DestinationTypeSelection.tsx | 15 +- .../Replication/useIsETLPrivateAlpha.ts | 35 ++- .../layouts/DatabaseLayout/DatabaseLayout.tsx | 49 +--- .../DatabaseLayout/DatabaseMenu.utils.tsx | 222 +++++++----------- .../NavigationBar/NavigationBar.utils.tsx | 7 - 7 files changed, 127 insertions(+), 213 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index f9291d4db11..3e0c87901ec 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useFlag, useParams } from 'common' +import { useParams } from 'common' import { CreateAnalyticsBucketSheet } from 'components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' @@ -35,6 +35,10 @@ import { import { Button, DialogSectionSeparator, Form_Shadcn_, SheetFooter, SheetSection } from 'ui' import * as z from 'zod' +import { + useIsETLBigQueryPrivateAlpha, + useIsETLIcebergPrivateAlpha, +} from '../../useIsETLPrivateAlpha' import { DestinationType } from '../DestinationPanel.types' import { AdvancedSettings } from './AdvancedSettings' import { CREATE_NEW_NAMESPACE } from './DestinationForm.constants' @@ -75,8 +79,8 @@ export const DestinationForm = ({ const { ref: projectRef } = useParams() const { setRequestStatus } = usePipelineRequestStatus() - const etlEnableBigQuery = useFlag('etlEnableBigQuery') - const etlEnableIceberg = useFlag('etlEnableIceberg') + const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha() + const etlEnableIceberg = useIsETLIcebergPrivateAlpha() const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const [isFormInteracting, setIsFormInteracting] = useState(false) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index 8c2d69d6437..68e51584c9b 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -7,6 +7,7 @@ import { useEffect } from 'react' import { toast } from 'sonner' import { Button, + cn, DialogSectionSeparator, Sheet, SheetContent, @@ -14,7 +15,6 @@ import { SheetHeader, SheetSection, SheetTitle, - cn, } from 'ui' import { EnableReplicationCallout } from '../EnableReplicationCallout' diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index af2328c95e0..58d5350bc95 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -1,18 +1,17 @@ import { useFlag } from 'common' import { AnalyticsBucket, BigQuery, Database } from 'icons' import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs' -import { Badge, RadioGroupStacked, RadioGroupStackedItem, cn } from 'ui' +import { Badge, cn, RadioGroupStacked, RadioGroupStackedItem } from 'ui' import { useDestinationInformation } from '../useDestinationInformation' -import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha' +import { useIsETLBigQueryPrivateAlpha, useIsETLIcebergPrivateAlpha } from '../useIsETLPrivateAlpha' import { DestinationType } from './DestinationPanel.types' import { InlineLink } from '@/components/ui/InlineLink' export const DestinationTypeSelection = () => { - const enablePgReplicate = useIsETLPrivateAlpha() const unifiedReplication = useFlag('unifiedReplication') - const etlEnableBigQuery = useFlag('etlEnableBigQuery') - const etlEnableIceberg = useFlag('etlEnableIceberg') + const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha() + const etlEnableIceberg = useIsETLIcebergPrivateAlpha() const numberOfTypes = [unifiedReplication, etlEnableBigQuery, etlEnableIceberg].filter( Boolean @@ -52,8 +51,8 @@ export const DestinationTypeSelection = () => { value={destinationType} onValueChange={(value) => setDestinationType(value as DestinationType)} className={cn( - 'grid [&>button>div]:py-4', - numberOfTypes === 3 ? 'grid-cols-3' : numberOfTypes === 2 ? 'grid-cols-2' : 'grid-cols-1', + 'grid [&>button>div]:py-4 grid-cols-3', + numberOfTypes === 3 && !editMode ? 'grid-cols-3' : 'grid-cols-2', '[&>button:first-of-type]:rounded-none [&>button:last-of-type]:rounded-none', '[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg' )} @@ -121,7 +120,7 @@ export const DestinationTypeSelection = () => { )} - {destinationType !== 'Read Replica' && enablePgReplicate && ( + {destinationType !== 'Read Replica' && (

Replication is in alpha. Expect rapid changes and possible breaking updates.{' '} diff --git a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts index 04486c97f74..351d8095e56 100644 --- a/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts +++ b/apps/studio/components/interfaces/Database/Replication/useIsETLPrivateAlpha.ts @@ -2,18 +2,37 @@ import { useFlag } from 'common' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' /** - * Organization level opt in for ETL private alpha + * Organization level opt in for ETL private alpha, there's 2 flags we're using which controls + * the individual destination types. Access to the ETL UI (`useIsETLPrivateAlpha`) will just + * check if the org has access to at least one of the destination types */ -export const useIsETLPrivateAlpha = () => { +const useIsCurrentOrgInFlagList = (flag: string) => { + const flagValue = useFlag(flag) const { data: organization } = useSelectedOrganizationQuery() - const etlPrivateAlpha = useFlag('etlPrivateAlpha') - const privateAlphaOrgSlugs = - typeof etlPrivateAlpha === 'string' - ? (etlPrivateAlpha as string).split(',').map((x) => x.trim()) + const allowedOrgSlugs = + typeof flagValue === 'string' + ? (flagValue as string).split(',').map((x: string) => x.trim()) : [] - const etlShowForAllProjects = useFlag('etlPrivateAlphaOverride') + // [Joshen] Override for to enable for all organizations by setting the flag value as `all` + if (allowedOrgSlugs.includes('all')) return true - return etlShowForAllProjects || privateAlphaOrgSlugs.includes(organization?.slug ?? '') + // [Joshen] Otherwise fallback to checking against org slug + return allowedOrgSlugs.includes(organization?.slug ?? '') +} + +export const useIsETLBigQueryPrivateAlpha = () => { + return useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha') +} + +export const useIsETLIcebergPrivateAlpha = () => { + return useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha') +} + +export const useIsETLPrivateAlpha = () => { + const hasAccessToETLBigQuery = useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha') + const hasAccessToETLIceberg = useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha') + + return hasAccessToETLBigQuery || hasAccessToETLIceberg } diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index a7967289937..ae652ef847a 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -1,60 +1,21 @@ +import { ProductMenu } from 'components/ui/ProductMenu' +import { withAuth } from 'hooks/misc/withAuth' import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' -import { useIsColumnLevelPrivilegesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha' -import { ProductMenu } from 'components/ui/ProductMenu' -import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { withAuth } from 'hooks/misc/withAuth' import { ProjectLayout } from '../ProjectLayout' -import { generateDatabaseMenu } from './DatabaseMenu.utils' +import { useGenerateDatabaseMenu } from './DatabaseMenu.utils' export interface DatabaseLayoutProps { title?: string } const DatabaseProductMenu = () => { - const { data: project } = useSelectedProjectQuery() - const router = useRouter() const page = router.pathname.split('/')[4] + const menu = useGenerateDatabaseMenu() - const { data } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref }) - - const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined - const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined - const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled() - const enablePgReplicate = useIsETLPrivateAlpha() - - const { - databaseReplication: showPgReplicate, - databaseRoles: showRoles, - integrationsWrappers: showWrappers, - } = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers']) - - return ( - <> - - - ) + return } const DatabaseLayout = ({ children }: PropsWithChildren) => { diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx index 6939c2d6a05..e8a6a4bb1c0 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx @@ -1,163 +1,104 @@ +import { useParams } from 'common' +import type { + ProductMenuGroup, + ProductMenuGroupItem, +} from 'components/ui/ProductMenu/ProductMenu.types' +import { IS_PLATFORM } from 'lib/constants' import { ArrowUpRight } from 'lucide-react' -import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types' -import type { Project } from 'data/projects/project-detail-query' -import { IS_PLATFORM } from 'lib/constants' +import { useIsColumnLevelPrivilegesEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha' +import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' +import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' +import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +const ExternalLinkIcon = + +export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() -export const generateDatabaseMenu = ( - project?: Project, - flags?: { - pgNetExtensionExists: boolean - pitrEnabled: boolean - columnLevelPrivileges: boolean - showPgReplicate: boolean - enablePgReplicate: boolean - showRoles: boolean - showWrappers: boolean - } -): ProductMenuGroup[] => { - const ref = project?.ref ?? 'default' const { - pgNetExtensionExists, - pitrEnabled, - columnLevelPrivileges, - showPgReplicate, - enablePgReplicate, - showRoles, - showWrappers, - } = flags || {} + databaseReplication: showPgReplicate, + databaseRoles: showRoles, + integrationsWrappers: showWrappers, + } = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers']) + + const { data } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref }) + + const pgNetExtensionExists = (data ?? []).some((ext) => ext.name === 'pg_net') + const pitrEnabled = addons?.selected_addons.some((addon) => addon.type === 'pitr') ?? false + const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled() + const enablePgReplicate = useIsETLPrivateAlpha() + + const getDatabaseURL = (path: string) => `/project/${ref}/database/${path}` return [ { title: 'Database Management', items: [ - { - name: 'Schema Visualizer', - key: 'schemas', - url: `/project/${ref}/database/schemas`, - items: [], - }, - { name: 'Tables', key: 'tables', url: `/project/${ref}/database/tables`, items: [] }, - { - name: 'Functions', - key: 'functions', - url: `/project/${ref}/database/functions`, - items: [], - }, - { - name: 'Triggers', - key: 'triggers', - url: `/project/${ref}/database/triggers/data`, - items: [], - }, - { - name: 'Enumerated Types', - key: 'types', - url: `/project/${ref}/database/types`, - - items: [], - }, - { - name: 'Extensions', - key: 'extensions', - url: `/project/${ref}/database/extensions`, - items: [], - }, - { - name: 'Indexes', - key: 'indexes', - url: `/project/${ref}/database/indexes`, - items: [], - }, - { - name: 'Publications', - key: 'publications', - url: `/project/${ref}/database/publications`, - items: [], - }, + { name: 'Schema Visualizer', key: 'schemas', url: getDatabaseURL('schemas') }, + { name: 'Tables', key: 'tables', url: getDatabaseURL('tables') }, + { name: 'Functions', key: 'functions', url: getDatabaseURL('functions') }, + { name: 'Triggers', key: 'triggers', url: getDatabaseURL('triggers/data') }, + { name: 'Enumerated Types', key: 'types', url: getDatabaseURL('types') }, + { name: 'Extensions', key: 'extensions', url: getDatabaseURL('extensions') }, + { name: 'Indexes', key: 'indexes', url: getDatabaseURL('indexes') }, + { name: 'Publications', key: 'publications', url: getDatabaseURL('publications') }, ], }, { title: 'Configuration', items: [ - ...(showRoles - ? [{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] }] - : []), - ...(columnLevelPrivileges - ? [ - { - name: 'Column Privileges', - key: 'column-privileges', - url: `/project/${ref}/database/column-privileges`, - items: [], - }, - ] - : []), + showRoles && { name: 'Roles', key: 'roles', url: getDatabaseURL('roles') }, + columnLevelPrivileges && { + name: 'Column Privileges', + key: 'column-privileges', + url: getDatabaseURL('column-privileges'), + }, { name: 'Policies', key: 'policies', url: `/project/${ref}/auth/policies`, - rightIcon: , - items: [], + rightIcon: ExternalLinkIcon, }, - { name: 'Settings', key: 'settings', url: `/project/${ref}/database/settings`, items: [] }, - ], + { name: 'Settings', key: 'settings', url: getDatabaseURL('settings') }, + ].filter(Boolean) as ProductMenuGroupItem[], }, { title: 'Platform', items: [ - ...(IS_PLATFORM && showPgReplicate - ? [ - { - name: 'Replication', - key: 'replication', - url: `/project/${ref}/database/replication`, - label: enablePgReplicate ? 'New' : undefined, - items: [], - }, - ] - : []), - ...(IS_PLATFORM - ? [ - { - name: 'Backups', - key: 'backups', - url: pitrEnabled - ? `/project/${ref}/database/backups/pitr` - : `/project/${ref}/database/backups/scheduled`, - items: [], - }, - ] - : []), - { - name: 'Migrations', - key: 'migrations', - url: `/project/${ref}/database/migrations`, - items: [], + IS_PLATFORM && + showPgReplicate && { + name: 'Replication', + key: 'replication', + url: getDatabaseURL('replication'), + label: enablePgReplicate ? 'New' : undefined, + }, + IS_PLATFORM && { + name: 'Backups', + key: 'backups', + url: pitrEnabled ? getDatabaseURL('backups/pitr') : getDatabaseURL('backups/scheduled'), }, - ...(showWrappers - ? [ - { - name: 'Wrappers', - key: 'wrappers', - url: `/project/${ref}/integrations?category=wrapper`, - rightIcon: , - items: [], - }, - ] - : []), - ...(!!pgNetExtensionExists - ? [ - { - name: 'Webhooks', - key: 'hooks', - url: `/project/${ref}/integrations/webhooks/overview`, - rightIcon: , - items: [], - }, - ] - : []), - ], + { name: 'Migrations', key: 'migrations', url: getDatabaseURL('migrations') }, + showWrappers && { + name: 'Wrappers', + key: 'wrappers', + url: `/project/${ref}/integrations?category=wrapper`, + rightIcon: ExternalLinkIcon, + }, + pgNetExtensionExists && { + name: 'Webhooks', + key: 'hooks', + url: `/project/${ref}/integrations/webhooks/overview`, + rightIcon: ExternalLinkIcon, + }, + ].filter(Boolean) as ProductMenuGroupItem[], }, { title: 'Tools', @@ -166,22 +107,19 @@ export const generateDatabaseMenu = ( name: 'Security Advisor', key: 'security-advisor', url: `/project/${ref}/advisors/security`, - rightIcon: , - items: [], + rightIcon: ExternalLinkIcon, }, { name: 'Performance Advisor', key: 'performance-advisor', url: `/project/${ref}/advisors/performance`, - rightIcon: , - items: [], + rightIcon: ExternalLinkIcon, }, { name: 'Query Performance', key: 'query-performance', url: `/project/${ref}/observability/query-performance`, - rightIcon: , - items: [], + rightIcon: ExternalLinkIcon, }, ], }, diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 995e6915d87..30bebf679ab 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -1,6 +1,4 @@ import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar' -import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils' -import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils' import { generateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils' import type { Route } from 'components/ui/ui.types' import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor' @@ -54,9 +52,6 @@ export const generateProductRoutes = ( const realtimeEnabled = features?.realtime ?? true const authOverviewPageEnabled = features?.authOverviewPage ?? false - const databaseMenu = generateDatabaseMenu(project) - const authMenu = generateAuthMenu(ref as string) - return [ { key: 'database', @@ -70,7 +65,6 @@ export const generateProductRoutes = ( : isProjectActive ? `/project/${ref}/database/schemas` : `/project/${ref}/database/backups/scheduled`), - items: databaseMenu, }, ...(authEnabled ? [ @@ -86,7 +80,6 @@ export const generateProductRoutes = ( : authOverviewPageEnabled ? `/project/${ref}/auth/overview` : `/project/${ref}/auth/users`), - items: authMenu, }, ] : []), From 732180608a0390bfb9d718e4bffca41af8106059 Mon Sep 17 00:00:00 2001 From: Kevin Strong-Holte Date: Tue, 3 Mar 2026 01:41:18 -0800 Subject: [PATCH 10/19] Fix some typos; update passages to conform to style guide (#43254) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. **YES** ## What kind of change does this PR introduce? Docs update ## What is the current behavior? N/A ## What is the new behavior? N/A ## Additional context N/A --------- Co-authored-by: Chris Chinchilla Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../content/guides/realtime/broadcast.mdx | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 7fd48580d3e..9d740fcbd06 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -10,22 +10,22 @@ You can use Realtime Broadcast to send low-latency messages between users. Messa The way Broadcast works changes based on the channel you are using: -- From REST API will receive an HTTP request which then will be sent via WebSocket to connected clients -- From Client libraries we have an established WebSocket connection and we use that to send a message to the server which then will be sent via WebSocket to connected clients -- From Database we add a new entry to `realtime.messages` where we have logical replication set to listen for changes which then will be sent via WebSocket to connected clients +- **REST API**: Receives an HTTP request and then sends a message via WebSocket to connected clients +- **Client libraries**: Sends a message via WebSocket to the server, and then the server sends a message via WebSocket to connected clients +- **Database**: Adds a new entry to `realtime.messages` where a logical replication is set to listen for changes, and then sends a message via WebSocket to connected clients -The public flag (the last argument in `realtime.send(payload, event, topic, is_private))` only affects who can subscribe to the topic not who can read messages from the database. +The public flag (the last argument in `realtime.send(payload, event, topic, is_private)`) only affects who can subscribe to the topic not who can read messages from the database. -- Public (false) → Anyone can subscribe to that topic without authentication -- Private (true) → Only authenticated clients can subscribe to that topic +- Public (`false`) → Anyone can subscribe to that topic without authentication +- Private (`true`) → Only authenticated clients can subscribe to that topic -However, regardless of whether it's public or private, the Realtime service connects to your database as the authenticated Supabase Admin role. +Regardless if it's public or private, the Realtime service connects to your database as the authenticated Supabase Admin role. -For Authorization we insert a message and try to read it, and rollback the transaction to verify that the RLS policies set by the user are being respected by the user joining the channel, but this message isn't sent to the user. You can read more about it in the [Authorization docs](/docs/guides/realtime/authorization). +For Authorization, we insert a message and try to read it, and rollback the transaction to verify that the Row Level Security (RLS) policies set by the user are being respected by the user joining the channel, but this message isn't sent to the user. You can read more about it in [Authorization](/docs/guides/realtime/authorization). ## Subscribe to messages @@ -132,9 +132,9 @@ In most cases, you can get the correct key from [the Project's **Connect** dialo -### Receiving Broadcast messages +### Receive Broadcast messages -You can provide a callback for the `broadcast` channel to receive messages. This example will receive any `broadcast` messages that are sent to `test-channel`: +You can receive Broadcast messages by providing a callback to the channel. -The realtime.send function in the database includes a flag that determines whether the broadcast is private or public, and client channels also have the same configuration. For broadcasts to work correctly, these settings must match a public broadcast will only reach public channels, and a private broadcast will only reach private ones. +The `realtime.send()` function in the database includes a flag that determines whether the broadcast is private or public, and client channels also have the same configuration. For broadcasts to work correctly, these settings must match. A public broadcast only reaches public channels and a private broadcast only reaches private channels. -By default, all database broadcasts are private, meaning clients must authenticate to receive them. If the database sends a public message but the client subscribes to a private channel, the message won't be delivered since private channels only accept signed, authenticated messages. +By default, all database broadcasts are private, meaning clients must authenticate to receive them. If the database sends a public message but the client subscribes to a private channel, the message is not delivered because private channels only accept signed, authenticated messages. -It's a common use case to broadcast messages when a record is created, updated, or deleted. We provide a helper function specific to this use case, `realtime.broadcast_changes()`. For more details, check out the [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes) guide. +You can use the `realtime.broadcast_changes()` helper function to broadcast messages when a record is created, updated, or deleted. For more details, read [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes). ### Broadcast using the REST API @@ -685,7 +685,7 @@ You can pass configuration options while initializing the Supabase Client. > - You can confirm that the Realtime servers have received your message by setting Broadcast's `ack` config to `true`. + You can confirm that the Realtime servers have received your message by setting Broadcast's `ack` setting to `true`. {/* prettier-ignore */} ```js @@ -881,20 +881,20 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv ### How it works -Broadcast Changes allows you to trigger messages from your database. To achieve it Realtime is directly reading your WAL (Write Append Log) file using a publication against the `realtime.messages` table so whenever a new insert happens a message is sent to connected users. +Broadcast Changes allows you to trigger messages from your database. To achieve it, Realtime directly reads your Write-Ahead Log (WAL) file using a publication against the `realtime.messages` table. Whenever a new insert occurs, a message is sent to connected users. -It uses partitioned tables per day which allows the deletion your previous messages in a performant way by dropping the physical tables of this partitioned table. Tables older than 3 days old are deleted. +It uses partitioned tables per day, which allows performant deletion of your previous messages by dropping the physical tables of this partitioned table. Tables older than 3 days are deleted. -Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON packages. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data. +Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON payloads. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data. -The database broadcast feature provides two functions to help you send messages: +Broadcast Changes provides two functions to help you send messages: -- `realtime.send` will insert a message into realtime.messages without a specific format. -- `realtime.broadcast_changes` will insert a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes. +- `realtime.send()` inserts a message into `realtime.messages` without a specific format. +- `realtime.broadcast_changes()` inserts a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes. ### Broadcasting a message from your database -The `realtime.send` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change. +The `realtime.send()` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change. ```sql SELECT realtime.send ( @@ -909,7 +909,7 @@ SELECT realtime.send ( #### Setup realtime authorization -Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create an RLS (Row Level Security) policy: +Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create an RLS policy: ```sql CREATE POLICY "authenticated can receive broadcasts" @@ -920,13 +920,13 @@ USING ( true ); ``` -See the [Realtime Authorization](/docs/guides/realtime/authorization) docs to learn how to set up more specific policies. +Read [Realtime Authorization](/docs/guides/realtime/authorization) to learn how to set up more specific policies. #### Set up trigger function -First, set up a trigger function that uses `realtime.broadcast_changes` to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it. +First, set up a trigger function that uses the `realtime.broadcast_changes()` function to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it. -For this example use case, we want to have a topic with the name `topic:` to which we're going to broadcast events. +For this example, you're going broadcast events to a topic named `topic:`. ```sql CREATE OR REPLACE FUNCTION public.your_table_changes() @@ -948,7 +948,7 @@ END; $$ LANGUAGE plpgsql; ``` -Of note are the Postgres native trigger special variables used: +The Postgres native trigger special variables used are: - `TG_OP` - the operation that triggered the function - `TG_TABLE_NAME` - the table that caused the trigger @@ -1000,7 +1000,7 @@ Broadcast Replay enables **private** channels to access messages that were sent You can configure replay with the following options: -- **`since`** (Required): The epoch timestamp in milliseconds (e.g., `1697472000000`), specifying the earliest point from which messages should be retrieved. +- **`since`** (Required): The epoch timestamp in milliseconds (for example, `1697472000000`), specifying the earliest point from which messages should be retrieved. - **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25. Date: Tue, 3 Mar 2026 11:47:50 +0200 Subject: [PATCH 11/19] Blog: Add Timescale to pg_partman migration guide (#40037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Blog post on using pg_partman instead of TimescaleDB to prepare for the upcoming deprecation ## What is the current behavior? ## What is the new behavior? Blog post to include migration information for those using Timescale ## Additional context Not to be merged until pg_partman is released in 15 and 17 images ## Summary by CodeRabbit * **Documentation** * Added a comprehensive pg_partman guide covering setup, time- and integer-based partitioning, maintenance, automation, and resources. * Added a migration guide for moving from TimescaleDB hypertables to native PostgreSQL partitioning using pg_partman. * Updated TimescaleDB docs with migration notes and support guidance. * **New Features** * Listed pg_partman in the public extensions reference and added navigation entries linking to the pg_partman guide and migration guide. ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../NavigationMenu.constants.ts | 8 ++ .../guides/database/extensions/pg_partman.mdx | 95 +++++++++++++++ .../database/extensions/timescaledb.mdx | 4 + .../database/migrating-to-pg-partman.mdx | 113 ++++++++++++++++++ packages/shared-data/extensions.json | 9 ++ 5 files changed, 229 insertions(+) create mode 100644 apps/docs/content/guides/database/extensions/pg_partman.mdx create mode 100644 apps/docs/content/guides/database/migrating-to-pg-partman.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 6fca567215c..4cea890356d 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1049,6 +1049,10 @@ export const database: NavMenuConstant = { name: 'Partitioning your tables', url: '/guides/database/partitions' as `/${string}`, }, + { + name: 'Migrating to pg_partman', + url: '/guides/database/migrating-to-pg-partman' as `/${string}`, + }, { name: 'Managing connections', url: '/guides/database/connection-management' as `/${string}`, @@ -1249,6 +1253,10 @@ export const database: NavMenuConstant = { name: 'pg_net: Async Networking', url: '/guides/database/extensions/pg_net' as `/${string}`, }, + { + name: 'pg_partman: Partition management', + url: '/guides/database/extensions/pg_partman' as `/${string}`, + }, { name: 'pg_plan_filter: Restrict Total Cost', url: '/guides/database/extensions/pg_plan_filter' as `/${string}`, diff --git a/apps/docs/content/guides/database/extensions/pg_partman.mdx b/apps/docs/content/guides/database/extensions/pg_partman.mdx new file mode 100644 index 00000000000..de2db96c9c0 --- /dev/null +++ b/apps/docs/content/guides/database/extensions/pg_partman.mdx @@ -0,0 +1,95 @@ +--- +id: 'pg_partman' +title: 'pg_partman: partition management' +description: 'Automated partition management' +--- + +[`pg_partman`](https://github.com/pgpartman/pg_partman) is a Postgres extension that automates the creation and maintenance of partitions for tables using Postgres native partitioning. + +## Enable the extension + +To enable `pg_partman`, create a dedicated schema for it and enable the extension there. + +{/* prettier-ignore */} +```sql +create schema if not exists partman; +create extension if not exists pg_partman with schema partman; +``` + +## Create a partitioned table + +`pg_partman` requires your parent table to already be declared as a partitioned table. + +{/* prettier-ignore */} +```sql +create table public.messages ( + id bigint generated by default as identity, + sent_at timestamptz not null, + sender_id uuid, + recipient_id uuid, + body text, + primary key (sent_at, id) +) +partition by range (sent_at); +``` + +## Set up partitioning + +You configure the parent table using `partman.create_parent()`. The function takes an `ACCESS EXCLUSIVE` lock briefly while it creates the initial partitions. + +### Time-based partitions + +{/* prettier-ignore */} +```sql +select partman.create_parent( + p_parent_table := 'public.messages', + p_control := 'sent_at', + p_type := 'range', + p_interval := '7 days', + p_premake := 7, + p_start_partition := '2025-01-01 00:00:00' +); +``` + +### Integer-based partitions + +{/* prettier-ignore */} +```sql +create table public.events ( + id bigint generated by default as identity, + inserted_at timestamptz not null default now(), + payload jsonb, + primary key (id) +) +partition by range (id); + +select partman.create_parent( + p_parent_table := 'public.events', + p_control := 'id', + p_type := 'range', + p_interval := '100000' +); +``` + +## Running maintenance + +It’s important to call `pg_partman` maintenance regularly so future partitions are pre-created and retention policies are applied. + +{/* prettier-ignore */} +```sql +call partman.run_maintenance_proc(); +``` + +To automate this, schedule it using `pg_cron`. + +{/* prettier-ignore */} +```sql +create extension if not exists pg_cron; + +select + cron.schedule('@hourly', $$call partman.run_maintenance_proc()$$); +``` + +## Resources + +- Official [pg_partman documentation](https://github.com/pgpartman/pg_partman/blob/development/doc/pg_partman.md) diff --git a/apps/docs/content/guides/database/extensions/timescaledb.mdx b/apps/docs/content/guides/database/extensions/timescaledb.mdx index 8ca28081578..a3aa0303646 100644 --- a/apps/docs/content/guides/database/extensions/timescaledb.mdx +++ b/apps/docs/content/guides/database/extensions/timescaledb.mdx @@ -8,6 +8,10 @@ description: 'Scalable time-series data storage and analysis' The `timescaledb` extension is deprecated in projects using Postgres 17. It continues to be supported in projects using Postgres 15, but will need to dropped before those projects are upgraded to Postgres 17. See the [Upgrading to Postgres 17 notes](/docs/guides/platform/upgrading#upgrading-to-postgres-17) for more information. +If you are using hypertables, follow the [migration guide](/docs/guides/database/migrating-to-pg-partman) to convert to native partitioning managed by `pg_partman`. + +For additional support, contact our Success team by creating a support ticket in the Supabase Dashboard. + [`timescaledb`](https://docs.timescale.com/timescaledb/latest/) is a Postgres extension designed for improved handling of time-series data. It provides a scalable, high-performance solution for storing and querying time-series data on top of a standard Postgres database. diff --git a/apps/docs/content/guides/database/migrating-to-pg-partman.mdx b/apps/docs/content/guides/database/migrating-to-pg-partman.mdx new file mode 100644 index 00000000000..55243113dcd --- /dev/null +++ b/apps/docs/content/guides/database/migrating-to-pg-partman.mdx @@ -0,0 +1,113 @@ +--- +id: 'migrating-from-timescaledb-to-pg-partman' +title: 'Migrate from TimescaleDB to pg_partman' +description: 'Convert TimescaleDB hypertables to Postgres native partitions managed by pg_partman.' +--- + +Starting from Postgres 17, Supabase projects do not have the `timescaledb` extension available. If your project relies on TimescaleDB hypertables, you will need to migrate to standard Postgres tables before upgrading. + +This guide shows one approach to migrate a hypertable to a native Postgres partitioned table and optionally configure `pg_partman` to automate ongoing partition maintenance. +The approach outlined in this guide can also be used for traditional partitioned tables. + +## Before you begin + +- Test the migration path in a staging environment (for example by creating a copy of your production project or using branching). +- Review your application for TimescaleDB-specific SQL usage (for example `time_bucket()`, compression policies). Those features are not provided by `pg_partman`. + +## Migration overview + +1. Create a new partitioned table. +2. Copy data from the hypertable to the new table. +3. Swap over and drop the hypertable. +4. Configure `pg_partman` (optional) and schedule maintenance. + +## Example: Migrate `messages` from hypertable to native partitions + +This example assumes a `messages` hypertable partitioned by `sent_at`. + +### 1. Rename the existing hypertable + +This keeps the original data in place while you create a new partitioned table with the original name. + +{/* prettier-ignore */} +```sql +alter table public.messages rename to ht_messages; +``` + +### 2. Create a new partitioned table + +When using native partitioning, the partitioning column must be included in any unique index (including the primary key). + +{/* prettier-ignore */} +```sql +create table public.messages ( + like public.ht_messages including all, + primary key (sent_at, id) +) +partition by range (sent_at); +``` + +### 3. Copy data into the new table + +For large tables, consider copying in batches (for example by time range) during a maintenance window. + +{/* prettier-ignore */} +```sql +insert into public.messages +select * +from public.ht_messages; +``` + +### 4. Drop the old hypertable (and TimescaleDB) + +Only drop the extension once you’ve migrated all hypertables and no other objects depend on it. + +{/* prettier-ignore */} +```sql +drop table public.ht_messages; + +drop extension if exists timescaledb; +``` + +### 5. Configure `pg_partman` (optional) + +Enable `pg_partman` and register your table so partitions are created ahead of time. + +{/* prettier-ignore */} +```sql +create schema if not exists partman; +create extension if not exists pg_partman with schema partman; + +select partman.create_parent( + p_parent_table := 'public.messages', + p_control := 'sent_at', + p_type := 'range', + p_interval := '7 days', + p_premake := 7, + p_start_partition := '2025-01-01 00:00:00' +); +``` + +## Keep partitions up to date + +`pg_partman` requires running maintenance to pre-make partitions and apply retention policies. + +{/* prettier-ignore */} +```sql +call partman.run_maintenance_proc(); +``` + +To automate this, schedule it with `pg_cron`. + +{/* prettier-ignore */} +```sql +create extension if not exists pg_cron; + +select cron.schedule('@daily', $$call partman.run_maintenance_proc()$$); +``` + +## Additional resources + +- [Partitioning your tables](/docs/guides/database/partitions). +- [`pg_partman` documentation](/docs/guides/database/extensions/pg_partman) +- [`pg_partman` migration guides](https://github.com/pgpartman/pg_partman/blob/development/doc/migrate_to_partman.md) diff --git a/packages/shared-data/extensions.json b/packages/shared-data/extensions.json index bbb5378f482..b937f861509 100644 --- a/packages/shared-data/extensions.json +++ b/packages/shared-data/extensions.json @@ -278,6 +278,15 @@ "product": "Database Webhooks", "product_url": "/project/{ref}/integrations/webhooks" }, + { + "name": "pg_partman", + "comment": "Automated partition management", + "tags": ["Utility"], + "link": "/guides/database/extensions/pg_partman", + "github_url": "https://github.com/pgpartman/pg_partman", + "product": null, + "product_url": null + }, { "name": "pg_prewarm", "comment": "prewarm relation data", From efdf6d188b657be6d640d09da7cb4ff405cc8ede Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 3 Mar 2026 18:10:01 +0800 Subject: [PATCH 12/19] Fix auth hooks, enabled state in panel doesnt match hook state if disabled (#43326) ## Context If an auth hook is disabled, clicking "Configure hook" will incorrectly show the enable toggle as true image image Was due to the `enabled` value being default to `true` in the `useEffect` ## To test - Just verify that the enable toggle in the panel matches the auth hook's enabled state --- .../interfaces/Auth/Hooks/CreateHookSheet.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index 9c51aa717b5..3598ebd0f2d 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -1,10 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import randomBytes from 'randombytes' -import { useEffect, useMemo } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { useParams } from 'common' import { convertArgumentTypes } from 'components/interfaces/Database/Functions/Functions.utils' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' @@ -16,11 +10,15 @@ import { useAuthHooksUpdateMutation } from 'data/auth/auth-hooks-update-mutation import { executeSql } from 'data/sql/execute-sql-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import randomBytes from 'randombytes' +import { useEffect, useMemo } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { Button, + Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, - Form_Shadcn_, Input_Shadcn_, RadioGroupStacked, RadioGroupStackedItem, @@ -35,7 +33,9 @@ import { } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' -import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants' +import * as z from 'zod' + +import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constants' import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils' interface CreateHookSheetProps { @@ -239,7 +239,7 @@ export const CreateHookSheet = ({ form.reset({ hookType: definition.title, - enabled: authConfig?.[definition.enabledKey] || true, + enabled: isCreating ? true : authConfig?.[definition.enabledKey], selectedType: values.type, httpsValues: { url: (values.type === 'https' && values.url) || '', From f363d69bc37e22763aaa3d1a2e676e57f2d6cf90 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 3 Mar 2026 18:14:34 +0800 Subject: [PATCH 13/19] Persist dismissal of table filter bar banner if already enabled or enabling feature preview (#43323) ## Context Persists the dismissal of the table filter banner promo if - Already enabled - After enabling feature preview image --- .../App/FeaturePreview/FeaturePreviewModal.tsx | 13 +++++++++++++ .../layouts/TableEditorLayout/TableEditorLayout.tsx | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 106f6ea8d15..2c4c5f7a2ba 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -29,6 +29,7 @@ import { QueueOperationsPreview } from './QueueOperationsPreview' import { TableFilterBarPreview } from './TableFilterBarPreview' import { UnifiedLogsPreview } from './UnifiedLogsPreview' import { useFeaturePreviews } from './useFeaturePreviews' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' const FEATURE_PREVIEW_KEY_TO_CONTENT: { [key: string]: ReactNode @@ -56,6 +57,11 @@ export const FeaturePreviewModal = () => { const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() + const [isDismissedTableFilterBar, setIsDismissedTableFilterBar] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), + false + ) + const { flags, onUpdateFlag } = featurePreviewContext const selectedFeature = featurePreviews.find((preview) => preview.key === selectedFeatureKey) ?? featurePreviews[0] @@ -72,6 +78,13 @@ export const FeaturePreviewModal = () => { properties: { feature: selectedFeature.key }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) + + if ( + selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR && + !isDismissedTableFilterBar + ) { + setIsDismissedTableFilterBar(true) + } } return ( diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index d2b622044a4..3ed0b57989c 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -6,6 +6,7 @@ import { PropsWithChildren, useEffect } from 'react' import { ProjectLayoutWithAuth } from '../ProjectLayout' import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar' +import { useIsTableFilterBarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { BannerTableEditorFilter } from '@/components/ui/BannerStack/Banners/BannerTableEditorFilter' import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' @@ -15,6 +16,7 @@ const TABLE_EDITOR_NEW_FILTER_BANNER_ID = 'table-editor-new-filter-banner' export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { const { ref } = useParams() const { addBanner, dismissBanner } = useBannerStack() + const isTableFilterBarEnabled = useIsTableFilterBarEnabled() const [isTableEditorNewFilterBannerDismissed] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), @@ -29,7 +31,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { if (!isPermissionsLoaded) return - if (canReadTables && !isTableEditorNewFilterBannerDismissed) { + if (canReadTables && !isTableEditorNewFilterBannerDismissed && !isTableFilterBarEnabled) { addBanner({ id: TABLE_EDITOR_NEW_FILTER_BANNER_ID, priority: 2, @@ -49,6 +51,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { canReadTables, isPermissionsLoaded, isTableEditorNewFilterBannerDismissed, + isTableFilterBarEnabled, ]) if (isPermissionsLoaded && !canReadTables) { From 92d7d20be1892407e1428d630a8e50586a2317b2 Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Tue, 3 Mar 2026 11:32:10 +0100 Subject: [PATCH 14/19] docs: add information about storage in oauth app scope documentation (#43330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES Recreation of https://github.com/supabase/supabase/pull/39706 as I am unable to resolve the merge conflict in that PR. --------- Co-authored-by: Emil Wåreus --- .../build-a-supabase-oauth-integration/oauth-scopes.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx index ef13b111f2c..2e749fc4da2 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx @@ -39,3 +39,5 @@ You can update scopes of your OAuth app at any time, but existing OAuth app user | `Rest` | `Write` | Update a project's PostgREST configuration | | `Secrets` | `Read` | Retrieve a project's API keys
Retrieve a project's secrets
Retrieve a project's pgsodium config | | `Secrets` | `Write` | Create or update a project's secrets
Update a project's pgsodium configuration | +| `Storage` | `Read` | Retrieve a project's storage buckets. | +| | From 589cdae7e5fb38218a6bcc3592f777162518d64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 3 Mar 2026 19:10:23 +0800 Subject: [PATCH 15/19] chore: log drains on Pro Plan (#42435) --- .../platform/manage-your-usage/log-drains.mdx | 2 +- .../content/guides/telemetry/log-drains.mdx | 2 +- .../PausedState/ProjectPausedState.tsx | 30 +++++++++---------- packages/shared-data/plans.ts | 2 +- packages/shared-data/pricing.ts | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx index dac00f80c61..fef7ebb55fc 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx @@ -36,7 +36,7 @@ Usage is shown as "Log Drain Hours" on your invoice. ### Pricing -Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs per hour ( per month). +Log Drains are available as a project Add-On for all Pro, Team and Enterprise users. Each Log Drain costs per hour ( per month). ## Log Drain Events diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx index 9bf926f643d..9b5b8adfdd0 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/telemetry/log-drains.mdx @@ -4,7 +4,7 @@ title: 'Log Drains' description: 'Getting started with Supabase Log Drains' --- -Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains). +Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains). You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top). diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index f1c0c2f3940..9899cd98fd2 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -236,23 +236,9 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { {isPauseStatusSuccess && !isRestoreDisabled && ( - {isFreePlan ? ( - - ) : ( - - )} - { > Resume project + + {isFreePlan ? ( + + ) : ( + + )} )} diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index 0b906209837..b32860d63c5 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -62,6 +62,7 @@ export const plans: PricingInformation[] = [ 'Email support', 'Daily backups stored for 7 days', '7-day log retention', + ['Add Log Drains', 'additional $60 per drain, per project'], ], preface: 'Everything in the Free Plan, plus:', cta: 'Get Started', @@ -85,7 +86,6 @@ export const plans: PricingInformation[] = [ 'Priority email support & SLAs', 'Daily backups stored for 14 days', '28-day log retention', - ['Add Log Drains', 'additional $60 per drain, per project'], ], preface: 'Everything in the Pro Plan, plus:', cta: 'Get Started', diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 495798f6e38..f6ff197a0cb 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -590,7 +590,7 @@ export const pricing: Pricing = { title: 'Log Drain', plans: { free: false, - pro: false, + pro: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], team: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], enterprise: 'Custom', }, From bb10862e074cdb918e203537348ac6249d383b94 Mon Sep 17 00:00:00 2001 From: Illia Basalaiev <44750366+Ellba@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:18:01 +0100 Subject: [PATCH 16/19] chore: update jwt expiration link in the user sessions docs (#43338) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update ## What is the current behavior? This can be customized in your project's Auth settings in the Advanced Settings section. ## What is the new behavior? Feel free to include screenshots if it includes visual changes. This can be customized in your project's settings in the JWT Keys > Legacy JWT Secret section. - /docs/guides/auth/sessions --- apps/docs/content/guides/auth/sessions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/sessions.mdx b/apps/docs/content/guides/auth/sessions.mdx index 1b79d9f8d66..c7eac35d2bf 100644 --- a/apps/docs/content/guides/auth/sessions.mdx +++ b/apps/docs/content/guides/auth/sessions.mdx @@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th ### What are recommended values for access token (JWT) expiration? -Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/jwt) in the Advanced Settings section. +Most applications should use the default expiration time of 1 hour. This can be customized in your [project's settings](/dashboard/project/_/settings/jwt/legacy) in the JWT Keys > Legacy JWT Secret section. Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations. From 911eb03498e813c9f13555444cd9440974b4fe33 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:20:42 +0100 Subject: [PATCH 17/19] fix: banning / unbanning a user does not update user pane ban status (#43332) --- apps/studio/data/auth/user-update-mutation.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/studio/data/auth/user-update-mutation.ts b/apps/studio/data/auth/user-update-mutation.ts index 986a2416a1e..2c50c824855 100644 --- a/apps/studio/data/auth/user-update-mutation.ts +++ b/apps/studio/data/auth/user-update-mutation.ts @@ -1,8 +1,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - import { handleError, patch } from 'data/fetchers' +import { toast } from 'sonner' import type { ResponseError, UseCustomMutationOptions } from 'types' + import { authKeys } from './keys' export type UserUpdateVariables = { @@ -37,8 +37,11 @@ export const useUserUpdateMutation = ({ return useMutation({ mutationFn: (vars) => updateUser(vars), async onSuccess(data, variables, context) { - const { projectRef } = variables - await queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }) + const { projectRef, userId } = variables + await Promise.all([ + queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }), + queryClient.invalidateQueries({ queryKey: authKeys.user(projectRef, userId) }), + ]) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { From 997999779f312f184279633a97ed218c8df53ac1 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:04:35 +0100 Subject: [PATCH 18/19] add caddy and nginx configs for self-hosted --- docker/.env.example | 19 +++ docker/docker-compose.caddy.yml | 34 ++++++ docker/docker-compose.nginx.yml | 33 ++++++ docker/volumes/proxy/caddy/Caddyfile | 36 ++++++ .../proxy/nginx/supabase-nginx.conf.tpl | 109 ++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 docker/docker-compose.caddy.yml create mode 100644 docker/docker-compose.nginx.yml create mode 100644 docker/volumes/proxy/caddy/Caddyfile create mode 100644 docker/volumes/proxy/nginx/supabase-nginx.conf.tpl diff --git a/docker/.env.example b/docker/.env.example index 5fcff319982..3e1e2dc35e6 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -217,3 +217,22 @@ KONG_HTTPS_PORT=8443 # Enable webp support IMGPROXY_ENABLE_WEBP_DETECTION=true + + +############ +# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt +############ + +# Documentation: +# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https + +# Usage: +# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d +# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d + +# Domain name for the proxy (must point to your server) +PROXY_DOMAIN=your-domain.example.com + +# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN). +# This should be a valid email, not a placehoder (otherwise Certbot may fail to start). +CERTBOT_EMAIL=admin@example.com diff --git a/docker/docker-compose.caddy.yml b/docker/docker-compose.caddy.yml new file mode 100644 index 00000000000..b31ee8db586 --- /dev/null +++ b/docker/docker-compose.caddy.yml @@ -0,0 +1,34 @@ +services: + + kong: + ports: !reset [] + + caddy: + container_name: supabase-caddy + image: caddy:2 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/sh + - -c + - | + PROXY_AUTH_PASSWORD=$$(caddy hash-password --plaintext "$$PROXY_AUTH_PASSWORD") && \ + caddy run --config /etc/caddy/Caddyfile --adapter caddyfile + volumes: + - ./volumes/proxy/caddy:/etc/caddy + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: diff --git a/docker/docker-compose.nginx.yml b/docker/docker-compose.nginx.yml new file mode 100644 index 00000000000..e693a86aee1 --- /dev/null +++ b/docker/docker-compose.nginx.yml @@ -0,0 +1,33 @@ +services: + + kong: + ports: !reset [] + + nginx: + container_name: supabase-nginx + image: jonasal/nginx-certbot:6.0.1-nginx1.29.5 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/bash + - -c + - | + printf '%s:%s\n' "$${PROXY_AUTH_USERNAME}" "$$(openssl passwd -apr1 "$${PROXY_AUTH_PASSWORD}")" > /etc/nginx/user_conf.d/dashboard-passwd && \ + envsubst '$${PROXY_DOMAIN}' < /etc/nginx/supabase-nginx.conf.tpl > /etc/nginx/user_conf.d/nginx.conf && \ + /scripts/start_nginx_certbot.sh + volumes: + - ./volumes/proxy/nginx/supabase-nginx.conf.tpl:/etc/nginx/supabase-nginx.conf.tpl:ro + - nginx_letsencrypt:/etc/letsencrypt + +volumes: + nginx_letsencrypt: diff --git a/docker/volumes/proxy/caddy/Caddyfile b/docker/volumes/proxy/caddy/Caddyfile new file mode 100644 index 00000000000..d76c3ed36f3 --- /dev/null +++ b/docker/volumes/proxy/caddy/Caddyfile @@ -0,0 +1,36 @@ +{$PROXY_DOMAIN} { + @supabase_api path /auth/v1/* /rest/v1/* /graphql/v1/* /realtime/v1/* /functions/v1/* /mcp + + handle @supabase_api { + reverse_proxy kong:8000 + } + + handle_path /storage/v1/* { + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + @cors_preflight method OPTIONS + handle @cors_preflight { + header Access-Control-Allow-Origin * + header Access-Control-Allow-Methods "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS" + header Access-Control-Allow-Headers * + respond 204 + } + + header Access-Control-Allow-Origin * + + reverse_proxy storage:5000 { + # Required for TUS resumable upload Location headers and S3 signature verification. + header_up X-Forwarded-Prefix /{http.request.orig_uri.path.0}/{http.request.orig_uri.path.1} + } + } + + handle { + basic_auth { + # PROXY_AUTH_PASSWORD is overwritten with the bcrypt on startup + {$PROXY_AUTH_USERNAME} {$PROXY_AUTH_PASSWORD} + } + + reverse_proxy studio:3000 + } + + header -server +} diff --git a/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl new file mode 100644 index 00000000000..f50f23653c0 --- /dev/null +++ b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl @@ -0,0 +1,109 @@ +upstream kong_upstream { + server kong:8000; + keepalive 2; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name ${PROXY_DOMAIN}; + server_tokens off; + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + ssl_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${PROXY_DOMAIN}/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/chain.pem; + + ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Prevent 502 errors from large Supabase auth cookies + large_client_header_buffers 4 16k; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + location / { + auth_basic "supabase"; + auth_basic_user_file /etc/nginx/user_conf.d/dashboard-passwd; + + proxy_pass http://studio:3000; + } + + location /auth { + proxy_pass http://kong_upstream; + } + + location /rest { + proxy_pass http://kong_upstream; + } + + location /realtime/v1/ { + proxy_pass http://kong_upstream; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 3600s; + } + + location /storage/v1/ { + proxy_pass http://storage:5000/; + proxy_buffering off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + # Required for TUS resumable upload Location headers and S3 signature verification. + proxy_set_header X-Forwarded-Prefix /storage/v1; + + client_max_body_size 0; + + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' '*'; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + } + + location /functions { + proxy_pass http://kong_upstream; + } + + location /graphql { + proxy_pass http://kong_upstream; + } + + location /mcp { + proxy_pass http://kong_upstream; + } +} From bdab64e8442ed55a7e4b9f7307ce910cc0254ff1 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:57:45 +0100 Subject: [PATCH 19/19] chore(docs): add new words to rule003 (#43344) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Add a couple of technical terms, and a handful of common brand and products names to simplify docs submissions. --- supa-mdx-lint/Rule003Spelling.toml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index da1a0cc3c44..7d7bdf8dd6e 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -51,6 +51,7 @@ allow_list = [ "[Dd]ata[Ff]rames?", "[Dd]atasets?", "[Dd]atasources?", + "[Dd]atetime", "[Dd]e facto", "[Dd]enylists?", "[Dd]evs?", @@ -103,6 +104,7 @@ allow_list = [ "[Oo]vercommit(s|ted|ting)?", "[Pp]arallelization", "[Pp]arams?", + "[Pp]assthrough", "[Pp]laintext", "[Pp]olyfill(s|ed)?", "[Pp]oolers?", @@ -137,6 +139,7 @@ allow_list = [ "[Tt]radeoffs?", "[Tt]unneled", "UncaughtException", + "[Uu]ncomment(ing|ed)?", "[Uu]nlink(ing|s|ed)?", "[Uu]pserts?", "[Uu]ptime", @@ -161,8 +164,10 @@ allow_list = [ "BotFather", "Brevo", "bytea", + "[Cc]addy", "CAPTCHA", "Cartes Bancaires", + "[Cc]ertbot", "ChatGPT", "Citus", "ClickHouse", @@ -210,6 +215,7 @@ allow_list = [ "GraphQL", "Groonga", "HackerOne", + "[Hh][Aa][Pp]roxy", "HashiCorp", "Heroku", "Homebrew", @@ -218,6 +224,7 @@ allow_list = [ "HypoPG", "IdP", "ImageMagick", + "imgproxy", "Inbucket", "Inferencer", "Infisical", @@ -249,6 +256,7 @@ allow_list = [ "Mailtrap", "Mansueli", "Metabase", + "[Mm]in[Ii][Oo]", "Mixpeek", "Multiplatform", "MySQL", @@ -256,6 +264,7 @@ allow_list = [ "Nano", "Netlify", "Next.js", + "[Nn]ginx", "NoSQL", "Node.js", "Nuxt", @@ -297,6 +306,7 @@ allow_list = [ "PrivateLink", "PyIceberg", "Qodo", + "rclone", "README", "Redis", "RedwoodJS", @@ -313,15 +323,17 @@ allow_list = [ "SecureStore", "SendGrid", "Session Timebox", + "Snapchat", "Snaplet", "Solana", "SolidJS", "Spotify", "Sqitch", + "Supabase", "Supavisor", "SvelteKit", "SwiftUI", - "Supabase", + "Reddit", "Remapper", "TablePlus", "TextLocal", @@ -329,6 +341,7 @@ allow_list = [ "TimescaleDB", "tmux", "TooManyChannels", + "[Tt]raefik", "Transformers.js", "tsquery", "Twilio",