From daa3119b2f5a03fc40bb0a1e2c8f17e74d9aadb1 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 5 May 2026 23:52:11 +1000 Subject: [PATCH 01/10] chore(studio): align sidebar hover states (#45569) ## What kind of change does this PR introduce? UI polish. Updates sidebar and submenu navigation hover and active styling. ## What is the current behavior? Product submenu navigation items either lack a hover fill or use a hover fill that visually matches the active state. Adjacent hovered and selected rows can appear to touch. ## What is the new behavior? Primary sidebar buttons, sidebar sub-buttons, and product submenu pills now share a muted hover fill while preserving the full accent fill for active/selected states. Product submenu rows also get a small visual gap with slightly reduced vertical padding to keep the overall spacing compact. | After | | --- | | CleanShot 2026-05-05 at 11 53
05@2x | ## Summary by CodeRabbit * **Style** * Refined sidebar hover/active states with subtle accent alpha colors for a more polished visual experience. * Updated sidebar menu spacing and rounded corners for improved touch and visual clarity. * **UI Improvements** * Sidebar now only displays when sections exist and uses a streamlined submenu flow for more consistent, predictable navigation. --------- Co-authored-by: Joshen Lim --- .../layouts/AccountLayout/WithSidebar.tsx | 108 ++---------------- .../OrganizationSettingsLayout.tsx | 2 - apps/studio/pages/org/[slug]/general.tsx | 2 +- .../pages/org/[slug]/private-apps/index.tsx | 4 +- apps/studio/pages/org/[slug]/security.tsx | 2 +- apps/studio/pages/org/[slug]/sso.tsx | 4 +- .../org/[slug]/webhooks/[endpointId].tsx | 4 +- .../pages/org/[slug]/webhooks/index.tsx | 4 +- .../ui/src/components/shadcn/ui/sidebar.tsx | 12 +- packages/ui/src/lib/theme/defaultTheme.ts | 3 +- 10 files changed, 28 insertions(+), 117 deletions(-) diff --git a/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx b/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx index bbb1170e27..b261b149af 100644 --- a/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx +++ b/apps/studio/components/layouts/AccountLayout/WithSidebar.tsx @@ -1,18 +1,16 @@ import { ArrowLeft } from 'lucide-react' import Link from 'next/link' import { PropsWithChildren, ReactNode } from 'react' -import { cn, Menu } from 'ui' +import { cn } from 'ui' import type { SidebarSection } from './AccountLayout.types' +import { getActiveKey, toSubMenuSections } from './AccountLayout.utils' +import { SubMenu } from '@/components/ui/ProductMenu/SubMenu' interface WithSidebarProps { title?: string sections: SidebarSection[] header?: ReactNode - subitems?: any[] - subitemsParentKey?: number - hideSidebar?: boolean - customSidebarContent?: ReactNode backToDashboardURL?: string } @@ -21,24 +19,17 @@ export const WithSidebar = ({ header, children, sections, - subitems, - subitemsParentKey, - hideSidebar = false, - customSidebarContent, backToDashboardURL, }: PropsWithChildren) => { - const noContent = !sections && !customSidebarContent + const noContent = !sections return (
- {!hideSidebar && !noContent && ( + {!noContent && ( @@ -53,12 +44,12 @@ export const WithSidebar = ({ export const SidebarContent = ({ header, sections, - subitems, - subitemsParentKey, - customSidebarContent, backToDashboardURL, className, }: PropsWithChildren> & { className?: string }) => { + const page = getActiveKey(sections) + const subMenuSections = toSubMenuSections(sections) + return ( <>
-
- - {customSidebarContent} - {sections.map((section, idx) => ( -
- {Boolean(section.heading) ? ( - - ) : ( -
-
- {section.links.map((link, i: number) => { - const isActive = link.isActive && !subitems - return ( - - -
-
- {link.label} -
-
- -
- ) - })} -
-
- )} - {idx !== sections.length - 1 && ( -
- )} -
- ))} -
+
+
@@ -135,41 +85,3 @@ export const SidebarContent = ({ ) } - -interface SectionWithHeadersProps { - section: SidebarSection - subitems?: any[] - subitemsParentKey?: number -} - -const SectionWithHeaders = ({ section, subitems }: SectionWithHeadersProps) => ( -
-
- {section.heading && ( - - {section.heading} -
- } - /> - )} -
- {section.links.map((link, i: number) => { - const isActive = link.isActive && !subitems - return ( - - -
-
- {link.label} -
-
- -
- ) - })} -
-
-
-) diff --git a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx index e12a7fc7a7..3d09a22dd2 100644 --- a/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx @@ -236,5 +236,3 @@ export function OrganizationSettingsLayout({ children }: PropsWithChildren) { ) } - -export default OrganizationSettingsLayout diff --git a/apps/studio/pages/org/[slug]/general.tsx b/apps/studio/pages/org/[slug]/general.tsx index 79c107e758..a5170df5af 100644 --- a/apps/studio/pages/org/[slug]/general.tsx +++ b/apps/studio/pages/org/[slug]/general.tsx @@ -11,7 +11,7 @@ import { import { GeneralSettings } from '@/components/interfaces/Organization/GeneralSettings/GeneralSettings' import DefaultLayout from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import { usePermissionsQuery } from '@/data/permissions/permissions-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import type { NextPageWithLayout } from '@/types' diff --git a/apps/studio/pages/org/[slug]/private-apps/index.tsx b/apps/studio/pages/org/[slug]/private-apps/index.tsx index b39a8994f5..c730c0b066 100644 --- a/apps/studio/pages/org/[slug]/private-apps/index.tsx +++ b/apps/studio/pages/org/[slug]/private-apps/index.tsx @@ -25,9 +25,9 @@ import { PrivateAppsProvider, usePrivateApps, } from '@/components/interfaces/Organization/PrivateApps/PrivateAppsContext' -import DefaultLayout from '@/components/layouts/DefaultLayout' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import type { NextPageWithLayout } from '@/types' function PrivateAppsContent() { diff --git a/apps/studio/pages/org/[slug]/security.tsx b/apps/studio/pages/org/[slug]/security.tsx index 18988da695..aa6f36dc5e 100644 --- a/apps/studio/pages/org/[slug]/security.tsx +++ b/apps/studio/pages/org/[slug]/security.tsx @@ -10,7 +10,7 @@ import { import { SecuritySettings } from '@/components/interfaces/Organization/SecuritySettings' import DefaultLayout from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import { UnknownInterface } from '@/components/ui/UnknownInterface' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from '@/types' diff --git a/apps/studio/pages/org/[slug]/sso.tsx b/apps/studio/pages/org/[slug]/sso.tsx index 4bccfcddb7..baabd7c732 100644 --- a/apps/studio/pages/org/[slug]/sso.tsx +++ b/apps/studio/pages/org/[slug]/sso.tsx @@ -8,9 +8,9 @@ import { } from 'ui-patterns/PageHeader' import { SSOConfig } from '@/components/interfaces/Organization/SSO/SSOConfig' -import DefaultLayout from '@/components/layouts/DefaultLayout' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import { UnknownInterface } from '@/components/ui/UnknownInterface' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from '@/types' diff --git a/apps/studio/pages/org/[slug]/webhooks/[endpointId].tsx b/apps/studio/pages/org/[slug]/webhooks/[endpointId].tsx index a26985f774..226196d8fd 100644 --- a/apps/studio/pages/org/[slug]/webhooks/[endpointId].tsx +++ b/apps/studio/pages/org/[slug]/webhooks/[endpointId].tsx @@ -1,9 +1,9 @@ import { useRouter } from 'next/router' import { PlatformWebhooksPage } from '@/components/interfaces/Platform/Webhooks' -import DefaultLayout from '@/components/layouts/DefaultLayout' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import type { NextPageWithLayout } from '@/types' const OrgWebhookEndpointSettings: NextPageWithLayout = () => { diff --git a/apps/studio/pages/org/[slug]/webhooks/index.tsx b/apps/studio/pages/org/[slug]/webhooks/index.tsx index 3c2eee60a1..ba4e353494 100644 --- a/apps/studio/pages/org/[slug]/webhooks/index.tsx +++ b/apps/studio/pages/org/[slug]/webhooks/index.tsx @@ -1,7 +1,7 @@ import { PlatformWebhooksPage } from '@/components/interfaces/Platform/Webhooks' -import DefaultLayout from '@/components/layouts/DefaultLayout' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' -import OrganizationSettingsLayout from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' +import { OrganizationSettingsLayout } from '@/components/layouts/ProjectLayout/OrganizationSettingsLayout' import type { NextPageWithLayout } from '@/types' const OrgWebhooksSettings: NextPageWithLayout = () => { diff --git a/packages/ui/src/components/shadcn/ui/sidebar.tsx b/packages/ui/src/components/shadcn/ui/sidebar.tsx index 39a7d98649..1b7521c8b4 100644 --- a/packages/ui/src/components/shadcn/ui/sidebar.tsx +++ b/packages/ui/src/components/shadcn/ui/sidebar.tsx @@ -453,7 +453,7 @@ const SidebarGroupAction = React.forwardRef< ref={ref} data-sidebar="group-action" className={cn( - 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-5 [&>svg]:shrink-0', + 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-5 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. 'after:absolute after:-inset-2 after:md:hidden', 'group-data-[collapsible=icon]:hidden', @@ -503,14 +503,14 @@ SidebarMenuItem.displayName = 'SidebarMenuItem' const sidebarMenuButtonVariants = cva( cn( - 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground' + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent/50 active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent/50 data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground' ), { variants: { variant: { - default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', + default: 'hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground', outline: - 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', + 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', }, size: { default: 'h-8 text-sm', @@ -614,7 +614,7 @@ const SidebarMenuAction = React.forwardRef< ref={ref} data-sidebar="menu-action" className={cn( - 'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-5 [&>svg]:shrink-0', + 'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-5 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. 'after:absolute after:-inset-2 after:md:hidden', 'peer-data-[size=sm]/menu-button:top-1', @@ -722,7 +722,7 @@ const SidebarMenuSubButton = React.forwardRef< data-size={size} data-active={isActive} className={cn( - 'flex h-6 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', + 'flex h-6 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent/50 active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', size === 'sm' && 'text-xs', size === 'md' && 'text-sm', diff --git a/packages/ui/src/lib/theme/defaultTheme.ts b/packages/ui/src/lib/theme/defaultTheme.ts index 3a36e143b6..17948b2a86 100644 --- a/packages/ui/src/lib/theme/defaultTheme.ts +++ b/packages/ui/src/lib/theme/defaultTheme.ts @@ -706,10 +706,11 @@ export default { rounded: `rounded-md`, }, pills: { - base: `px-3 py-1`, + base: `my-px px-3 py-[3px] rounded-md transition-colors active:bg-sidebar-accent/50`, normal: ` font-normal border-default + hover:bg-sidebar-accent/50 group-hover:border-foreground-muted`, active: ` font-semibold From 89ed7f1a24d3f386d361911cb40b31243abc9017 Mon Sep 17 00:00:00 2001 From: Kai Mavyn <94161518+saltspell@users.noreply.github.com> Date: Tue, 5 May 2026 07:05:12 -0700 Subject: [PATCH 02/10] Add Kai M to the humans.txt list (#45563) Onboarding Task - adding myself to the list! ## Summary by CodeRabbit * **Chores** * Updated team information. --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 81fe74d50d..76b17ecdc1 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -142,6 +142,7 @@ Jordi Enric José Luis Ledesma Joshen Lim Julien Goux +Kai M Kalleby Santos Kanishk Dudeja Kamil Ogórek From 97a8df0a2317cf7b955daafc167a924442df0d2c Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 5 May 2026 17:18:46 +0300 Subject: [PATCH 03/10] feat: Handle the `classic-dark` theme in `www` and `docs` apps (#45214) This PR fixes a bug where a user might choose `classic-dark` as a theme in `studio` but then `docs` and `marketing` apps will look weird. To test: - Change the localStorage value of `theme` to `classic-dark` - Open `www` and `docs` apps, they should look ok ## Summary by CodeRabbit * **New Features** * Added a new "classic-dark" theme option for enhanced visual customization. * **Improvements** * Unified and simplified theme handling across apps for more consistent behavior. * Improved system-theme detection and smoother transitions when switching themes. --- apps/design-system/app/Providers.tsx | 9 +- apps/design-system/app/layout.tsx | 10 +- apps/design-system/package.json | 3 +- apps/docs/components/HomePageCover.tsx | 10 +- apps/docs/features/app.providers.tsx | 2 +- apps/docs/package.json | 2 +- apps/docs/styles/main.css | 1 + apps/learn/app/Providers.tsx | 13 ++- apps/learn/app/layout.tsx | 10 +- apps/learn/package.json | 2 +- apps/lite-studio/app/app.css | 1 + apps/studio/package.json | 2 +- apps/studio/pages/_app.tsx | 9 +- apps/ui-library/app/Providers.tsx | 11 +-- .../example/password-based-auth/layout.tsx | 11 +-- .../app/example/social-auth/layout.tsx | 10 +- apps/ui-library/app/layout.tsx | 10 +- apps/ui-library/package.json | 2 +- apps/www/app/providers.tsx | 4 +- apps/www/package.json | 2 +- apps/www/pages/_app.tsx | 9 +- apps/www/styles/index.css | 1 + packages/common/Providers.tsx | 13 ++- packages/common/package.json | 2 +- packages/ui-patterns/package.json | 2 +- .../ui/build/css/themes/faux-classic-dark.css | 68 +++++++++++++ pnpm-lock.yaml | 95 ++++++++----------- pnpm-workspace.yaml | 1 + 28 files changed, 169 insertions(+), 146 deletions(-) create mode 100644 packages/ui/build/css/themes/faux-classic-dark.css diff --git a/apps/design-system/app/Providers.tsx b/apps/design-system/app/Providers.tsx index a7fe031c76..46dce70854 100644 --- a/apps/design-system/app/Providers.tsx +++ b/apps/design-system/app/Providers.tsx @@ -1,20 +1,19 @@ 'use client' +import { ThemeProvider } from 'common' import { Provider as JotaiProvider } from 'jotai' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { ThemeProviderProps } from 'next-themes/dist/types' import { TooltipProvider } from 'ui' import { MobileSidebarProvider } from '@/context/mobile-sidebar-context' -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { +export function Providers({ children }: { children: React.ReactNode }) { return ( - + {children} - + ) } diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 93fa102345..69176828eb 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -4,7 +4,7 @@ import '@/styles/globals.css' import type { Metadata, Viewport } from 'next' import { customFont, sourceCodePro } from './fonts' -import { ThemeProvider } from './Providers' +import { Providers } from './Providers' import { Toaster } from './toaster' const className = `${customFont.variable} ${sourceCodePro.variable}` @@ -131,16 +131,12 @@ export default async function Layout({ children }: RootLayoutProps) { /> - +
{children}
-
+ ) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 9e80848471..5f8269c8af 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -20,6 +20,7 @@ "@hookform/resolvers": "^3.1.1", "@tanstack/react-table": "^8.21.3", "contentlayer2": "0.4.6", + "common": "workspace:*", "date-fns": "^2.30.0", "dayjs": "1.11.13", "eslint-config-supabase": "workspace:*", @@ -29,7 +30,7 @@ "markdown-wasm": "^1.2.0", "next": "catalog:", "next-contentlayer2": "0.4.6", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "react": "catalog:", "react-data-grid": "7.0.0-beta.47", "react-day-picker": "^9.11.1", diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 93f642501b..92ba17cb22 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -1,13 +1,13 @@ 'use client' -import { ChevronRight, Play, Sparkles } from 'lucide-react' -import Link from 'next/link' -import { useTheme } from 'next-themes' // End of third-party imports - import { isFeatureEnabled, useBreakpoint } from 'common' +import { ChevronRight, Play, Sparkles } from 'lucide-react' +import { useTheme } from 'next-themes' +import Link from 'next/link' import { cn, IconBackground } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' + import { getCustomContent } from '../lib/custom-content/getCustomContent' import DocsCoverLogo from './DocsCoverLogo' @@ -41,7 +41,7 @@ const HomePageCover = (props) => { const iconSize = isXs ? 'sm' : 'lg' const { homepageHeading } = getCustomContent(['homepage:heading']) const { resolvedTheme } = useTheme() - const isLightMode = resolvedTheme !== 'dark' + const isLightMode = !resolvedTheme?.includes('dark') const frameworks = [ { diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 8469b004cb..cc7460313a 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -24,7 +24,7 @@ function GlobalProviders({ children }: PropsWithChildren) { - +
diff --git a/apps/docs/package.json b/apps/docs/package.json index e21a6d9ace..1259814500 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -93,7 +93,7 @@ "next": "^15.5.15", "next-mdx-remote": "^6.0.0", "next-plugin-yaml": "^1.0.1", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "nuqs": "^1.19.1", "openai": "^4.75.1", "openapi-fetch": "0.12.4", diff --git a/apps/docs/styles/main.css b/apps/docs/styles/main.css index e5366ce723..de09695612 100644 --- a/apps/docs/styles/main.css +++ b/apps/docs/styles/main.css @@ -2,6 +2,7 @@ @import './../../../packages/ui/build/css/source/global.css'; @import './../../../packages/ui/build/css/themes/dark.css'; +@import './../../../packages/ui/build/css/themes/faux-classic-dark.css'; @import './../../../packages/ui/build/css/themes/light.css'; @config '../tailwind.config.cjs'; diff --git a/apps/learn/app/Providers.tsx b/apps/learn/app/Providers.tsx index 24d7eac122..1cfc344881 100644 --- a/apps/learn/app/Providers.tsx +++ b/apps/learn/app/Providers.tsx @@ -1,25 +1,24 @@ 'use client' +import { AuthProvider, ThemeProvider } from 'common' import { Provider as JotaiProvider } from 'jotai' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { ThemeProviderProps } from 'next-themes/dist/types' +import { PropsWithChildren } from 'react' +import { TooltipProvider } from 'ui' import { FrameworkProvider } from '@/context/framework-context' import { MobileMenuProvider } from '@/context/mobile-menu-context' -import { AuthProvider } from 'common' -import { TooltipProvider } from 'ui' -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { +export function Providers({ children }: PropsWithChildren) { return ( - + {children} - + ) diff --git a/apps/learn/app/layout.tsx b/apps/learn/app/layout.tsx index d4dd63ee7d..abd7a75e38 100644 --- a/apps/learn/app/layout.tsx +++ b/apps/learn/app/layout.tsx @@ -6,7 +6,7 @@ import { FeatureFlagProvider, TelemetryTagManager } from 'common' import { genFaviconData } from 'common/MetaFavicons/app-router' import { Inter } from 'next/font/google' -import { ThemeProvider } from './Providers' +import { Providers } from './Providers' import { Toaster } from './toaster' import { API_URL } from '@/lib/constants' @@ -47,14 +47,10 @@ export default async function Layout({ children }: RootLayoutProps) { - + {children} - + diff --git a/apps/learn/package.json b/apps/learn/package.json index 217c202dc5..f2159e418c 100644 --- a/apps/learn/package.json +++ b/apps/learn/package.json @@ -23,7 +23,7 @@ "lucide-react": "*", "next": "catalog:", "next-contentlayer2": "0.4.6", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-wrap-balancer": "^1.1.0", diff --git a/apps/lite-studio/app/app.css b/apps/lite-studio/app/app.css index da69bb81ee..1361ee2608 100644 --- a/apps/lite-studio/app/app.css +++ b/apps/lite-studio/app/app.css @@ -2,6 +2,7 @@ @import './../../../packages/ui/build/css/source/global.css'; @import './../../../packages/ui/build/css/themes/dark.css'; +@import './../../../packages/ui/build/css/themes/classic-dark.css'; @import './../../../packages/ui/build/css/themes/light.css'; @config '../tailwind.config.js'; diff --git a/apps/studio/package.json b/apps/studio/package.json index 907bc868a7..4640ed90be 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -104,7 +104,7 @@ "mime-db": "^1.53.0", "monaco-editor": "0.52.2", "next": "catalog:", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "nuqs": "2.7.1", "openai": "^4.104.0", "openapi-fetch": "0.12.4", diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index c9cd956a01..d352a24054 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -13,8 +13,6 @@ import '@/styles/storage.css' import '@/styles/stripe.css' import '@/styles/ui.css' import 'ui-patterns/ShimmeringLoader/index.css' -import 'ui/build/css/themes/dark.css' -import 'ui/build/css/themes/light.css' import { loader } from '@monaco-editor/react' import * as Sentry from '@sentry/nextjs' @@ -184,12 +182,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { - + diff --git a/apps/ui-library/app/Providers.tsx b/apps/ui-library/app/Providers.tsx index 5956f800c4..76fa8a6d01 100644 --- a/apps/ui-library/app/Providers.tsx +++ b/apps/ui-library/app/Providers.tsx @@ -1,30 +1,29 @@ 'use client' import { QueryClientProvider } from '@tanstack/react-query' -import { AuthProvider } from 'common' +import { AuthProvider, ThemeProvider } from 'common' import { Provider as JotaiProvider } from 'jotai' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { ThemeProviderProps } from 'next-themes/dist/types' +import { PropsWithChildren } from 'react' import { TooltipProvider } from 'ui' import { FrameworkProvider } from '@/context/framework-context' import { MobileMenuProvider } from '@/context/mobile-menu-context' import { useRootQueryClient } from '@/lib/fetch/queryClient' -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { +export function Providers({ children }: PropsWithChildren) { const queryClient = useRootQueryClient() return ( - + {children} - + diff --git a/apps/ui-library/app/example/password-based-auth/layout.tsx b/apps/ui-library/app/example/password-based-auth/layout.tsx index 030beb3a32..df06062cea 100644 --- a/apps/ui-library/app/example/password-based-auth/layout.tsx +++ b/apps/ui-library/app/example/password-based-auth/layout.tsx @@ -1,7 +1,8 @@ +import { html } from 'monaco-editor' import { Metadata } from 'next' import { BaseInjector } from './../base-injector' -import { ThemeProvider } from '@/app/Providers' +import { Providers } from '@/app/Providers' export const metadata: Metadata = { title: 'Password Based Auth Example', @@ -27,11 +28,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - +
{children}
-
+ ) diff --git a/apps/ui-library/app/example/social-auth/layout.tsx b/apps/ui-library/app/example/social-auth/layout.tsx index de42457be6..e75df4c7c4 100644 --- a/apps/ui-library/app/example/social-auth/layout.tsx +++ b/apps/ui-library/app/example/social-auth/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next' import { BaseInjector } from './../base-injector' -import { ThemeProvider } from '@/app/Providers' +import { Providers } from '@/app/Providers' export const metadata: Metadata = { title: 'Social Auth Example', @@ -27,11 +27,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - +
{children}
-
+ ) diff --git a/apps/ui-library/app/layout.tsx b/apps/ui-library/app/layout.tsx index e251215d89..3561e4d7da 100644 --- a/apps/ui-library/app/layout.tsx +++ b/apps/ui-library/app/layout.tsx @@ -6,7 +6,7 @@ import { FeatureFlagProvider, TelemetryTagManager } from 'common' import { genFaviconData } from 'common/MetaFavicons/app-router' import { Inter } from 'next/font/google' -import { ThemeProvider } from './Providers' +import { Providers } from './Providers' import { Toaster } from './toaster' import { API_URL } from '@/lib/constants' @@ -47,14 +47,10 @@ export default async function Layout({ children }: RootLayoutProps) { - + {children} - + diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index a79668ff67..32634d6cfe 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -40,7 +40,7 @@ "monaco-editor": "^0.55.1", "next": "catalog:", "next-contentlayer2": "0.4.6", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "openai": "^5.9.0", "openapi-fetch": "0.12.4", "radix-ui": "catalog:", diff --git a/apps/www/app/providers.tsx b/apps/www/app/providers.tsx index 61e8b8e8d3..33eea5b09e 100644 --- a/apps/www/app/providers.tsx +++ b/apps/www/app/providers.tsx @@ -12,7 +12,7 @@ import { import { WwwCommandMenu } from 'components/CommandMenu' import { DevToolbar, DevToolbarProvider } from 'dev-tools' import { API_URL } from 'lib/constants' -import { themes, TooltipProvider } from 'ui' +import { TooltipProvider } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' import { useConsentToast } from 'ui-patterns/consent' @@ -26,7 +26,7 @@ function Providers({ children }: { children: React.ReactNode }) { - t.value)} enableSystem disableTransitionOnChange> + diff --git a/apps/www/package.json b/apps/www/package.json index b3ee88bb8a..6730c8ac2b 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -67,7 +67,7 @@ "next": "^15.5.15", "next-mdx-remote": "^6.0.0", "next-seo": "^6.5.0", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "nuqs": "^2.8.1", "openai": "^4.75.1", "parse-numeric-range": "^1.3.0", diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index 3567e72850..1af7637f23 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -21,7 +21,7 @@ import { DefaultSeo } from 'next-seo' import type { AppProps } from 'next/app' import Head from 'next/head' import { useRouter } from 'next/router' -import { themes, TooltipProvider } from 'ui' +import { TooltipProvider } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' import { useConsentToast } from 'ui-patterns/consent' @@ -105,12 +105,7 @@ export default function App({ Component, pageProps }: AppProps) { {/* [TODO] I think we need to deconflict with the providers in layout.tsx? */} - theme.value)} - enableSystem - disableTransitionOnChange - forcedTheme={forceDarkMode ? 'dark' : undefined} - > + diff --git a/apps/www/styles/index.css b/apps/www/styles/index.css index 91c814f863..11cd481309 100644 --- a/apps/www/styles/index.css +++ b/apps/www/styles/index.css @@ -2,6 +2,7 @@ @import './../../../packages/ui/build/css/source/global.css'; @import './../../../packages/ui/build/css/themes/dark.css'; +@import './../../../packages/ui/build/css/themes/faux-classic-dark.css'; @import './../../../packages/ui/build/css/themes/light.css'; @config '../tailwind.config.js'; diff --git a/packages/common/Providers.tsx b/packages/common/Providers.tsx index 76a6c775ee..6b0aa5b566 100644 --- a/packages/common/Providers.tsx +++ b/packages/common/Providers.tsx @@ -1,13 +1,16 @@ 'use client' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import type { ThemeProviderProps } from 'next-themes/dist/types' +import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - // @ts-ignore next-themes is old :/ - return ( - + {children} ) diff --git a/packages/common/package.json b/packages/common/package.json index b2d498e4fd..c5385c58e3 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -20,7 +20,7 @@ "dat.gui": "^0.7.9", "flags": "^4.0.0", "lodash": "catalog:", - "next-themes": "^0.3.0", + "next-themes": "catalog:", "posthog-js": "^1.333.0", "react-use": "^17.4.0", "valtio": "catalog:" diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 975a6f3de6..ec08d6fc7a 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -802,7 +802,7 @@ "mdast": "^3.0.0", "mermaid": "^11.12.1", "monaco-editor": "*", - "next-themes": "*", + "next-themes": "catalog:", "openai": "^4.75.1", "prism-react-renderer": "^2.3.1", "radix-ui": "catalog:", diff --git a/packages/ui/build/css/themes/faux-classic-dark.css b/packages/ui/build/css/themes/faux-classic-dark.css new file mode 100644 index 0000000000..e99e7a8427 --- /dev/null +++ b/packages/ui/build/css/themes/faux-classic-dark.css @@ -0,0 +1,68 @@ +/* + * This theme has the same values as the dark theme meant to be used in www and docs. www and docs can only support + * light and dark themes because they only have images for light and dark themes. + */ +[data-theme='classic-dark'], +.classic-dark { + --helpers-os-appearance: Dark; + --code-block-5: 13.8deg 89.7% 69.6%; + --code-block-4: 276.1deg 67.7% 74.5%; + --code-block-3: 83.8deg 61.7% 63.1%; + --code-block-2: 33.2deg 90.3% 75.7%; + --code-block-1: 170.8deg 43.1% 61.4%; + --secondary-default: 247.8deg 100% 70%; + --secondary-400: 248.3deg 54.5% 25.9%; + --secondary-200: 248deg 53.6% 11%; + --brand-link: 155deg 100% 38.6%; + --brand-default: 153.1deg 60.2% 52.7%; + --brand-600: 154.9deg 59.5% 70%; + --brand-500: 154.9deg 100% 19.2%; + --brand-400: 155.5deg 100% 9.6%; + --brand-300: 155.1deg 100% 8%; + --brand-200: 162deg 100% 2%; + --warning-default: 38.9deg 100% 42.9%; /* warning-600 */ + --warning-600: 38.9deg 100% 42.9%; + --warning-500: 34.8deg 90.9% 21.6%; + --warning-400: 33.2deg 100% 14.5%; + --warning-300: 32.3deg 100% 10.2%; + --warning-200: 36.6deg 100% 8%; + --destructive-default: 10.2deg 77.9% 53.9%; + --destructive-600: 9.7deg 85.2% 62.9%; + --destructive-500: 7.9deg 71.6% 29%; + --destructive-400: 6.7deg 60% 20.6%; + --destructive-300: 7.5deg 51.3% 15.3%; + --destructive-200: 10.9deg 23.4% 9.2%; + --border-stronger: 0deg 0% 27.1%; + --border-strong: 0deg 0% 21.2%; + --border-alternative: 0deg 0% 26.7%; + --border-control: 0deg 0% 22.4%; + --border-overlay: 0deg 0% 20%; + --border-secondary: 0deg 0% 14.1%; + --border-muted: 0deg 0% 14.1%; + --border-default: 0deg 0% 18%; + --background-dash-canvas: 0deg 0% 7.1%; + --background-dash-sidebar: 0deg 0% 9%; + --background-dialog-default: 0deg 0% 7.1%; + --background-muted: 0deg 0% 14.1%; + --background-overlay-hover: 0deg 0% 18%; + --background-overlay-default: 0deg 0% 14.1%; + --background-surface-400: 0deg 0% 16.1%; + --background-surface-300: 0deg 0% 16.1%; + --background-surface-200: 0deg 0% 12.9%; + --background-surface-100: 0deg 0% 12.2%; + --background-surface-75: 0deg 0% 9%; + --background-control: 0deg 0% 14.1%; + --background-selection: 0deg 0% 19.2%; + --background-alternative-default: 0deg 0% 5.9%; + --background-default: 0deg 0% 7.1%; + --background-200: 0deg 0% 9%; + --foreground-contrast: 0deg 0% 8.6%; + --foreground-muted: 0deg 0% 30.2%; + --foreground-lighter: 0deg 0% 53.7%; + --foreground-light: 0deg 0% 70.6%; + --foreground-default: 0deg 0% 98%; + --border-button-hover: var(--colors-gray-dark-800); + --border-button-default: var(--colors-gray-dark-700); + --background-button-default: var(--colors-gray-dark-500); + --background-alternative-200: var(--colors-gray-dark-200); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac10db3f91..75c4023316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,9 @@ catalogs: next: specifier: 16.2.3 version: 16.2.3 + next-themes: + specifier: ^0.4.6 + version: 0.4.6 radix-ui: specifier: ^1.4.3 version: 1.4.3 @@ -169,6 +172,9 @@ importers: '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + common: + specifier: workspace:* + version: link:../../packages/common contentlayer2: specifier: 0.4.6 version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) @@ -200,8 +206,8 @@ importers: specifier: 0.4.6 version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 'catalog:' version: 18.3.1 @@ -462,8 +468,8 @@ importers: specifier: ^1.0.1 version: 1.0.1 next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 version: 1.19.1(next@15.5.15(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) @@ -715,8 +721,8 @@ importers: specifier: 0.4.6 version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 'catalog:' version: 18.3.1 @@ -913,7 +919,7 @@ importers: version: 0.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: 'catalog:' - version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2)) + version: 10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2)) '@std/path': specifier: npm:@jsr/std__path@^1.0.8 version: '@jsr/std__path@1.0.8' @@ -1065,11 +1071,11 @@ importers: specifier: 'catalog:' version: 16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: 2.7.1 - version: 2.7.1(@tanstack/react-router@1.168.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 2.7.1(@tanstack/react-router@1.168.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) openai: specifier: ^4.104.0 version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) @@ -1319,7 +1325,7 @@ importers: version: 2.11.3(@types/node@22.13.14)(typescript@6.0.2) next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) node-mocks-http: specifier: ^1.17.2 version: 1.17.2(@types/node@22.13.14) @@ -1426,8 +1432,8 @@ importers: specifier: 0.4.6 version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^5.9.0 version: 5.9.0(ws@8.19.0)(zod@3.25.76) @@ -1724,8 +1730,8 @@ importers: specifier: ^6.5.0 version: 6.5.0(next@15.5.15(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.8.1 version: 2.8.1(@tanstack/react-router@1.168.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@15.5.15(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -2082,8 +2088,8 @@ importers: specifier: 'catalog:' version: 16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-themes: - specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) posthog-js: specifier: ^1.333.0 version: 1.357.0 @@ -2206,7 +2212,7 @@ importers: version: link:../config next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) tailwindcss: specifier: 'catalog:' version: 4.2.4 @@ -2563,8 +2569,8 @@ importers: specifier: 'catalog:' version: 16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) next-themes: - specifier: '*' - version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openai: specifier: ^4.75.1 version: 4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@3.25.76) @@ -2688,7 +2694,7 @@ importers: version: link:../config next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) + version: 0.9.13(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1) tailwindcss: specifier: ^4.2.4 version: 4.2.4 @@ -3460,24 +3466,15 @@ packages: '@electric-sql/pglite@0.2.15': resolution: {integrity: sha512-Jiq31Dnk+rg8rMhcSxs4lQvHTyizNo5b269c1gCC3ldQ0sCLrNVPGzy+KnmonKy1ZArTUuXZf23/UamzFMKVaA==} - '@emnapi/core@1.9.0': - resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} '@emnapi/runtime@0.43.1': resolution: {integrity: sha512-Q5sMc4Z4gsD4tlmlyFu+MpNAwpR7Gv2errDhVJ+SOhNjWcx8UTqy+hswb8L31RfC8jBvDgcnT87l3xI2w08rAg==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -13886,11 +13883,11 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' - next-themes@0.3.0: - resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: - react: ^16.8 || ^17 || ^18 - react-dom: ^16.8 || ^17 || ^18 + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -19310,12 +19307,6 @@ snapshots: '@electric-sql/pglite@0.2.15': optional: true - '@emnapi/core@1.9.0': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -19326,21 +19317,11 @@ snapshots: dependencies: tslib: 2.8.1 - '@emnapi/runtime@1.7.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -20336,7 +20317,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.7.1 + '@emnapi/runtime': 1.9.2 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -20745,8 +20726,8 @@ snapshots: '@napi-rs/wasm-runtime@1.1.1': dependencies: - '@emnapi/core': 1.9.0 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -23591,7 +23572,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2))': + '@sentry/nextjs@10.27.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 @@ -31613,7 +31594,7 @@ snapshots: dependencies: js-yaml-loader: 1.2.2 - next-router-mock@0.9.13(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): + next-router-mock@0.9.13(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react@18.3.1): dependencies: next: 16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) react: 18.3.1 @@ -31624,7 +31605,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -31946,7 +31927,7 @@ snapshots: mitt: 3.0.1 next: 15.5.15(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) - nuqs@2.7.1(@tanstack/react-router@1.168.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@16.2.3(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + nuqs@2.7.1(@tanstack/react-router@1.168.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@16.2.3(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-router@7.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@standard-schema/spec': 1.0.0 react: 18.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 079e4b4330..bf312ed3f6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,6 +20,7 @@ catalog: lodash: ^4.18.1 lodash-es: ^4.18.1 next: 16.2.3 + next-themes: ^0.4.6 postcss: ^8.5.10 radix-ui: ^1.4.3 react: ^18.3.0 From d401fd89da9f9169eb02eb351491ce0b485bb7dd Mon Sep 17 00:00:00 2001 From: Ana <30495040+ana1337x@users.noreply.github.com> Date: Tue, 5 May 2026 10:39:30 -0400 Subject: [PATCH 04/10] blog: Realtime or ETL? How to choose the right tool (#45568) ## 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? New blog post ## What is the current behavior? N/A ## What is the new behavior? Adds a new blog post explaining when to use Supabase Realtime vs Supabase ETL, covering delivery guarantees, destinations, scale characteristics, and common mistakes. ## Additional context N/A ## Summary by CodeRabbit * **Documentation** * Added a comprehensive blog post comparing Supabase Realtime and ETL, covering technical differences, delivery guarantees, use cases, and best practices to help users select the appropriate tool for their data integration needs. --------- Co-authored-by: Ana --- ...me-or-etl-how-to-choose-the-right-tool.mdx | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 apps/www/_blog/2026-05-05-realtime-or-etl-how-to-choose-the-right-tool.mdx diff --git a/apps/www/_blog/2026-05-05-realtime-or-etl-how-to-choose-the-right-tool.mdx b/apps/www/_blog/2026-05-05-realtime-or-etl-how-to-choose-the-right-tool.mdx new file mode 100644 index 0000000000..fa52d0ef77 --- /dev/null +++ b/apps/www/_blog/2026-05-05-realtime-or-etl-how-to-choose-the-right-tool.mdx @@ -0,0 +1,186 @@ +--- +title: 'Realtime or ETL? How to choose the right tool' +description: 'Both Supabase Realtime and Supabase ETL read changes from your Postgres database using logical replication. But they solve very different problems. Here is how to pick the right one.' +author: eduardo_gurgel, riccardo_busetti +date: '2026-05-05' +categories: + - developers +tags: + - realtime + - postgres + - etl + - database +imgSocial: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=announcement&layout=horizontal©=Realtime+or+ETL%0A%5BHow+to+choose+the+right+tool%5D&icon=icon-realtime.svg' +imgThumb: 'https://zhfonblqamxferhoguzj.supabase.co/functions/v1/generate-og?template=ruler&layout=icon-only©=Realtime+or+ETL%0A%5BHow+to+choose+the+right+tool%5D&icon=icon-realtime.svg' +toc_depth: 2 +--- + +Both [Supabase Realtime](https://supabase.com/docs/guides/realtime) and [Supabase ETL](https://supabase.com/features/supabase-etl) read changes from your Postgres database. They both use [logical replication](https://supabase.com/docs/guides/database/replication) under the hood. They even look similar when you squint. But they solve very different problems, and choosing the wrong one will frustrate you. + +This post explains what each product does, how they differ, and when you should pick one over the other. + +## Two tools, two jobs + +Here is the simplest way to think about it: + +- **Realtime** sends database changes to your users' browsers and apps, right now, as they happen. It is built for live experiences. +- **ETL** sends database changes in near real-time to analytical destinations like BigQuery and Analytics Buckets. It is built for reliable data movement. + +Realtime answers the question: "How do I show my users what just happened?" + +ETL answers the question: "How do I get my production data into my analytics warehouse?" + +If you mix these up, you will run into problems. We see it happen regularly, and the rest of this post will help you avoid that. + +## What Realtime does + +[Supabase Realtime](https://supabase.com/docs/guides/realtime) is three features in one product: + +1. [**Broadcast.**](https://supabase.com/docs/guides/realtime/broadcast) Send messages between connected clients in real time. No database required. Think cursor positions, typing indicators, or game state. +2. [**Presence.**](https://supabase.com/docs/guides/realtime/presence) Track who is online and what they are doing. Also no database required. Think "3 users are editing this document" or "Jane is typing..." +3. [**Postgres Changes.**](https://supabase.com/docs/guides/realtime/postgres-changes) Listen to INSERT, UPDATE, and DELETE events on your database tables and deliver them to subscribed clients over WebSocket. + +Two of these three features, Broadcast and Presence, can work without any database interaction. Client-to-client Broadcast sends messages purely over WebSocket with nothing stored. However, [Broadcast from Database](https://supabase.com/blog/realtime-broadcast-from-database) lets you trigger broadcasts from database changes using triggers, giving you control over which events reach which channels. This matters because Realtime is not just a database change listener. It is a real-time communication layer for your application. + +### How Postgres Changes works + +When a client subscribes to a table, Realtime uses a PostgreSQL replication slot to read changes from the Write-Ahead Log (WAL). For each change, it checks [Row Level Security (RLS)](https://supabase.com/docs/guides/database/postgres/row-level-security) policies against every subscribed user. If a user is authorized to see the change, Realtime sends it over their WebSocket connection. + +This is designed for live UI updates. A user inserts a message into a chat table. Other users see it appear instantly. A row updates in a dashboard table. The chart refreshes automatically. + +### What Realtime does not guarantee + +Realtime's Postgres Changes feature does not guarantee delivery. If a client disconnects for 30 seconds and reconnects, the changes that happened during those 30 seconds are gone. Realtime does not queue them and does not track how far each client has read. + +[Broadcast Replay](https://supabase.com/blog/realtime-broadcast-replay) offers limited catch-up for Broadcast from Database messages: clients can request up to 25 messages from the last 3 days using a `since` timestamp. But this only works on private channels, only for database-sourced broadcasts, and is currently in public alpha. It is not a general-purpose replay mechanism for all Realtime events. + +Postgres Changes uses temporary replication slots. When no clients are subscribed, it stops replicating data entirely. When clients subscribe again, a new slot is created. + +The Realtime team built it this way on purpose. Guaranteed delivery requires persistent state tracking, message queuing, and acknowledgment protocols. Those things add latency and complexity that would make Realtime worse at its actual job: delivering live updates as fast as possible. + +If you need every change to arrive at its destination, no matter what, Realtime is not the right tool. + +## What ETL does + +Supabase ETL is a change-data-capture (CDC) pipeline. It reads every INSERT, UPDATE, DELETE, and TRUNCATE from your Postgres tables and writes them to a destination. Right now, the supported destinations are [Analytics Buckets](https://supabase.com/docs/guides/storage/analytics/introduction) (built on Apache Iceberg) and BigQuery. + +ETL replicates your data 1-to-1 in near real-time. If your destination disconnects or has problems, ETL does not skip over data. + +### How ETL works + +When you create an ETL pipeline, it connects to your database through a permanent replication slot. First, it performs a full copy of your existing data. Then it switches to streaming mode and captures every change as it happens, with latency measured in milliseconds to seconds (based on configuration parameters, data size, and destination type). + +It's important to note that Supabase ETL doesn't respect Row-Level Security. Supabase ETL reads every piece of data. If you need to filter data, you should use publication filters. + +Changes are batched and written to your destination. If the pipeline crashes, it restarts from the last acknowledged position. No data changes are lost. Note that schema changes (adding or removing columns) do not propagate automatically and require manual handling. + +### What ETL guarantees + +ETL provides at-least-once delivery. Every change that happens in your database will reach the destination at least once. In rare cases (like a crash during a long-running transaction), a change might be delivered more than once. Exactly-once processing is handled by the destination. Some destinations like BigQuery deduplicate automatically, while others may not. + +ETL uses permanent replication slots. This means Postgres holds onto WAL data until ETL confirms it has been processed. If you stop the pipeline for maintenance and restart it later, it picks up exactly where it left off. Be aware that while the pipeline is paused, Postgres continues to retain WAL data. Extended pauses can lead to significant disk growth, and depending on your Postgres configuration, the pipeline may fail if the WAL retention limit is exceeded. + +This is the opposite of Realtime's approach. ETL trades speed for reliability. It may not deliver changes to your warehouse in the same millisecond they happen, but it will deliver every single one. + +## The key differences + +### Delivery guarantees + +| | **Realtime** | **ETL** | +| ----------------------- | ------------ | --------------------- | +| Guarantee | Best effort | At-least-once | +| Missed changes | Lost forever | Replayed on reconnect | +| Replication slot | Temporary | Permanent | +| Resume after disconnect | No | Yes | + +This is the most important difference. If you need every change to arrive, use ETL. If you need changes to arrive fast and can tolerate occasional gaps, use Realtime. + +### Where data goes + +Realtime sends data to client applications over WebSocket connections. Your users' browsers and mobile apps are the destination. + +ETL sends data to analytical systems. BigQuery, Analytics Buckets, and eventually other data warehouses are the destination. + +These are fundamentally different targets with fundamentally different needs. Client apps need low latency. Analytical systems need completeness. + +### Database dependency + +Realtime's Broadcast and Presence features can work without touching the database. You can build an entire collaborative experience (cursors, presence indicators, ephemeral messaging) without writing a single database query. However, Postgres Changes and [Broadcast from Database](https://supabase.com/blog/realtime-broadcast-from-database) both require database interaction. + +ETL is entirely database-driven. Every byte of data it moves comes from your Postgres tables. + +### Scale characteristics + +Realtime's Broadcast and Presence features are built for high throughput and low latency. They do not run per-subscriber database queries and scale well across many concurrent connections. + +Postgres Changes works differently. It processes changes sequentially to maintain ordering. For each change, it runs an RLS authorization check against every subscribed client. With 100 subscribers watching a table, one INSERT generates 100 authorization queries. This is a deliberate design choice that prioritizes correctness and low latency for typical workloads over raw throughput. + +ETL processes changes in configurable batches with tunable parallelism. It does not need to authorize individual users because it is moving data to a system, not to end users. + +## When to use Realtime + +Use Realtime when you need to push live updates to your users: + +- **Chat applications.** Messages appear instantly for all participants. +- **Collaborative editing.** See other users' cursors and changes in real time. +- **Live dashboards.** Charts and metrics update without page refresh. +- **Notifications.** Alert users when something relevant happens. +- **Multiplayer features.** Synchronize game state or shared experiences. +- **Presence tracking.** Show who is online, who is typing, who is viewing a document. + +The common thread: a human is watching and needs to see changes as they happen. + +## When to use ETL + +Use ETL when you need reliable data movement to analytical systems: + +- **Analytics and reporting.** Move production data to BigQuery or Iceberg for querying without impacting your production database. +- **Audit trails.** Analytics Buckets stores an append-only changelog of every INSERT, UPDATE, and DELETE. Nothing is lost. +- **Data warehousing.** Replicate your operational data to a columnar format optimized for analytical queries. +- **Compliance.** Maintain a complete, verifiable history of all data changes. +- **ML pipelines.** Feed fresh data to training or feature stores without querying production. +- **Workload isolation.** Run heavy analytical queries against your warehouse instead of your production database. + +The common thread: a system needs a complete, reliable copy of your data. + +## The mistake we see most often + +Some developers discover Realtime's Postgres Changes feature and think: "I can use this to replicate data from one system to another." They write 20 lines of code with supabase-js, subscribe to table changes, and pipe them into another system. + +It works great in development. It even works fine in production for a while. Then a WebSocket connection drops for a few seconds and data goes missing. Or the subscribing process restarts and misses a batch of changes. Or load spikes and the sequential RLS authorization checks cannot keep up. + +The problem is not that Realtime is broken. The problem is that Realtime was not designed for this job. + +If you are piping database changes into another system and you need every change to arrive, use ETL. That is exactly what it was built for. + +## Can I use both? + +Yes. In fact, many applications should. + +Consider an e-commerce platform. You might use Realtime to push order status updates to customers in real time ("Your order has shipped!"). At the same time, you use ETL to replicate all order data to BigQuery for daily sales reports and trend analysis. + +Same database. Same tables. Different tools for different jobs. + +Realtime handles the live experience. ETL handles the analytical pipeline. Each does what it was designed to do. + +## Quick reference + +| | **Realtime** | **ETL** | +| ------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------- | +| Purpose | Live updates to client apps | Reliable data movement to analytics | +| Delivery | Best effort | At-least-once | +| Destinations | Browsers, mobile apps (WebSocket) | BigQuery, Analytics Buckets | +| Replication slot | Temporary | Permanent | +| Resume on reconnect | No | Yes | +| Database required | Only for Postgres Changes and Broadcast from Database | Yes, always | +| Processing | Sequential per-change with per-subscriber authorization | Batched with configurable parallelism | +| Latency | Typically under 100ms | Seconds (batched) | +| Best for | Human users watching live data | Systems consuming complete data | +| Built with | Elixir (Phoenix) | Rust | +| Open source | [github.com/supabase/realtime](https://github.com/supabase/realtime) | [github.com/supabase/etl](https://github.com/supabase/etl) | + +## Getting started + +Supabase Realtime is available on all Supabase projects. Check out the [Realtime documentation](https://supabase.com/docs/guides/realtime) to get started. + +Supabase ETL is currently in private alpha. You can request access through the Supabase Dashboard or contact your account manager. Read the [ETL blog post](https://supabase.com/blog/introducing-supabase-etl) for more details on how it works. From e8ad071e6488e9d20ac72c20d812c23d06b67be6 Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Date: Wed, 6 May 2026 03:14:50 +1200 Subject: [PATCH 05/10] fix(docs): update Realtime Postgres Changes + Authorization interaction (#44199) ## 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? update docs on Realtime Postgres Changes + Authorization interaction ## What is the current behavior? It can be misleading to say `The `private` Channel option does not apply to Postgres Changes.` As Postgres Changes can happen under a private channel. ## What is the new behavior? Fix docs ## Summary by CodeRabbit * **Documentation** * Updated the authorization guide to clarify how Postgres Changes interact with Channel authorization, including RLS policy enforcement and channel type compatibility. Co-authored-by: Chris Chinchilla --- apps/docs/content/guides/realtime/authorization.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx index 5a4a68ef20..2b65ba51e7 100644 --- a/apps/docs/content/guides/realtime/authorization.mdx +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -381,9 +381,9 @@ with check ( ## Interaction with Postgres Changes -Realtime Postgres Changes are separate from Channel authorization. The `private` Channel option does not apply to Postgres Changes. +When using Postgres Changes on tables with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies. -When using Postgres Changes with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies. +Private and public channels can subscribe to Postgres Changes. ## Updating RLS policies From ce214c1ca5d6907d6dddb55a3ddadaa337a94ddc Mon Sep 17 00:00:00 2001 From: sasikanumuri-sb Date: Tue, 5 May 2026 08:49:22 -0700 Subject: [PATCH 06/10] Update humans.txt (#45599) --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 76b17ecdc1..338c6c0e48 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -223,6 +223,7 @@ Sam Rome Sam Rose Samir Ketema Sana Cordeaux +Sasi Kanumuri Sara Read Sean Oliver Sean Romberg From fe93df7d6b260882359c904daad0d08eb41868cb Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 5 May 2026 17:56:42 +0200 Subject: [PATCH 07/10] chore: migrate `Input` usages to Shadcn component in auth and policies screens/components (#45590) ## Screenshots ### Auth: Create or edit custom Auth provider See the callback URL input at the bottom. Before: image After: image ### Custom Auth provider list search input Before: image After: image ### Auth hooks Before: image After: image ### OAuth App list search input Before: image After: image ### New policy sheet template search input Before: image After: image ### Storage new policy dialog Before: image After: image ## Summary by CodeRabbit * **Refactor** * Updated search input design across authentication interfaces for improved consistency. * Standardized input control layout in auth configuration forms. * **Bug Fixes** * Corrected webhook configuration field behavior. --- .../CreateOrUpdateCustomProviderSheet.tsx | 2 +- .../CustomAuthProvidersList.tsx | 23 ++++++---- .../interfaces/Auth/Hooks/HookCard.tsx | 42 ++++++++++++------- .../Auth/OAuthApps/OAuthAppsList.tsx | 23 ++++++---- .../Auth/Policies/PolicyEditor/PolicyName.tsx | 22 +++++----- .../PolicyEditorPanel/PolicyTemplates.tsx | 30 +++++++++---- 6 files changed, 88 insertions(+), 54 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx index 8916086b46..8496b652ef 100644 --- a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx +++ b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx @@ -12,7 +12,6 @@ import { FormControl, FormField, FormInputGroupInput, - Input, Input_Shadcn_, InputGroup, InputGroupAddon, @@ -30,6 +29,7 @@ import { Switch, useWatch, } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' diff --git a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx index 3b23e23d14..39f86f5b35 100644 --- a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx +++ b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx @@ -15,7 +15,9 @@ import { HoverCard, HoverCardContent, HoverCardTrigger, - Input, + InputGroup, + InputGroupAddon, + InputGroupInput, Table, TableBody, TableCell, @@ -284,14 +286,17 @@ export const CustomAuthProvidersList = () => {
- } - value={filterString} - className="w-full lg:w-52" - onChange={(e) => setFilterString(e.target.value)} - /> + + setFilterString(e.target.value)} + /> + + + + { Postgres function
- schema + - hook.method.type === 'postgres' && copyToClipboard(hook.method.schema) - } />
- function + - hook.method.type === 'postgres' && copyToClipboard(hook.method.functionName) - } />
@@ -67,26 +70,33 @@ export const HookCard = ({ hook, onSelect }: HookCardProps) => { HTTPS endpoint
- endpoint + hook.method.type === 'https' && copyToClipboard(hook.method.url)} />
- secret +
diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index d995647864..149271a6f5 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -13,7 +13,9 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - Input, + InputGroup, + InputGroupAddon, + InputGroupInput, Table, TableBody, TableCell, @@ -231,14 +233,17 @@ export const OAuthAppsList = () => { )}
- } - value={filterString} - className="w-full lg:w-52" - onChange={(e) => setFilterString(e.target.value)} - /> + + setFilterString(e.target.value)} + /> + + + + A descriptive name for your policy

- onUpdatePolicyName(e.target.value)} - actions={ - + + onUpdatePolicyName(e.target.value)} + /> + + {name.length}/{limit} - - } - /> + + +
) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx index 2194d35771..d1377074e9 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyEditorPanel/PolicyTemplates.tsx @@ -1,7 +1,16 @@ import { PostgresPolicy } from '@supabase/postgres-meta' import { Search } from 'lucide-react' import { useState } from 'react' -import { Badge, cn, HoverCard, HoverCardContent, HoverCardTrigger, Input } from 'ui' +import { + Badge, + cn, + HoverCard, + HoverCardContent, + HoverCardTrigger, + InputGroup, + InputGroupAddon, + InputGroupInput, +} from 'ui' import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock' import { @@ -56,14 +65,17 @@ export const PolicyTemplates = ({ - } - placeholder="Search templates" - value={search} - onChange={(event) => setSearch(event.target.value)} - /> + + setSearch(e.target.value)} + /> + + + + {search.length > 0 && filteredTemplates.length === 0 && ( From 92404788163921cba6e96e51ceb6569806c1f3a0 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 5 May 2026 17:56:59 +0200 Subject: [PATCH 08/10] chore: migrate `Input` usages to Shadcn component in integrations screens/components (#45591) ## Screenshots ### New cron job edge function timeout Before: image After: image ### New cron job http request timeout Before: image After: image ### New queue, partition configuration Before: image After: image ### Queue: send message dialog Before: image After: image ## Summary by CodeRabbit * **Style** * Enhanced input field presentation for timeout, delay, and interval configurations with inline unit labels (milliseconds, seconds, messages) for improved clarity and consistency across integration settings. --- .../CronJobs/EdgeFunctionSection.tsx | 17 ++++++---- .../CronJobs/HttpRequestSection.tsx | 18 ++++++---- .../PartitionConfigFields.tsx | 34 ++++++++++++------- .../Queues/SingleQueue/SendMessageModal.tsx | 23 +++++++++---- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx index 95589d1819..745fea8d77 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/EdgeFunctionSection.tsx @@ -17,7 +17,10 @@ import { FormItem, FormLabel, FormMessage, - Input, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, @@ -192,12 +195,12 @@ export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => { name="values.timeoutMs" render={({ field: { ref, ...rest } }) => ( - ms

} - /> + + + + ms + +
)} /> diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx index 00743443fc..ee964d84b7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/HttpRequestSection.tsx @@ -5,7 +5,11 @@ import { FormItem, FormLabel, FormMessage, - Input, + Input_Shadcn_ as Input, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, @@ -63,12 +67,12 @@ export const HttpRequestSection = ({ form }: HttpRequestSectionProps) => { name="values.timeoutMs" render={({ field: { ref, ...rest } }) => ( - ms

} - /> + + + + ms + +
)} /> diff --git a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx index 1ccb8c5259..7b1a007860 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/CreateQueueSheet/PartitionConfigFields.tsx @@ -1,5 +1,13 @@ import { UseFormReturn } from 'react-hook-form' -import { FormField, Input, Separator, SheetSection } from 'ui' +import { + FormField, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, + Separator, + SheetSection, +} from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CreateQueueForm } from './CreateQueueSheet.schema' @@ -21,12 +29,12 @@ export function PartitionConfigFields({ form }: { form: UseFormReturn - messages

} - /> + + + + messages + + )} /> @@ -39,12 +47,12 @@ export function PartitionConfigFields({ form }: { form: UseFormReturn - messages

} - /> + + + + messages + + )} /> diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx index 0f30424ccd..69db0ad087 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx @@ -3,7 +3,16 @@ import { useParams } from 'common' import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' -import { Form, FormControl, FormField, Input, Modal } from 'ui' +import { + Form, + FormControl, + FormField, + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, + Modal, +} from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' @@ -102,12 +111,12 @@ export const SendMessageModal = ({ visible, onClose }: SendMessageModalProps) => description="Time in seconds before the message becomes available for reading." > - sec

} - /> + + + + sec + +
)} From 2e904abebfda38a24e58433e8667d5d81a2ec8cb Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 5 May 2026 09:57:25 -0600 Subject: [PATCH 09/10] feat(studio): add D + letter shortcuts for Database sub-pages (#45546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds a contextual `D + ` chord pattern for jumping between Database sub-pages, mounted only while `DatabaseLayout` is active. Establishes the pattern we can repeat for other sections (Auth, Storage, Functions, etc.). Linear: [FE-3140](https://linear.app/supabase/issue/FE-3140/define-subnavigation-pattern-for-database-management-page) ## Pattern - Chords are 2-key sequences (`D`, ``) — no global leader, no `G` prefix. - Registration is contextual: `` lives inside `DatabaseLayout`, so the leading `D` is only "owned" while the user is under `/project//database/*`. Doesn't burn a global key. - Hover tooltips on each sub-menu item show the chord, anchored to the label text (Linear-style). Powered by `` already used in the main nav. - Items hidden by feature flags (Roles, Column Privileges, Replication) auto-disable the chord — no muscle-memory navigating to a 404. ## Shortcuts added | Sub-page | Chord | Notes | |---|---|---| | Tables | `D T` | | | Functions | `D F` | | | Triggers | `D R` | t**R**iggers — `T` taken by Tables | | Indexes | `D I` | | | Extensions | `D X` | e**X**tensions | | Schema Visualizer | `D V` | | | Enumerated Types | `D E` | | | Publications | `D U` | p**U**blications — avoids collision with Schema Visualizer's `D P` (Download as PNG) | | Column Privileges | `D C` | flag-gated | | Settings | `D ,` | mirrors global `G ,` for project settings — avoids collision with Schema Visualizer's `D S` (Download as SVG) | | Replication | `D L` | rep**L**ication — flag-gated | | Roles | `D O` | r**O**les — flag-gated | | Backups | `D B` | platform-only | | Migrations | `D M` | | External-link sub-menu items (Policies, Wrappers, Webhooks, Security Advisor, Performance Advisor, Query Performance) are intentionally not chorded — they route out of `/database/*` and don't belong to the section's namespace. ## Collision audit Other shortcuts active on database pages (table-list, schema-visualizer) were checked against the new chords: - **Schema Visualizer** (`/database/schemas`): `D P` (Download PNG), `D S` (Download SVG), `O A`, `O S`. Publications and Settings were remapped to `D U` and `D ,` to avoid the `D P` / `D S` clashes. - **List pages** (`/database/tables`, etc.): `Shift+F`, `Shift+N`, `O S`, `F C` — no overlap with `D + `. ## Files - `state/shortcuts/registry/database-nav.ts` — new registry module with the 14 chord definitions. - `state/shortcuts/registry.ts` — spreads the new IDs/definitions into the canonical registry. - `components/interfaces/DatabaseNavShortcuts.tsx` — null-rendering hook component that wires `useShortcut` for each chord, keyed off `useGenerateDatabaseMenu` so URLs and feature gating stay in sync with the sidebar. - `components/layouts/DatabaseLayout/DatabaseLayout.tsx` — mounts the component. - `components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx` — tags each menu item with its `shortcutId`. - `components/ui/ProductMenu/ProductMenu.types.ts` — adds optional `shortcutId?: ShortcutId` field. - `components/ui/ProductMenu/ProductMenuItem.tsx` — renders the hover tooltip when an item has a `shortcutId`, anchored to the label span. ## Test plan - [ ] On `/project//database/tables`, press `D F` — navigates to `/database/functions`. - [ ] On `/project//database/schemas`, press `D P` — downloads the PNG (Schema Visualizer wins, no nav conflict). - [ ] On `/project//database/schemas`, press `D U` — navigates to `/database/publications`. - [ ] On `/project//database/tables`, press `D ,` — navigates to `/database/settings`. - [ ] Hover any sub-menu item with a chord — pill appears next to the label after ~1s. - [ ] On a project with the Replication flag off — `D L` does nothing. - [ ] Navigate to `/auth` — pressing `D F` does nothing (chord unmounts with the layout). - [ ] Type `D` then `F` slowly inside an input — does not navigate (input-focus guard). --- .../interfaces/DatabaseNavShortcuts.tsx | 96 ++++++++++++++ .../layouts/DatabaseLayout/DatabaseLayout.tsx | 2 + .../DatabaseLayout/DatabaseMenu.utils.tsx | 81 ++++++++++-- .../ui/ProductMenu/ProductMenu.types.ts | 3 + .../ui/ProductMenu/ProductMenuItem.tsx | 17 ++- apps/studio/state/shortcuts/registry.ts | 7 ++ .../state/shortcuts/registry/database-nav.ts | 117 ++++++++++++++++++ 7 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 apps/studio/components/interfaces/DatabaseNavShortcuts.tsx create mode 100644 apps/studio/state/shortcuts/registry/database-nav.ts diff --git a/apps/studio/components/interfaces/DatabaseNavShortcuts.tsx b/apps/studio/components/interfaces/DatabaseNavShortcuts.tsx new file mode 100644 index 0000000000..1b524aeaa7 --- /dev/null +++ b/apps/studio/components/interfaces/DatabaseNavShortcuts.tsx @@ -0,0 +1,96 @@ +import { useRouter } from 'next/router' +import { useCallback, useMemo } from 'react' + +import { useGenerateDatabaseMenu } from '@/components/layouts/DatabaseLayout/DatabaseMenu.utils' +import { SHORTCUT_IDS, type ShortcutId } from '@/state/shortcuts/registry' +import { useShortcut } from '@/state/shortcuts/useShortcut' + +export const DatabaseNavShortcuts = () => { + const router = useRouter() + const groups = useGenerateDatabaseMenu() + + const urlByShortcut = useMemo(() => { + const map = new Map() + for (const group of groups) { + for (const item of group.items) { + if (item.shortcutId && item.url) map.set(item.shortcutId, item.url) + } + } + return map + }, [groups]) + + const navigate = useCallback( + (id: ShortcutId) => { + const url = urlByShortcut.get(id) + if (url) router.push(url) + }, + [router, urlByShortcut] + ) + + useShortcut(SHORTCUT_IDS.NAV_DATABASE_TABLES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_TABLES), { + enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TABLES), + }) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_TRIGGERS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_TRIGGERS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TRIGGERS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_INDEXES, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_INDEXES), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_INDEXES) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER) } + ) + useShortcut(SHORTCUT_IDS.NAV_DATABASE_ROLES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_ROLES), { + enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_ROLES), + }) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_BACKUPS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_BACKUPS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_BACKUPS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS) } + ) + useShortcut(SHORTCUT_IDS.NAV_DATABASE_TYPES, () => navigate(SHORTCUT_IDS.NAV_DATABASE_TYPES), { + enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_TYPES), + }) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_SETTINGS, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_SETTINGS), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_SETTINGS) } + ) + useShortcut( + SHORTCUT_IDS.NAV_DATABASE_REPLICATION, + () => navigate(SHORTCUT_IDS.NAV_DATABASE_REPLICATION), + { enabled: urlByShortcut.has(SHORTCUT_IDS.NAV_DATABASE_REPLICATION) } + ) + + return null +} diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 30db23926f..7365cd2c3c 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -3,6 +3,7 @@ import type { PropsWithChildren } from 'react' import { ProjectLayout } from '../ProjectLayout' import { useGenerateDatabaseMenu } from './DatabaseMenu.utils' +import { DatabaseNavShortcuts } from '@/components/interfaces/DatabaseNavShortcuts' import { ProductMenu } from '@/components/ui/ProductMenu' import { withAuth } from '@/hooks/misc/withAuth' @@ -26,6 +27,7 @@ const DatabaseLayout = ({ children, title }: PropsWithChildren} isBlocking={false} > + {children} ) diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx index b42d5ce4e7..f8f93ed79a 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx @@ -12,6 +12,7 @@ import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { IS_PLATFORM } from '@/lib/constants' +import { SHORTCUT_IDS } from '@/state/shortcuts/registry' const ExternalLinkIcon = @@ -42,24 +43,70 @@ export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => { { title: 'Database Management', 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') }, + { + name: 'Schema Visualizer', + key: 'schemas', + url: getDatabaseURL('schemas'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER, + }, + { + name: 'Tables', + key: 'tables', + url: getDatabaseURL('tables'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_TABLES, + }, + { + name: 'Functions', + key: 'functions', + url: getDatabaseURL('functions'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS, + }, + { + name: 'Triggers', + key: 'triggers', + url: getDatabaseURL('triggers/data'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_TRIGGERS, + }, + { + name: 'Enumerated Types', + key: 'types', + url: getDatabaseURL('types'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_TYPES, + }, + { + name: 'Extensions', + key: 'extensions', + url: getDatabaseURL('extensions'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS, + }, + { + name: 'Indexes', + key: 'indexes', + url: getDatabaseURL('indexes'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_INDEXES, + }, + { + name: 'Publications', + key: 'publications', + url: getDatabaseURL('publications'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS, + }, ], }, { title: 'Configuration', items: [ - showRoles && { name: 'Roles', key: 'roles', url: getDatabaseURL('roles') }, + showRoles && { + name: 'Roles', + key: 'roles', + url: getDatabaseURL('roles'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_ROLES, + }, columnLevelPrivileges && { name: 'Column Privileges', key: 'column-privileges', url: getDatabaseURL('column-privileges'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES, }, { name: 'Policies', @@ -67,7 +114,12 @@ export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => { url: `/project/${ref}/auth/policies`, rightIcon: ExternalLinkIcon, }, - { name: 'Settings', key: 'settings', url: getDatabaseURL('settings') }, + { + name: 'Settings', + key: 'settings', + url: getDatabaseURL('settings'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_SETTINGS, + }, ].filter(Boolean) as ProductMenuGroupItem[], }, { @@ -79,13 +131,20 @@ export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => { key: 'replication', url: getDatabaseURL('replication'), label: enablePgReplicate ? 'New' : undefined, + shortcutId: SHORTCUT_IDS.NAV_DATABASE_REPLICATION, }, IS_PLATFORM && { name: 'Backups', key: 'backups', url: pitrEnabled ? getDatabaseURL('backups/pitr') : getDatabaseURL('backups/scheduled'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_BACKUPS, + }, + { + name: 'Migrations', + key: 'migrations', + url: getDatabaseURL('migrations'), + shortcutId: SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS, }, - { name: 'Migrations', key: 'migrations', url: getDatabaseURL('migrations') }, showWrappers && { name: 'Wrappers', key: 'wrappers', diff --git a/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts b/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts index 477022c9b2..54a8f3fcff 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts +++ b/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts @@ -1,5 +1,7 @@ import { ReactNode } from 'react' +import type { ShortcutId } from '@/state/shortcuts/registry' + export interface ProductMenuGroup { title?: string /** Set to "main" if page is on a '/' route */ @@ -25,6 +27,7 @@ export interface ProductMenuGroupItem { childIcon?: ReactNode childItems?: ProductMenuGroupItem[] pages?: string[] + shortcutId?: ShortcutId } /** diff --git a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx index eb751c6891..53b02d6804 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { Badge, Button, Menu } from 'ui' import { ProductMenuGroupItem } from './ProductMenu.types' +import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip' interface ProductMenuItemProps { item: ProductMenuGroupItem @@ -18,16 +19,26 @@ export const ProductMenuItem = ({ hoverText = '', onClick, }: ProductMenuItemProps) => { - const { name = '', url = '', icon, rightIcon, isExternal, label, disabled } = item + const { name = '', url = '', icon, rightIcon, isExternal, label, disabled, shortcutId } = item + + const labelNode = shortcutId ? ( + + {name} + + ) : ( + {name} + ) const menuItem = (
- {name} + {labelNode} {label !== undefined && ( = { // Shared list-page shortcut registration ...listPageRegistry, + + // Database sub-page navigation chord registration + ...databaseNavRegistry, } diff --git a/apps/studio/state/shortcuts/registry/database-nav.ts b/apps/studio/state/shortcuts/registry/database-nav.ts new file mode 100644 index 0000000000..9a437c3b39 --- /dev/null +++ b/apps/studio/state/shortcuts/registry/database-nav.ts @@ -0,0 +1,117 @@ +import { RegistryDefinations } from '../types' + +/** + * Contextual chords for jumping between Database sub-pages — `D + `. + * + * Active only while DatabaseLayout is mounted (i.e. the user is somewhere + * under `/project//database/*`). The chord intentionally lives on the + * page rather than globally so the leading `D` doesn't burn a global key for + * a destination most users only care about while already in the section. + * + */ +export const DATABASE_NAV_SHORTCUT_IDS = { + NAV_DATABASE_TABLES: 'nav.database-tables', + NAV_DATABASE_FUNCTIONS: 'nav.database-functions', + NAV_DATABASE_TRIGGERS: 'nav.database-triggers', + NAV_DATABASE_INDEXES: 'nav.database-indexes', + NAV_DATABASE_EXTENSIONS: 'nav.database-extensions', + NAV_DATABASE_SCHEMA_VISUALIZER: 'nav.database-schema-visualizer', + NAV_DATABASE_ROLES: 'nav.database-roles', + NAV_DATABASE_BACKUPS: 'nav.database-backups', + NAV_DATABASE_MIGRATIONS: 'nav.database-migrations', + NAV_DATABASE_TYPES: 'nav.database-types', + NAV_DATABASE_PUBLICATIONS: 'nav.database-publications', + NAV_DATABASE_COLUMN_PRIVILEGES: 'nav.database-column-privileges', + NAV_DATABASE_SETTINGS: 'nav.database-settings', + NAV_DATABASE_REPLICATION: 'nav.database-replication', +} + +export type DatabaseNavShortcutId = + (typeof DATABASE_NAV_SHORTCUT_IDS)[keyof typeof DATABASE_NAV_SHORTCUT_IDS] + +export const databaseNavRegistry: RegistryDefinations = { + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TABLES]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TABLES, + label: 'Go to Tables', + sequence: ['D', 'T'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_FUNCTIONS, + label: 'Go to Functions', + sequence: ['D', 'F'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TRIGGERS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TRIGGERS, + label: 'Go to Triggers', + sequence: ['D', 'R'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_INDEXES]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_INDEXES, + label: 'Go to Indexes', + sequence: ['D', 'I'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_EXTENSIONS, + label: 'Go to Extensions', + sequence: ['D', 'X'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_SCHEMA_VISUALIZER, + label: 'Go to Schema Visualizer', + sequence: ['D', 'V'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_ROLES]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_ROLES, + label: 'Go to Roles', + sequence: ['D', 'O'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_BACKUPS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_BACKUPS, + label: 'Go to Backups', + sequence: ['D', 'B'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_MIGRATIONS, + label: 'Go to Migrations', + sequence: ['D', 'M'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TYPES]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_TYPES, + label: 'Go to Enumerated Types', + sequence: ['D', 'E'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_PUBLICATIONS, + label: 'Go to Publications', + sequence: ['D', 'U'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_COLUMN_PRIVILEGES, + label: 'Go to Column Privileges', + sequence: ['D', 'C'], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_SETTINGS]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_SETTINGS, + label: 'Go to Database Settings', + sequence: ['D', ','], + showInSettings: false, + }, + [DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_REPLICATION]: { + id: DATABASE_NAV_SHORTCUT_IDS.NAV_DATABASE_REPLICATION, + label: 'Go to Replication', + sequence: ['D', 'L'], + showInSettings: false, + }, +} From c26b64a033a9e7094aa249e19f4c8ef1bcdd048e Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Tue, 5 May 2026 23:57:53 +0800 Subject: [PATCH 10/10] feat(www): emit BreadcrumbList JSON-LD on marketing surfaces (#45478) ## Summary - Adds `breadcrumbListSchema(items)` helper to `apps/www/lib/json-ld.ts` and a hand-curated `apps/www/lib/breadcrumbs.ts` route map. - Wires inline `