From 3ea6fcae90aca8876ca9ba178267909bc23eb645 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:40:52 -0600 Subject: [PATCH 001/488] chore(studio): remove unused API authorization layout (#46962) ## What kind of change does this PR introduce? Code cleanup. Resolves DEPR-570. ## What is the current behavior? The old `APIAuthorizationLayout` component still exists in Studio even though the API/OAuth, CLI, Stripe, invite, credits, and AWS connect surfaces have moved onto the shared interstitial layout. ## What is the new behavior? Removes the unused `APIAuthorizationLayout` component. `IntegrationWindowLayout` and `VercelIntegrationWindowLayout` are still required for upcoming Vercel connection UI clean up work. ## Summary by CodeRabbit * **Refactor** * Removed unused layout component from the API authorization flow, streamlining the codebase and improving maintainability. --- .../layouts/APIAuthorizationLayout.tsx | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 apps/studio/components/layouts/APIAuthorizationLayout.tsx diff --git a/apps/studio/components/layouts/APIAuthorizationLayout.tsx b/apps/studio/components/layouts/APIAuthorizationLayout.tsx deleted file mode 100644 index cbd890004c0..00000000000 --- a/apps/studio/components/layouts/APIAuthorizationLayout.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useTheme } from 'next-themes' -import Image from 'next/legacy/image' -import Link from 'next/link' -import type { PropsWithChildren } from 'react' -import { Separator } from 'ui' - -import { Head, type HeadProvider } from '@/components/ui/Head' -import { useCustomContent } from '@/hooks/custom-content/useCustomContent' -import { BASE_PATH } from '@/lib/constants' - -export interface APIAuthorizationLayoutProps { - HeadProvider: HeadProvider -} - -export const APIAuthorizationLayout = ({ - HeadProvider, - children, -}: PropsWithChildren) => { - const { resolvedTheme } = useTheme() - const { appTitle } = useCustomContent(['app:title']) - - return ( - <> - -
-
-
-
-
- - Supabase - Supabase Logo - -
-
-
-
- -
- {children} -
-
- - ) -} From f761c66a9f407daeb148e9b4410283eb160bb50b Mon Sep 17 00:00:00 2001 From: "supabase-supabase-autofixer[bot]" <248690971+supabase-supabase-autofixer[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:19:28 +0300 Subject: [PATCH 002/488] feat: update @supabase/*-js libraries to v2.108.2 (#46927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates @supabase/*-js libraries to version 2.108.2. **Source**: supabase-js-stable-release **Changes**: - Updated @supabase/supabase-js to 2.108.2 - Updated @supabase/auth-js to 2.108.2 - Updated @supabase/realtime-js to 2.108.2 - Updated @supabase/postgest-js to 2.108.2 - Refreshed pnpm-lock.yaml --- ## Release Notes ## v2.108.2 ## 2.108.2 (2026-06-15) ### 🩹 Fixes - **auth:** preserve valid session on refresh failure and cooldown repeat failures ([#2436](https://github.com/supabase/supabase-js/pull/2436)) - **realtime:** clarify httpSend() 404 error and server migration note ([#2444](https://github.com/supabase/supabase-js/pull/2444)) - **release:** pin Deno and bound JSR publish to survive stranded-task hangs ([#2439](https://github.com/supabase/supabase-js/pull/2439)) - **release:** restore JSR publish flags and enable for beta ([#2440](https://github.com/supabase/supabase-js/pull/2440)) ### ❤️ Thank You - Katerina Skroumpelou @mandarini ## v2.108.1 ## 2.108.1 (2026-06-09) ### 🩹 Fixes - **ci:** forward DOGFOOD_APP_CLIENT_ID to dogfood workflow ([#2434](https://github.com/supabase/supabase-js/pull/2434)) - **postgrest:** then typing ([#2349](https://github.com/supabase/supabase-js/pull/2349)) ### ❤️ Thank You - Katerina Skroumpelou @mandarini - Vaibhav @7ttp This PR was created automatically. Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 102 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 8 ++-- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13832b6f1f0..0d47db28815 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,20 +10,20 @@ catalogs: specifier: ^10.26.0 version: 10.27.0 '@supabase/auth-js': - specifier: 2.108.0 - version: 2.108.0 + specifier: 2.108.2 + version: 2.108.2 '@supabase/postgrest-js': - specifier: 2.108.0 - version: 2.108.0 + specifier: 2.108.2 + version: 2.108.2 '@supabase/realtime-js': - specifier: 2.108.0 - version: 2.108.0 + specifier: 2.108.2 + version: 2.108.2 '@supabase/ssr': specifier: 0.10.2 version: 0.10.2 '@supabase/supabase-js': - specifier: 2.108.0 - version: 2.108.0 + specifier: 2.108.2 + version: 2.108.2 '@types/node': specifier: ^22.0.0 version: 22.13.14 @@ -353,7 +353,7 @@ importers: 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@15.5.18(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2)) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@tanstack/react-query': specifier: ^5.13.4 version: 5.83.0(react@19.2.6) @@ -938,7 +938,7 @@ importers: version: 9.1.0 '@supabase/auth-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@supabase/mcp-server-supabase': specifier: ^0.7.0 version: 0.7.0(@modelcontextprotocol/sdk@1.29.0(supports-color@8.1.1)(zod@3.25.76))(zod@3.25.76) @@ -947,7 +947,7 @@ importers: version: link:../../packages/pg-meta '@supabase/realtime-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@supabase/shared-types': specifier: 0.1.88 version: 0.1.88 @@ -956,7 +956,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@tanstack/react-hotkeys': specifier: ^0.9.1 version: 0.9.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1383,7 +1383,7 @@ importers: version: 0.1.0 '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -1528,10 +1528,10 @@ importers: version: 1.6.0 '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.108.0) + version: 0.10.2(@supabase/supabase-js@2.108.2) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@tanstack/react-router': specifier: ^1.168.0 version: 1.168.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1642,10 +1642,10 @@ importers: 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@15.5.18(@babel/core@7.29.0(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.25.2)) '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.108.0) + version: 0.10.2(@supabase/supabase-js@2.108.2) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@vercel/og': specifier: ^0.6.2 version: 0.6.2 @@ -1898,13 +1898,13 @@ importers: dependencies: '@supabase/postgrest-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@supabase/ssr': specifier: 'catalog:' - version: 0.10.2(@supabase/supabase-js@2.108.0) + version: 0.10.2(@supabase/supabase-js@2.108.2) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@vueuse/core': specifier: ^14.1.0 version: 14.1.0(vue@3.5.35(typescript@6.0.2)) @@ -1950,7 +1950,7 @@ importers: version: 1.59.1 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 cross-fetch: specifier: ^4.1.0 version: 4.1.0(encoding@0.1.13) @@ -1978,7 +1978,7 @@ importers: version: 0.18.5 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 ai: specifier: 5.0.52 version: 5.0.52(zod@3.25.76) @@ -2069,10 +2069,10 @@ importers: dependencies: '@supabase/auth-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@types/dat.gui': specifier: ^0.7.12 version: 0.7.12 @@ -2520,7 +2520,7 @@ importers: version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1) '@supabase/supabase-js': specifier: 'catalog:' - version: 2.108.0 + version: 2.108.2 '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -7720,12 +7720,12 @@ packages: '@supabase-labs/y-supabase@0.1.0': resolution: {integrity: sha512-1ItaKbcImgy7ueYfJcyVYihqCKi9RAee9MKBV8wcCqSMNsrWvw4ibhW8ai91kWsewsiWzIDXVPONgcUcxagk9g==} - '@supabase/auth-js@2.108.0': - resolution: {integrity: sha512-0CzVGVqHfNOhRQVEcAmu58Mex2Ce0zL3aGfyV+iFQjTK6OntLK/hLCLr/VDRX0E8/2CdsiY99L7fZ/8ys/op4w==} + '@supabase/auth-js@2.108.2': + resolution: {integrity: sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==} engines: {node: '>=20.0.0'} - '@supabase/functions-js@2.108.0': - resolution: {integrity: sha512-lqEGDzT7QBUuKYzi5lHpV/XecXT9wikzcbXMbFo6krNpSDynD1sHM8wcsfB/BAqa4NkFuy3vF4JCV8MeakV8IQ==} + '@supabase/functions-js@2.108.2': + resolution: {integrity: sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==} engines: {node: '>=20.0.0'} '@supabase/mcp-server-supabase@0.7.0': @@ -7744,12 +7744,12 @@ packages: '@supabase/phoenix@0.4.2': resolution: {integrity: sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==} - '@supabase/postgrest-js@2.108.0': - resolution: {integrity: sha512-8AwTkPqowDYv/qh016CyXeZ3Ukpw6NHyfqc7DWV4afLR2hAiapf3zRKV2ZLG+//T1LK84HrR6X8VBwfgHWmNyw==} + '@supabase/postgrest-js@2.108.2': + resolution: {integrity: sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==} engines: {node: '>=20.0.0'} - '@supabase/realtime-js@2.108.0': - resolution: {integrity: sha512-N3xR0u7TNr+c5wuLSU60rcfu/H/8N0WBs7iHWwjI/NxKwY3XWSyLUbpbpU8bzmL0dA/Gk9Mupri8mxKUXBW+iw==} + '@supabase/realtime-js@2.108.2': + resolution: {integrity: sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==} engines: {node: '>=20.0.0'} '@supabase/shared-types@0.1.88': @@ -7763,8 +7763,8 @@ packages: peerDependencies: '@supabase/supabase-js': ^2.102.1 - '@supabase/storage-js@2.108.0': - resolution: {integrity: sha512-zMYQmh87CId7d8i/1FIfv4fMDcXPutmIJSpoY58GLXi7M266MNzxWGNabDk4i555Oj1Nqtsu2i3Qo3rpWUXO6A==} + '@supabase/storage-js@2.108.2': + resolution: {integrity: sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==} engines: {node: '>=20.0.0'} '@supabase/supa-mdx-lint-darwin@0.2.6-alpha': @@ -7902,8 +7902,8 @@ packages: resolution: {integrity: sha512-iqJHDk/ToyxFMa/um9A5gsp9jYN7iI0cIabNq9nyBpK/Yat/J9I2IIMueQwIMy3TsG6a2qdPyUkZkuPC37SdRg==} hasBin: true - '@supabase/supabase-js@2.108.0': - resolution: {integrity: sha512-AjPoimM9MZLZbddnlDBGmpZ/Tas1dNcJvuZy/VD1AfmrjBC8J2RSw6UOqR4ISLLlEioOLca/5t1crFnAxa0wRQ==} + '@supabase/supabase-js@2.108.2': + resolution: {integrity: sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==} engines: {node: '>=20.0.0'} '@swc/helpers@0.5.15': @@ -23993,15 +23993,15 @@ snapshots: '@supabase-labs/y-supabase@0.1.0': dependencies: - '@supabase/supabase-js': 2.108.0 + '@supabase/supabase-js': 2.108.2 y-protocols: 1.0.7(yjs@13.6.29) yjs: 13.6.29 - '@supabase/auth-js@2.108.0': + '@supabase/auth-js@2.108.2': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.108.0': + '@supabase/functions-js@2.108.2': dependencies: tslib: 2.8.1 @@ -24023,11 +24023,11 @@ snapshots: '@supabase/phoenix@0.4.2': {} - '@supabase/postgrest-js@2.108.0': + '@supabase/postgrest-js@2.108.2': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.108.0': + '@supabase/realtime-js@2.108.2': dependencies: '@supabase/phoenix': 0.4.2 tslib: 2.8.1 @@ -24043,12 +24043,12 @@ snapshots: - encoding - supports-color - '@supabase/ssr@0.10.2(@supabase/supabase-js@2.108.0)': + '@supabase/ssr@0.10.2(@supabase/supabase-js@2.108.2)': dependencies: - '@supabase/supabase-js': 2.108.0 + '@supabase/supabase-js': 2.108.2 cookie: 1.0.2 - '@supabase/storage-js@2.108.0': + '@supabase/storage-js@2.108.2': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 @@ -24149,13 +24149,13 @@ snapshots: '@supabase/supa-mdx-lint-win32-x64': 0.3.2 node-pty: 1.0.0 - '@supabase/supabase-js@2.108.0': + '@supabase/supabase-js@2.108.2': dependencies: - '@supabase/auth-js': 2.108.0 - '@supabase/functions-js': 2.108.0 - '@supabase/postgrest-js': 2.108.0 - '@supabase/realtime-js': 2.108.0 - '@supabase/storage-js': 2.108.0 + '@supabase/auth-js': 2.108.2 + '@supabase/functions-js': 2.108.2 + '@supabase/postgrest-js': 2.108.2 + '@supabase/realtime-js': 2.108.2 + '@supabase/storage-js': 2.108.2 '@swc/helpers@0.5.15': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9bd4fbdbe06..be8053a81d5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,11 +8,11 @@ blockExoticSubdeps: true catalog: '@sentry/nextjs': ^10.26.0 - '@supabase/auth-js': 2.108.0 - '@supabase/postgrest-js': 2.108.0 - '@supabase/realtime-js': 2.108.0 + '@supabase/auth-js': 2.108.2 + '@supabase/postgrest-js': 2.108.2 + '@supabase/realtime-js': 2.108.2 '@supabase/ssr': 0.10.2 - '@supabase/supabase-js': 2.108.0 + '@supabase/supabase-js': 2.108.2 '@types/node': ^22.0.0 '@types/react': ^19.2.14 '@types/react-dom': ^19.2.3 From a1cc7294b291979f0e94a78155ad818eceb0d23d Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Tue, 16 Jun 2026 09:19:41 +0200 Subject: [PATCH 003/488] docs: Add the ability to individually toggle AI features (#46807) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## Summary by CodeRabbit * **New Features** * Agent Plugin, Agent Skills, and Prompts menu items added to navigation (now feature-gated). * **Documentation** * AI Prompts and Agent Skills sections are now conditionally shown in the docs based on feature flags. * **Chores** * New feature flags for docs (Agent Plugin, Agent Skills, Prompts) added and enabled in config/schema. --- .../NavigationMenu/NavigationMenu.constants.ts | 9 +++++++++ apps/docs/content/guides/ai-tools/mcp.mdx | 8 ++++++++ .../common/enabled-features/enabled-features.json | 3 +++ .../enabled-features/enabled-features.schema.json | 12 ++++++++++++ 4 files changed, 32 insertions(+) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 925f25838c6..d4ce3c8084f 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -31,6 +31,9 @@ const { sdkKotlin: sdkKotlinEnabled, sdkPython: sdkPythonEnabled, sdkSwift: sdkSwiftEnabled, + docsAgentPlugin: agentPluginEnabled, + docsAgentSkills: agentSkillsEnabled, + docsPrompts: promptsEnabled, } = isFeatureEnabled([ 'authentication:show_providers', 'billing:all', @@ -56,6 +59,9 @@ const { 'sdk:kotlin', 'sdk:python', 'sdk:swift', + 'docs:agent_plugin', + 'docs:agent_skills', + 'docs:prompts', ]) const aiToolsEnabled = true @@ -2395,6 +2401,7 @@ export const ai_tools: NavMenuConstant = { { name: 'Agent Plugin', url: '/guides/ai-tools/plugins' as `/${string}`, + enabled: agentPluginEnabled, }, { name: 'MCP server', @@ -2403,10 +2410,12 @@ export const ai_tools: NavMenuConstant = { { name: 'Agent Skills', url: '/guides/ai-tools/ai-skills' as `/${string}`, + enabled: agentSkillsEnabled, }, { name: 'Prompts', url: '/guides/ai-tools/ai-prompts' as `/${string}`, + enabled: promptsEnabled, }, { diff --git a/apps/docs/content/guides/ai-tools/mcp.mdx b/apps/docs/content/guides/ai-tools/mcp.mdx index bec493599d4..33126fc0707 100644 --- a/apps/docs/content/guides/ai-tools/mcp.mdx +++ b/apps/docs/content/guides/ai-tools/mcp.mdx @@ -28,10 +28,18 @@ After you log in, check that the MCP server is connected. For instance, in Curso To verify the client has access to the MCP server tools, try asking it to query your project or database using natural language. For example: "What tables are there in the database? Use MCP tools." +<$Show if="docs:prompts"> + For curated, ready-to-use prompts that work well with IDEs and AI agents, see our [AI Prompts](/docs/guides/getting-started/ai-prompts) collection. + + +<$Show if="docs:agent_skills"> + Additionally, you can install Supabase agent skills alongside the MCP server, use the [Supabase Plugin for AI Coding Agents](/docs/guides/getting-started/plugins) for a combined one-step setup. + + ## Available tools The Supabase MCP server provides tools organized into feature groups. All groups except Storage are enabled by default. You can enable or disable specific groups using the [configuration panel above](#step-2-configure-your-ai-tool). diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index 579eae29e4f..7e4fcb40075 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -64,6 +64,9 @@ "docs:pgtap": true, "docs:production_checklist": true, "docs:web_apps": true, + "docs:agent_plugin": true, + "docs:agent_skills": true, + "docs:prompts": true, "edge_functions:show_stripe_example": true, "edge_functions:show_all_edge_function_invocation_examples": true, diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index 8ce7bab0442..0a63c3e2b63 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -229,6 +229,18 @@ "type": "boolean", "description": "Enable web apps getting started documentation" }, + "docs:agent_plugin": { + "type": "boolean", + "description": "Enable documentation for the agent plugin" + }, + "docs:agent_skills": { + "type": "boolean", + "description": "Enable documentation for agent skills" + }, + "docs:prompts": { + "type": "boolean", + "description": "Enable documentation for prompts" + }, "edge_functions:show_stripe_example": { "type": "boolean", From 0717e1a8cb7d809b345dfb0c086f557dd71fab96 Mon Sep 17 00:00:00 2001 From: Bruno Kilian Date: Tue, 16 Jun 2026 09:22:14 +0200 Subject: [PATCH 004/488] Update humans.txt to include Bruno Kilian (#46963) Update humans.txt to include Bruno Kilian ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update: Adding to humans.txt ## What is the current behavior? n/a ## What is the new behavior? n/a ## Additional context n/a ## Summary by CodeRabbit * **Chores** * Updated team credits to include a new contributor. --- 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 bd189faec62..41d2978b64b 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -40,6 +40,7 @@ Brendan Stephens Brent Newson Brian Cullen Brent Schroeter +Bruno Kilian Carel de Waal Cameron Blackwood Cameron Michie From 7d4ec098ea50ec31bcde8f4e83af75936c801a47 Mon Sep 17 00:00:00 2001 From: "supabase-supabase-autofixer[bot]" <248690971+supabase-supabase-autofixer[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:22:42 +0200 Subject: [PATCH 005/488] feat: update mgmt api docs (#46955) This PR updates mgmt api docs automatically. Co-authored-by: jbergstroem <176984+jbergstroem@users.noreply.github.com> --- apps/docs/spec/api_v1_openapi.json | 110 +++- apps/docs/spec/api_v2_openapi.json | 318 +++++++++ apps/docs/spec/common-api-sections.json | 18 + .../transforms/api_v1_openapi_deparsed.json | 306 ++++++++- .../transforms/api_v2_openapi_deparsed.json | 616 ++++++++++++++++++ 5 files changed, 1344 insertions(+), 24 deletions(-) diff --git a/apps/docs/spec/api_v1_openapi.json b/apps/docs/spec/api_v1_openapi.json index 3fcc8254819..5cbed875278 100644 --- a/apps/docs/spec/api_v1_openapi.json +++ b/apps/docs/spec/api_v1_openapi.json @@ -700,6 +700,7 @@ }, "/v1/oauth/token": { "post": { + "description": "Supports `authorization_code`, `refresh_token`, and `urn:ietf:params:oauth:grant-type:jwt-bearer` grant types. The `jwt-bearer` grant type (IDJAG — identity-directed JWT assertion) is in beta and available on Team and Enterprise plans only.", "operationId": "v1-exchange-oauth-token", "parameters": [], "requestBody": { @@ -3222,7 +3223,7 @@ } }, "responses": { - "201": { "description": "" }, + "204": { "description": "" }, "401": { "description": "Unauthorized" }, "402": { "description": "This feature requires the Pro, Team, or Enterprise organization plan." @@ -3266,7 +3267,7 @@ } }, "responses": { - "201": { "description": "" }, + "204": { "description": "" }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden action" }, "429": { "description": "Rate limit exceeded" }, @@ -3956,6 +3957,38 @@ "x-oauth-scope": "projects:write" } }, + "/v1/projects/{ref}/restart": { + "post": { + "operationId": "v1-restart-a-project", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { "description": "" }, + "401": { "description": "Unauthorized" }, + "403": { "description": "Forbidden action" }, + "429": { "description": "Rate limit exceeded" } + }, + "security": [{ "bearer": [] }, { "fga_permissions": ["project_admin_write"] }], + "summary": "Restarts the given project", + "tags": ["Projects"], + "x-badges": [{ "name": "OAuth scope: projects:write", "position": "after" }], + "x-endpoint-owners": ["infra", "management-api"], + "x-oauth-scope": "projects:write" + } + }, "/v1/projects/{ref}/restore": { "get": { "operationId": "v1-list-available-restore-versions", @@ -7965,13 +7998,24 @@ "OAuthTokenBody": { "type": "object", "properties": { - "grant_type": { "type": "string", "enum": ["authorization_code", "refresh_token"] }, + "grant_type": { + "type": "string", + "enum": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:jwt-bearer" + ] + }, "client_id": { "type": "string", "format": "uuid" }, "client_secret": { "type": "string" }, "code": { "type": "string" }, "code_verifier": { "type": "string" }, "redirect_uri": { "type": "string" }, "refresh_token": { "type": "string" }, + "assertion": { + "type": "string", + "description": "IDJAG assertion JWT for grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Beta - available on Team and Enterprise plans only." + }, "resource": { "type": "string", "format": "uri", @@ -7994,11 +8038,14 @@ "type": "object", "properties": { "access_token": { "type": "string" }, - "refresh_token": { "type": "string" }, + "refresh_token": { + "type": "string", + "description": "The `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type issues access tokens only, no refresh token is returned and the token cannot be revoked via `/v1/oauth/revoke`." + }, "expires_in": { "type": "integer" }, "token_type": { "type": "string", "enum": ["Bearer"] } }, - "required": ["access_token", "refresh_token", "expires_in", "token_type"], + "required": ["access_token", "expires_in", "token_type"], "additionalProperties": false }, "OAuthRevokeTokenBody": { @@ -10309,7 +10356,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "variant": { @@ -10345,7 +10393,8 @@ { "type": "string", "enum": ["ipv4_default"] }, { "type": "string", "enum": ["auth_mfa_phone_default"] }, { "type": "string", "enum": ["auth_mfa_web_authn_default"] }, - { "type": "string", "enum": ["log_drain_default"] } + { "type": "string", "enum": ["log_drain_default"] }, + { "type": "string", "enum": ["etl_pipeline_default"] } ] }, "name": { "type": "string" }, @@ -10381,7 +10430,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "name": { "type": "string" }, @@ -10420,7 +10470,8 @@ { "type": "string", "enum": ["ipv4_default"] }, { "type": "string", "enum": ["auth_mfa_phone_default"] }, { "type": "string", "enum": ["auth_mfa_web_authn_default"] }, - { "type": "string", "enum": ["log_drain_default"] } + { "type": "string", "enum": ["log_drain_default"] }, + { "type": "string", "enum": ["etl_pipeline_default"] } ] }, "name": { "type": "string" }, @@ -10488,7 +10539,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] } }, @@ -11525,6 +11577,25 @@ "properties": { "effective_cache_size": { "type": "string" }, "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { "type": "boolean" }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { "type": "boolean" }, + "log_connections": { "type": "boolean" }, + "log_disconnections": { "type": "boolean" }, + "log_duration": { "type": "boolean" }, + "log_lock_waits": { "type": "boolean" }, + "log_recovery_conflict_waits": { "type": "boolean" }, + "log_replication_commands": { "type": "boolean" }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { "type": "string" }, "maintenance_work_mem": { "type": "string" }, "track_activity_query_size": { "type": "string" }, "max_connections": { "type": "integer", "minimum": 1, "maximum": 262143 }, @@ -11567,6 +11638,25 @@ "properties": { "effective_cache_size": { "type": "string" }, "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { "type": "boolean" }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { "type": "boolean" }, + "log_connections": { "type": "boolean" }, + "log_disconnections": { "type": "boolean" }, + "log_duration": { "type": "boolean" }, + "log_lock_waits": { "type": "boolean" }, + "log_recovery_conflict_waits": { "type": "boolean" }, + "log_replication_commands": { "type": "boolean" }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { "type": "string" }, "maintenance_work_mem": { "type": "string" }, "track_activity_query_size": { "type": "string" }, "max_connections": { "type": "integer", "minimum": 1, "maximum": 262143 }, diff --git a/apps/docs/spec/api_v2_openapi.json b/apps/docs/spec/api_v2_openapi.json index 464289a5721..a576b8f8cf3 100644 --- a/apps/docs/spec/api_v2_openapi.json +++ b/apps/docs/spec/api_v2_openapi.json @@ -265,6 +265,135 @@ "tags": ["Projects"], "x-endpoint-owners": ["management-api"] } + }, + "/v2/organizations/{slug}/members": { + "get": { + "description": "Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.", + "operationId": "v2-list-organization-members", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Organization slug", + "schema": { + "pattern": "^[\\w-]+$", + "example": "tsrqponmlkjihgfedcba", + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "properties": { + "size": { "type": "integer", "minimum": 1, "maximum": 100, "required": false }, + "after": { "type": "string", "format": "uuid", "required": false }, + "before": { "type": "string", "format": "uuid", "required": false } + }, + "type": "object" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "schema": { + "properties": { + "username": { "type": "string", "required": false }, + "primary_email": { "type": "string", "format": "email", "required": false } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2ListMembersResponse" } + } + } + }, + "401": { "description": "Unauthorized" }, + "403": { "description": "Forbidden action" }, + "429": { "description": "Rate limit exceeded" } + }, + "security": [{ "bearer": [] }, { "fga_permissions": ["members_read"] }], + "summary": "List members of an organization", + "tags": ["Organizations"], + "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], + "x-endpoint-owners": ["management-api"], + "x-oauth-scope": "organizations:read" + } + }, + "/v2/organizations/{slug}/roles": { + "get": { + "description": "Returns a cursor-paginated list of organization roles including both org-scoped and project-scoped roles.", + "operationId": "v2-list-organization-roles", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Organization slug", + "schema": { + "pattern": "^[\\w-]+$", + "example": "tsrqponmlkjihgfedcba", + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "properties": { + "size": { "type": "integer", "minimum": 1, "maximum": 100, "required": false }, + "after": { "type": "string", "required": false }, + "before": { "type": "string", "required": false } + }, + "type": "object" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "schema": { + "properties": { + "scope": { + "type": "string", + "enum": ["organization", "project"], + "required": false + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/V2ListRolesResponse" } + } + } + }, + "401": { "description": "Unauthorized" }, + "403": { "description": "Forbidden action" }, + "429": { "description": "Rate limit exceeded" } + }, + "security": [{ "bearer": [] }, { "fga_permissions": ["members_read"] }], + "summary": "List roles of an organization", + "tags": ["Organizations"], + "x-badges": [{ "name": "OAuth scope: organizations:read", "position": "after" }], + "x-endpoint-owners": ["management-api"], + "x-oauth-scope": "organizations:read" + } } }, "info": { @@ -893,6 +1022,195 @@ } }, "required": ["data"] + }, + "V2ListMembersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_member"], + "description": "Resource type." + }, + "id": { "type": "string", "format": "uuid" }, + "attributes": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true, + "description": "Member's username" + }, + "primary_email": { + "type": "string", + "nullable": true, + "description": "Member's primary email" + }, + "mfa_enabled": { + "type": "boolean", + "description": "Whether Multi-Factor Authentication is enabled for this member" + }, + "is_sso_user": { + "type": "boolean", + "description": "Whether this member is a Single Sign-On user" + }, + "roles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer", "description": "Role ID." }, + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref": { "type": "string" }, + "name": { "type": "string" } + }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["id", "name", "scope", "projects"] + }, + "description": "Roles assigned to this member. Includes both org-level and project-scoped roles." + } + }, + "required": ["username", "primary_email", "mfa_enabled", "is_sso_user", "roles"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] + }, + "V2ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_role"], + "description": "Resource type." + }, + "id": { "type": "string" }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "base_role_id": { + "type": "integer", + "nullable": true, + "description": "null for org-level roles; the base role ID for project-scoped roles." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { "ref": { "type": "string" }, "name": { "type": "string" } }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["name", "scope", "base_role_id", "projects"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] } } } diff --git a/apps/docs/spec/common-api-sections.json b/apps/docs/spec/common-api-sections.json index 6d4aad0cd97..254629246b8 100644 --- a/apps/docs/spec/common-api-sections.json +++ b/apps/docs/spec/common-api-sections.json @@ -735,6 +735,18 @@ "title": "List organization members", "slug": "v1-list-organization-members", "type": "operation" + }, + { + "id": "v2-list-organization-members", + "title": "List organization members", + "slug": "v2-list-organization-members", + "type": "operation" + }, + { + "id": "v2-list-organization-roles", + "title": "List organization roles", + "slug": "v2-list-organization-roles", + "type": "operation" } ] }, @@ -886,6 +898,12 @@ "slug": "v2-preview-a-project-transfer", "type": "operation" }, + { + "id": "v1-restart-a-project", + "title": "Restart a project", + "slug": "v1-restart-a-project", + "type": "operation" + }, { "id": "v1-restore-a-project", "title": "Restore a project", diff --git a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json index 0205bf7586a..de1ae800bcc 100644 --- a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json @@ -1858,6 +1858,7 @@ }, "/v1/oauth/token": { "post": { + "description": "Supports `authorization_code`, `refresh_token`, and `urn:ietf:params:oauth:grant-type:jwt-bearer` grant types. The `jwt-bearer` grant type (IDJAG — identity-directed JWT assertion) is in beta and available on Team and Enterprise plans only.", "operationId": "v1-exchange-oauth-token", "parameters": [], "requestBody": { @@ -1869,7 +1870,11 @@ "properties": { "grant_type": { "type": "string", - "enum": ["authorization_code", "refresh_token"] + "enum": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:jwt-bearer" + ] }, "client_id": { "type": "string", @@ -1890,6 +1895,10 @@ "refresh_token": { "type": "string" }, + "assertion": { + "type": "string", + "description": "IDJAG assertion JWT for grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Beta - available on Team and Enterprise plans only." + }, "resource": { "type": "string", "format": "uri", @@ -1925,7 +1934,8 @@ "type": "string" }, "refresh_token": { - "type": "string" + "type": "string", + "description": "The `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type issues access tokens only, no refresh token is returned and the token cannot be revoked via `/v1/oauth/revoke`." }, "expires_in": { "type": "integer" @@ -1935,7 +1945,7 @@ "enum": ["Bearer"] } }, - "required": ["access_token", "refresh_token", "expires_in", "token_type"], + "required": ["access_token", "expires_in", "token_type"], "additionalProperties": false } } @@ -8483,7 +8493,7 @@ } }, "responses": { - "201": { + "204": { "description": "" }, "401": { @@ -8560,7 +8570,7 @@ } }, "responses": { - "201": { + "204": { "description": "" }, "401": { @@ -13622,6 +13632,58 @@ "x-oauth-scope": "projects:write" } }, + "/v1/projects/{ref}/restart": { + "post": { + "operationId": "v1-restart-a-project", + "parameters": [ + { + "name": "ref", + "required": true, + "in": "path", + "description": "Project ref", + "schema": { + "minLength": 20, + "maxLength": 20, + "pattern": "^[a-z]+$", + "example": "abcdefghijklmnopqrst", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden action" + }, + "429": { + "description": "Rate limit exceeded" + } + }, + "security": [ + { + "bearer": [] + }, + { + "fga_permissions": ["project_admin_write"] + } + ], + "summary": "Restarts the given project", + "tags": ["Projects"], + "x-badges": [ + { + "name": "OAuth scope: projects:write", + "position": "after" + } + ], + "x-endpoint-owners": ["infra", "management-api"], + "x-oauth-scope": "projects:write" + } + }, "/v1/projects/{ref}/restore": { "get": { "operationId": "v1-list-available-restore-versions", @@ -13847,7 +13909,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "variant": { @@ -13901,6 +13964,10 @@ { "type": "string", "enum": ["log_drain_default"] + }, + { + "type": "string", + "enum": ["etl_pipeline_default"] } ] }, @@ -13951,7 +14018,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "name": { @@ -14010,6 +14078,10 @@ { "type": "string", "enum": ["log_drain_default"] + }, + { + "type": "string", + "enum": ["etl_pipeline_default"] } ] }, @@ -14151,7 +14223,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] } }, @@ -19469,6 +19542,43 @@ "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { + "type": "boolean" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, "maintenance_work_mem": { "type": "string" }, @@ -19624,6 +19734,43 @@ "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { + "type": "boolean" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, "maintenance_work_mem": { "type": "string" }, @@ -19741,6 +19888,43 @@ "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { + "type": "boolean" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, "maintenance_work_mem": { "type": "string" }, @@ -23558,7 +23742,11 @@ "properties": { "grant_type": { "type": "string", - "enum": ["authorization_code", "refresh_token"] + "enum": [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:jwt-bearer" + ] }, "client_id": { "type": "string", @@ -23579,6 +23767,10 @@ "refresh_token": { "type": "string" }, + "assertion": { + "type": "string", + "description": "IDJAG assertion JWT for grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Beta - available on Team and Enterprise plans only." + }, "resource": { "type": "string", "format": "uri", @@ -23606,7 +23798,8 @@ "type": "string" }, "refresh_token": { - "type": "string" + "type": "string", + "description": "The `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type issues access tokens only, no refresh token is returned and the token cannot be revoked via `/v1/oauth/revoke`." }, "expires_in": { "type": "integer" @@ -23616,7 +23809,7 @@ "enum": ["Bearer"] } }, - "required": ["access_token", "refresh_token", "expires_in", "token_type"], + "required": ["access_token", "expires_in", "token_type"], "additionalProperties": false }, "OAuthRevokeTokenBody": { @@ -28019,7 +28212,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "variant": { @@ -28073,6 +28267,10 @@ { "type": "string", "enum": ["log_drain_default"] + }, + { + "type": "string", + "enum": ["etl_pipeline_default"] } ] }, @@ -28123,7 +28321,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] }, "name": { @@ -28182,6 +28381,10 @@ { "type": "string", "enum": ["log_drain_default"] + }, + { + "type": "string", + "enum": ["etl_pipeline_default"] } ] }, @@ -28273,7 +28476,8 @@ "ipv4", "auth_mfa_phone", "auth_mfa_web_authn", - "log_drain" + "log_drain", + "etl_pipeline" ] } }, @@ -30021,6 +30225,43 @@ "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { + "type": "boolean" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, "maintenance_work_mem": { "type": "string" }, @@ -30120,6 +30361,43 @@ "logical_decoding_work_mem": { "type": "string" }, + "cron.log_statement": { + "type": "boolean" + }, + "log_autovacuum_min_duration": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_checkpoints": { + "type": "boolean" + }, + "log_connections": { + "type": "boolean" + }, + "log_disconnections": { + "type": "boolean" + }, + "log_duration": { + "type": "boolean" + }, + "log_lock_waits": { + "type": "boolean" + }, + "log_recovery_conflict_waits": { + "type": "boolean" + }, + "log_replication_commands": { + "type": "boolean" + }, + "log_startup_progress_interval": { + "type": "string", + "description": "Default unit: ms", + "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$" + }, + "log_temp_files": { + "type": "string" + }, "maintenance_work_mem": { "type": "string" }, diff --git a/apps/docs/spec/transforms/api_v2_openapi_deparsed.json b/apps/docs/spec/transforms/api_v2_openapi_deparsed.json index 3e430f5e530..45e90da5d75 100644 --- a/apps/docs/spec/transforms/api_v2_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v2_openapi_deparsed.json @@ -1581,6 +1581,414 @@ "tags": ["Projects"], "x-endpoint-owners": ["management-api"] } + }, + "/v2/organizations/{slug}/members": { + "get": { + "description": "Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.", + "operationId": "v2-list-organization-members", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Organization slug", + "schema": { + "pattern": "^[\\w-]+$", + "example": "tsrqponmlkjihgfedcba", + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "properties": { + "size": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "required": false + }, + "after": { + "type": "string", + "format": "uuid", + "required": false + }, + "before": { + "type": "string", + "format": "uuid", + "required": false + } + }, + "type": "object" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "schema": { + "properties": { + "username": { + "type": "string", + "required": false + }, + "primary_email": { + "type": "string", + "format": "email", + "required": false + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_member"], + "description": "Resource type." + }, + "id": { + "type": "string", + "format": "uuid" + }, + "attributes": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true, + "description": "Member's username" + }, + "primary_email": { + "type": "string", + "nullable": true, + "description": "Member's primary email" + }, + "mfa_enabled": { + "type": "boolean", + "description": "Whether Multi-Factor Authentication is enabled for this member" + }, + "is_sso_user": { + "type": "boolean", + "description": "Whether this member is a Single Sign-On user" + }, + "roles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Role ID." + }, + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["id", "name", "scope", "projects"] + }, + "description": "Roles assigned to this member. Includes both org-level and project-scoped roles." + } + }, + "required": [ + "username", + "primary_email", + "mfa_enabled", + "is_sso_user", + "roles" + ] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden action" + }, + "429": { + "description": "Rate limit exceeded" + } + }, + "security": [ + { + "bearer": [] + }, + { + "fga_permissions": ["members_read"] + } + ], + "summary": "List members of an organization", + "tags": ["Organizations"], + "x-badges": [ + { + "name": "OAuth scope: organizations:read", + "position": "after" + } + ], + "x-endpoint-owners": ["management-api"], + "x-oauth-scope": "organizations:read" + } + }, + "/v2/organizations/{slug}/roles": { + "get": { + "description": "Returns a cursor-paginated list of organization roles including both org-scoped and project-scoped roles.", + "operationId": "v2-list-organization-roles", + "parameters": [ + { + "name": "slug", + "required": true, + "in": "path", + "description": "Organization slug", + "schema": { + "pattern": "^[\\w-]+$", + "example": "tsrqponmlkjihgfedcba", + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "properties": { + "size": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "required": false + }, + "after": { + "type": "string", + "required": false + }, + "before": { + "type": "string", + "required": false + } + }, + "type": "object" + } + }, + { + "name": "filter", + "required": false, + "in": "query", + "schema": { + "properties": { + "scope": { + "type": "string", + "enum": ["organization", "project"], + "required": false + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_role"], + "description": "Resource type." + }, + "id": { + "type": "string" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "base_role_id": { + "type": "integer", + "nullable": true, + "description": "null for org-level roles; the base role ID for project-scoped roles." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["name", "scope", "base_role_id", "projects"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden action" + }, + "429": { + "description": "Rate limit exceeded" + } + }, + "security": [ + { + "bearer": [] + }, + { + "fga_permissions": ["members_read"] + } + ], + "summary": "List roles of an organization", + "tags": ["Organizations"], + "x-badges": [ + { + "name": "OAuth scope: organizations:read", + "position": "after" + } + ], + "x-endpoint-owners": ["management-api"], + "x-oauth-scope": "organizations:read" + } } }, "components": { @@ -2545,6 +2953,214 @@ } }, "required": ["data"] + }, + "V2ListMembersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_member"], + "description": "Resource type." + }, + "id": { + "type": "string", + "format": "uuid" + }, + "attributes": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true, + "description": "Member's username" + }, + "primary_email": { + "type": "string", + "nullable": true, + "description": "Member's primary email" + }, + "mfa_enabled": { + "type": "boolean", + "description": "Whether Multi-Factor Authentication is enabled for this member" + }, + "is_sso_user": { + "type": "boolean", + "description": "Whether this member is a Single Sign-On user" + }, + "roles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Role ID." + }, + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["id", "name", "scope", "projects"] + }, + "description": "Roles assigned to this member. Includes both org-level and project-scoped roles." + } + }, + "required": ["username", "primary_email", "mfa_enabled", "is_sso_user", "roles"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] + }, + "V2ListRolesResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["organization_role"], + "description": "Resource type." + }, + "id": { + "type": "string" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Role name. For project-scoped roles this is the base role name.", + "example": "Developer" + }, + "scope": { + "type": "string", + "enum": ["organization", "project"], + "description": "Whether this role applies org-wide or is scoped to specific projects for the user." + }, + "base_role_id": { + "type": "integer", + "nullable": true, + "description": "null for org-level roles; the base role ID for project-scoped roles." + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ref": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["ref", "name"] + }, + "description": "Project refs this role is scoped to. Empty array for org-level roles." + } + }, + "required": ["name", "scope", "base_role_id", "projects"] + } + }, + "required": ["type", "id", "attributes"] + } + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "nullable": true, + "description": "URL path to the first page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10" + }, + "prev": { + "type": "string", + "nullable": true, + "description": "URL path to the previous page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7" + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL path to the next page.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4" + }, + "last": { + "type": "string", + "nullable": true, + "description": "URL path to the last page if available.", + "example": "/v2/organizations/my-org/roles?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295" + } + }, + "required": ["prev", "next"] + } + }, + "required": ["data", "links"] } } } From aba4e147eb1eaae25dd86c3b0b423651f02ec4ff Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Jun 2026 15:45:39 +0800 Subject: [PATCH 006/488] Joshen/fe 3613 database tables query should have schema filter wherever appropriate (#46935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context There's certain areas in the dashboard where we're calling `useTablesQuery` without a schema filter, in which case the dashboard then fires a query against the project's database to fetch _all_ tables across _all_ schemas - this could easily be a heavy query if there's a large number of relations in the project's database. Am hence opting to either add a schema filter if appropriate, or otherwise opt to use the infinite loading behaviour ## Changes involved - Add schema filter to `useTablesQuery` in database triggers and publications - Use infinite loading for tables in Cmd K for "Run query on table" and "Search database tables" ## To test - [x] Verify that database triggers + publications still function as expected - [x] Verify that CMD K "Run query on table" and "Search database tables" still function as expected (including search) ## Summary by CodeRabbit * **New Features** * Implemented debounced infinite-scrolling table search in the command menu and SQL editor command flow. * Added a schema selector dropdown to publications management for easier navigation. * **Improvements** * Removed the “Schema” column from the publications tables UI. * Updated search guidance and table-picker status (counts/loading) during infinite browsing. * Trigger table listings now follow the selected schema context. * Refined command menu list height and improved the database-tables placeholder text. --- .../App/CommandMenu/ContextSearchCommands.tsx | 22 ++- .../ContextSearchResults.shared.tsx | 7 +- .../App/CommandMenu/ContextSearchResults.tsx | 9 +- .../App/CommandMenu/TableSearchResults.tsx | 182 ++++++++---------- .../Publications/PublicationsTableItem.tsx | 1 - .../Publications/PublicationsTables.tsx | 32 ++- .../Database/Triggers/TriggerSheet.tsx | 3 + .../Triggers/TriggersList/TriggersList.tsx | 1 + .../SidePanelEditor/SidePanelEditor.tsx | 12 +- .../SidePanelEditor/SidePanelEditor.utils.tsx | 4 +- .../TableEditor/TableEditor.tsx | 4 +- .../SQLEditorLayout/SqlEditor.Commands.tsx | 90 +++++++-- .../ui/AIAssistantPanel/AIAssistant.utils.ts | 5 +- apps/studio/data/tables/keys.ts | 7 +- .../data/tables/table-create-mutation.ts | 4 +- apps/studio/data/tables/tables-query.ts | 70 +++---- .../sql/studio/database/tables-paginated.ts | 7 +- .../ui/src/components/shadcn/ui/command.tsx | 2 +- 18 files changed, 267 insertions(+), 195 deletions(-) diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx index 7e8f5718a51..83060ab341d 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchCommands.tsx @@ -1,9 +1,10 @@ 'use client' +import { useDebounce } from '@uidotdev/usehooks' import { IS_PLATFORM } from 'common' import { Auth, EdgeFunctions, Storage } from 'icons' import { Database } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import type { ICommand } from 'ui-patterns/CommandMenu' import { CommandHeader, @@ -36,7 +37,7 @@ const SEARCH_CONTEXT_OPTIONS: SearchContextOption[] = [ value: 'database-tables', label: 'Database Tables', pageName: 'Search Database Tables', - placeholder: 'Search database tables...', + placeholder: 'Search database schema or tables...', icon: Database, }, { @@ -71,12 +72,23 @@ function ContextSearchPage({ }) { const query = useQuery() + const [filterString, setFilterString] = useState('') + const debouncedFilterString = useDebounce(filterString, 300) + return ( - + - + - + ) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx index d798f407c50..6e410076842 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.shared.tsx @@ -1,5 +1,3 @@ -'use client' - import { cn, CommandList } from 'ui' import { ShimmeringLoader } from 'ui-patterns' import { TextHighlighter } from 'ui-patterns/CommandMenu' @@ -53,6 +51,7 @@ interface ResultsListProps { onResultClick?: (result: SearchResult) => void getRoute?: (result: SearchResult) => `/${string}` | `http${string}` className?: string + infiniteLoadingObserverRef?: (node: Element | null) => void } export function ResultsList({ @@ -62,6 +61,7 @@ export function ResultsList({ onResultClick, getRoute, className, + infiniteLoadingObserverRef, }: ResultsListProps) { const commands = results.map((result): IRouteCommand | IActionCommand => { const ResultIcon = getIcon ? getIcon(result) : Icon @@ -92,7 +92,7 @@ export function ResultsList({ className )} > - + {commands.map((command) => (
@@ -106,6 +106,7 @@ export function ResultsList({ ))} + {infiniteLoadingObserverRef &&
} ) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx index 80701e4458b..205f34fa940 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/ContextSearchResults.tsx @@ -45,6 +45,7 @@ const StorageSearchResults = dynamic( interface ContextSearchResultsProps { context: SearchContextValue query: string + debouncedFilterString: string } const CONTEXT_CONFIG: Record< @@ -77,13 +78,17 @@ const CONTEXT_CONFIG: Record< }, } -export function ContextSearchResults({ context, query }: ContextSearchResultsProps) { +export function ContextSearchResults({ + context, + query, + debouncedFilterString, +}: ContextSearchResultsProps) { const config = CONTEXT_CONFIG[context] if (context === 'database-tables') { return (
- +
) } diff --git a/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx b/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx index 66266fcd47b..6861f0ca1dd 100644 --- a/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx +++ b/apps/studio/components/interfaces/App/CommandMenu/TableSearchResults.tsx @@ -1,8 +1,7 @@ -'use client' - +import { useIntersectionObserver } from '@uidotdev/usehooks' import { useParams } from 'common' import { Database, Loader2 } from 'lucide-react' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { EmptyState, @@ -10,55 +9,44 @@ import { SkeletonResults, type SearchResult, } from './ContextSearchResults.shared' -import { useTablesQuery } from '@/data/tables/tables-query' +import { useInfiniteTablesQuery } from '@/data/tables/tables-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' interface TableSearchResultsProps { - query: string + debouncedFilterString: string } -export function TableSearchResults({ query }: TableSearchResultsProps) { +export function TableSearchResults({ debouncedFilterString }: TableSearchResultsProps) { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() - const trimmedQuery = query.trim() + const [sentinelRef, sentinelEntry] = useIntersectionObserver({ + threshold: 0, + rootMargin: '200px 0px 200px 0px', + }) const { - data: tables, - isLoading: isLoadingTables, + data: tablesData, + isSuccess, isError: isErrorTables, - } = useTablesQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - includeColumns: false, - sortByProperty: 'name', - }, - { - enabled: !!project?.ref, - } - ) + isPending: isLoadingTables, + hasNextPage: hasNextTablesPage, + isFetchingNextPage: isFetchingNextTablesPage, + fetchNextPage: fetchNextTablesPage, + } = useInfiniteTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + includeColumns: false, + pageSize: 50, + nameFilter: debouncedFilterString, + }) + + const tables = useMemo(() => tablesData?.pages.flat() ?? [], [tablesData]) const tableResults: SearchResult[] = useMemo(() => { if (!tables) return [] - const filtered = trimmedQuery - ? tables.filter((table) => { - const searchLower = trimmedQuery.toLowerCase() - const tableName = table.name?.toLowerCase() || '' - const schemaName = table.schema?.toLowerCase() || '' - const fullName = `${schemaName}.${tableName}` - - return ( - tableName.includes(searchLower) || - schemaName.includes(searchLower) || - fullName.includes(searchLower) - ) - }) - : tables - - // Limit results for performance - return filtered.slice(0, 20).map((table) => { + return tables.map((table) => { const displayName = table.schema && table.schema !== 'public' ? `${table.schema}.${table.name}` @@ -76,79 +64,71 @@ export function TableSearchResults({ query }: TableSearchResultsProps) { description, } }) - }, [tables, trimmedQuery]) + }, [tables]) const totalTables = tables?.length ?? 0 - const renderFooter = () => ( -
-
- {isLoadingTables ? ( - - Loading... - - ) : ( - - Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''} - - )} -
-
- ) - - if (isLoadingTables) { - return ( -
-
- -
- {renderFooter()} -
- ) - } - - if (isErrorTables) { - return ( -
-
-
- -

Failed to load tables

-
-
- {renderFooter()} -
- ) - } - - if (tableResults.length === 0) { - return ( -
-
- -
- {renderFooter()} -
- ) - } + useEffect(() => { + if ( + sentinelEntry?.isIntersecting && + hasNextTablesPage && + !isFetchingNextTablesPage && + isSuccess + ) { + fetchNextTablesPage() + } + }, [ + isSuccess, + sentinelEntry?.isIntersecting, + hasNextTablesPage, + isFetchingNextTablesPage, + fetchNextTablesPage, + ]) return (
- { - const table = tables?.find((t) => String(t.id) === result.id) - if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}` + {isLoadingTables ? ( + + ) : isErrorTables ? ( +
+ +

Failed to load tables

+
+ ) : tableResults.length === 0 ? ( + + ) : ( + <> + { + const table = tables?.find((t) => String(t.id) === result.id) + if (!table || !projectRef) return `/project/${projectRef}/editor` as `/${string}` - const schemaParam = table.schema ? `?schema=${table.schema}` : '' - return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}` - }} - className="pb-9" - /> + const schemaParam = table.schema ? `?schema=${table.schema}` : '' + return `/project/${projectRef}/editor/${table.id}${schemaParam}` as `/${string}` + }} + infiniteLoadingObserverRef={sentinelRef} + className="pb-9" + /> +
+
+ {isLoadingTables ? ( + + Loading... + + ) : ( + + Total: {totalTables.toLocaleString()} table{totalTables !== 1 ? 's' : ''} + {hasNextTablesPage ? ' loaded' : ''} + + )} +
+
+ + )}
- {renderFooter()}
) } diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx index df2e4f2685f..5ef3ea67a85 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx @@ -75,7 +75,6 @@ export const PublicationsTableItem = ({ return ( {table.name} - {table.schema} {table.comment} diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx index 00390fd5d37..60b969f3564 100644 --- a/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsTables.tsx @@ -10,9 +10,12 @@ import { PublicationTablesSkeleton } from './PublicationSkeleton' import { PublicationsTableItem } from './PublicationsTableItem' import { AlertError } from '@/components/ui/AlertError' import { NoSearchResults } from '@/components/ui/NoSearchResults' +import SchemaSelector from '@/components/ui/SchemaSelector' +import { Shortcut } from '@/components/ui/Shortcut' import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query' import { useTablesQuery } from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { onSearchInputEscape } from '@/lib/keyboard' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' @@ -21,7 +24,10 @@ import { useShortcut } from '@/state/shortcuts/useShortcut' export const PublicationsTables = () => { const { id } = useParams() const { data: project } = useSelectedProjectQuery() + const { selectedSchema, setSelectedSchema } = useQuerySchemaState() + const [filterString, setFilterString] = useState('') + const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) const searchInputRef = useRef(null) const { can: canUpdatePublications, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( @@ -53,6 +59,7 @@ export const PublicationsTables = () => { } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const tables = useMemo(() => { @@ -63,7 +70,23 @@ export const PublicationsTables = () => { return ( <> -
+
+ setSchemaSelectorOpen(true)} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + { Name - Schema Description {/* We've disabled All tables toggle for publications. @@ -106,7 +128,7 @@ export const PublicationsTables = () => { {isError && ( - + @@ -114,7 +136,7 @@ export const PublicationsTables = () => { {!isLoading && !isLoadingPermissions && tables.length === 0 && ( - + { )) ) : ( - +

The selected publication with ID {id} cannot be found

Head back to the list of publications to select one from there diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx index 63d9499303f..35a1c109877 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -39,6 +39,7 @@ import FormBoxEmpty from '@/components/ui/FormBoxEmpty' import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-mutation' import { useTablesQuery } from '@/data/tables/tables-query' +import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' @@ -89,6 +90,7 @@ export const TriggerSheet = ({ onClose, }: TriggerSheetProps) => { const { data: project } = useSelectedProjectQuery() + const { selectedSchema } = useQuerySchemaState() const [showFunctionSelector, setShowFunctionSelector] = useState(false) @@ -118,6 +120,7 @@ export const TriggerSheet = ({ const { data = [], isSuccess: isSuccessTables } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas() const isSuccess = isSuccessTables && isSuccessProtectedSchemas diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index 5386a44f8e4..c47e1b89520 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -76,6 +76,7 @@ export const TriggersList = () => { const { data = [] } = useTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, + schema: selectedSchema, }) const hasTables = data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0 diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index f8f08a40881..42d782b88ec 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -437,7 +437,7 @@ export const SidePanelEditor = ({ }), queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(project?.ref) }), queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, selectedTable?.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, selectedTable?.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, selectedTable?.schema), @@ -508,7 +508,7 @@ export const SidePanelEditor = ({ // Switch it to individual tables via an array of strings // Refer to PublicationStore for more information about this const publicTables = await queryClient.fetchQuery({ - queryKey: tableKeys.list(project.ref, 'public', includeColumns), + queryKey: tableKeys.list(project.ref, 'public', { includeColumns }), queryFn: ({ signal }) => getTables( { @@ -646,10 +646,10 @@ export const SidePanelEditor = ({ .map(([, data]) => data?.pages?.[0]?.data?.count) .find((count) => typeof count === 'number') ?? queryClient.getQueryData( - tableKeys.list(project?.ref, payload.schema, true) + tableKeys.list(project?.ref, payload.schema, { includeColumns: true }) )?.length ?? queryClient.getQueryData( - tableKeys.list(project?.ref, payload.schema, false) + tableKeys.list(project?.ref, payload.schema, { includeColumns: false }) )?.length createTableSpan.setAttributes({ @@ -713,7 +713,7 @@ export const SidePanelEditor = ({ async () => { await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, table.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema), @@ -783,7 +783,7 @@ export const SidePanelEditor = ({ await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(project?.ref, table.schema, includeColumns), + queryKey: tableKeys.list(project?.ref, table.schema, { includeColumns }), }), queryClient.invalidateQueries({ queryKey: tableKeys.infiniteListPrefix(project?.ref, table.schema), diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 7a2b6277ed6..ac043ff81a7 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -908,7 +908,9 @@ export const updateTable = async ({ }), queryClient.invalidateQueries({ queryKey: databaseKeys.tableDefinition(projectRef, table.id) }), queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(projectRef) }), - queryClient.invalidateQueries({ queryKey: tableKeys.list(projectRef, table.schema, true) }), + queryClient.invalidateQueries({ + queryKey: tableKeys.list(projectRef, table.schema, { includeColumns: true }), + }), queryClient.invalidateQueries({ queryKey: lintKeys.lint(projectRef) }), ]) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 2a4c1937870..a7fb5b04002 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -478,7 +478,7 @@ export const TableEditor = ({ > @@ -496,7 +496,7 @@ export const TableEditor = ({ > diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx index df8be38a8f1..0c95f7a8691 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx @@ -1,10 +1,11 @@ import type { PGColumn } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks' import { useParams } from 'common' import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react' import { useRouter } from 'next/navigation' -import { useEffect, useMemo, useRef } from 'react' -import { cn, CommandEmpty, CommandGroup, CommandItem, CommandList } from 'ui' +import { useEffect, useRef, useState } from 'react' +import { cn, CommandGroup, CommandItem, CommandList } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' import type { CommandOptions } from 'ui-patterns/CommandMenu' import { @@ -26,10 +27,13 @@ import { import { COMMAND_MENU_SECTIONS } from '@/components/interfaces/App/CommandMenu/CommandMenu.utils' import { orderCommandSectionsByPriority } from '@/components/interfaces/App/CommandMenu/ordering' import { useSqlSnippetsQuery, type SqlSnippet } from '@/data/content/sql-snippets-query' -import { usePrefetchTables, useTablesQuery, type TablesData } from '@/data/tables/tables-query' +import { + useInfiniteTablesQuery, + usePrefetchTables, + type TablesData, +} from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' import { useProfile } from '@/lib/profile' export function useSqlEditorGotoCommands(options?: CommandOptions) { @@ -294,34 +298,66 @@ export function useQueryTableCommands(options?: CommandOptions) { function TableSelector() { const router = useRouter() const { data: project } = useSelectedProjectQuery() - const { data: protectedSchemas } = useProtectedSchemas() + + const [filterString, setFilterString] = useState('') + const debouncedFilterString = useDebounce(filterString, 300) + + const [sentinelRef, sentinelEntry] = useIntersectionObserver({ + threshold: 0, + rootMargin: '200px 0px 200px 0px', + }) + const { data: tablesData, - isPending: isLoading, isError, isSuccess, - } = useTablesQuery({ + isPending: isLoading, + hasNextPage: hasNextTablesPage, + isFetchingNextPage: isFetchingNextTablesPage, + fetchNextPage: fetchNextTablesPage, + } = useInfiniteTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, includeColumns: true, + pageSize: 50, + nameFilter: debouncedFilterString, }) - const tables = useMemo(() => { - return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema)) - }, [tablesData, protectedSchemas]) + const tables = tablesData?.pages.flat() ?? [] + + useEffect(() => { + if ( + sentinelEntry?.isIntersecting && + hasNextTablesPage && + !isFetchingNextTablesPage && + isSuccess + ) { + fetchNextTablesPage() + } + }, [ + isSuccess, + sentinelEntry?.isIntersecting, + hasNextTablesPage, + isFetchingNextTablesPage, + fetchNextTablesPage, + ]) return ( - + - + - + {isLoading && } {isError && } {isSuccess && ( <> - - + {tables?.map((table) => ( ))} + {tables.length === 0 && debouncedFilterString && ( +

+ No tables found based on your search +

+ )} + {tables.length > 0 && ( + <> +
+
+
+ {isFetchingNextTablesPage ? ( + + Loading... + + ) : ( + + Total: {tables.length} {tables.length === 1 ? 'table' : 'tables'} + {hasNextTablesPage ? ' loaded' : ''} + + )} +
+
+ + )} )} diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts index 6e544e99dbe..f399e554541 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -129,7 +129,10 @@ export const getContextualInvalidationKeys = ({ 'auth/users': [authKeys.usersInfinite(ref)], 'auth/policies': [databasePoliciesKeys.list(ref)], 'database/functions': [databaseKeys.databaseFunctions(ref)], - 'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)], + 'database/tables': [ + tableKeys.list(ref, schema, { includeColumns: true }), + tableKeys.list(ref, schema, { includeColumns: false }), + ], 'database/triggers': [databaseTriggerKeys.list(ref)], 'database/types': [enumeratedTypesKeys.list(ref)], 'database/extensions': [databaseExtensionsKeys.list(ref)], diff --git a/apps/studio/data/tables/keys.ts b/apps/studio/data/tables/keys.ts index 51440a268cc..fc6f54875e7 100644 --- a/apps/studio/data/tables/keys.ts +++ b/apps/studio/data/tables/keys.ts @@ -1,7 +1,10 @@ export const tableKeys = { names: (projectRef: string | undefined) => ['projects', projectRef, 'table-names'] as const, - list: (projectRef: string | undefined, schema?: string, includeColumns?: boolean) => - ['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean), + list: ( + projectRef: string | undefined, + schema: string | undefined, + options?: { includeColumns?: boolean } + ) => ['projects', projectRef, 'tables', schema, options].filter(Boolean), infiniteListPrefix: (projectRef: string | undefined, schema?: string) => ['projects', projectRef, 'tables', 'infinite', schema].filter( (part) => part !== undefined && part !== null && part !== '' diff --git a/apps/studio/data/tables/table-create-mutation.ts b/apps/studio/data/tables/table-create-mutation.ts index 74151da942d..ffe9dab4693 100644 --- a/apps/studio/data/tables/table-create-mutation.ts +++ b/apps/studio/data/tables/table-create-mutation.ts @@ -52,10 +52,10 @@ export const useTableCreateMutation = ({ await Promise.all([ queryClient.invalidateQueries({ - queryKey: tableKeys.list(projectRef, payload.schema, true), + queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: true }), }), queryClient.invalidateQueries({ - queryKey: tableKeys.list(projectRef, payload.schema, false), + queryKey: tableKeys.list(projectRef, payload.schema, { includeColumns: false }), }), queryClient.invalidateQueries({ queryKey: privilegeKeys.tablePrivilegesList(projectRef), diff --git a/apps/studio/data/tables/tables-query.ts b/apps/studio/data/tables/tables-query.ts index 5add82825bd..7101ac5da7c 100644 --- a/apps/studio/data/tables/tables-query.ts +++ b/apps/studio/data/tables/tables-query.ts @@ -1,11 +1,10 @@ -import { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta' -import { DEFAULT_PLATFORM_APPLICATION_NAME } from '@supabase/pg-meta/src/constants' +import pgMeta, { getTablesPaginatedSql, type PGTable } from '@supabase/pg-meta' import { InfiniteData, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' import { sortBy } from 'lodash' import { useCallback } from 'react' +import { z } from 'zod' import { tableKeys } from './keys' -import { get, handleError } from '@/data/fetchers' import { executeSql } from '@/data/sql/execute-sql-mutation' import type { SafePostgresTable } from '@/lib/postgres-types' import type { ResponseError, UseCustomInfiniteQueryOptions, UseCustomQueryOptions } from '@/types' @@ -14,13 +13,14 @@ export type TablesVariables = { projectRef?: string connectionString?: string | null schema?: string - /** - * Defaults to false - */ + /** Defaults to false */ includeColumns?: boolean sortByProperty?: keyof PGTable } +const pgMetaTablesList = pgMeta.tables.list() +export type TablesData = z.infer + export async function getTables( { projectRef, @@ -31,55 +31,31 @@ export async function getTables( }: TablesVariables, signal?: AbortSignal ) { - if (!projectRef) { - throw new Error('projectRef is required') - } - - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) - - let queryParams: Record = { - //include_columns is a string, even though it's true or false - include_columns: `${includeColumns}`, - } - if (schema) { - queryParams.included_schemas = schema - } - - const { data, error } = await get('/platform/pg-meta/{ref}/tables', { - params: { - header: { - 'x-connection-encrypted': connectionString!, - 'x-pg-application-name': DEFAULT_PLATFORM_APPLICATION_NAME, - }, - path: { ref: projectRef }, - query: queryParams as any, - }, - headers, - signal, - }) - - if (!Array.isArray(data) && error) handleError(error) + if (!projectRef) throw new Error('projectRef is required') + const sql = pgMeta.tables.list({ + includeColumns, + includedSchemas: schema ? [schema] : undefined, + }).sql + const queryKey = ['tables', schema] + const { result } = await executeSql({ projectRef, connectionString, sql, queryKey }, signal) // Sort the data if the sortByName option is true - if (Array.isArray(data) && sortByProperty) { - return sortBy(data, (t) => t[sortByProperty]) as SafePostgresTable[] + if (Array.isArray(result) && sortByProperty) { + return sortBy(result, (t) => t[sortByProperty]) as SafePostgresTable[] } - - return data as SafePostgresTable[] + return result as SafePostgresTable[] } -export type TablesData = Awaited> export type TablesError = ResponseError export const useTablesQuery = ( - { projectRef, connectionString, schema, includeColumns }: TablesVariables, + vars: TablesVariables, { enabled = true, ...options }: UseCustomQueryOptions = {} ) => { + const { projectRef, schema, includeColumns } = vars return useQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), - queryFn: ({ signal }) => - getTables({ projectRef, connectionString, schema, includeColumns }, signal), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), + queryFn: ({ signal }) => getTables(vars, signal), enabled: enabled && typeof projectRef !== 'undefined', ...options, }) @@ -98,7 +74,7 @@ export function useGetTables({ return useCallback( (schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => { return queryClient.fetchQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema, includeColumns }, signal), }) @@ -116,7 +92,7 @@ export function usePrefetchTables({ return useCallback( (schema?: TablesVariables['schema'], includeColumns?: TablesVariables['includeColumns']) => { return queryClient.prefetchQuery({ - queryKey: tableKeys.list(projectRef, schema, includeColumns), + queryKey: tableKeys.list(projectRef, schema, { includeColumns }), queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema, includeColumns }, signal), }) @@ -176,7 +152,7 @@ export async function getTablesPage( return result as SafePostgresTable[] } -export const useInfiniteTablesQuery = >( +export const useInfiniteTablesQuery = >( { projectRef, connectionString, diff --git a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts index d18dc71867d..0667bf2cdbe 100644 --- a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts +++ b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts @@ -37,7 +37,12 @@ export const getTablesPaginatedSql = ({ const schemaFilter = filter ? safeSql`and nc.nspname ${filter}` : safeSql`` const nameFilterClause = nameFilter && nameFilter.length > 0 - ? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}` + ? schema + ? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}` + : safeSql`and ( + c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)} + or nc.nspname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)} + )` : safeSql`` const columnsCte = includeColumns diff --git a/packages/ui/src/components/shadcn/ui/command.tsx b/packages/ui/src/components/shadcn/ui/command.tsx index 170c6b245ea..b75f0d9b62c 100644 --- a/packages/ui/src/components/shadcn/ui/command.tsx +++ b/packages/ui/src/components/shadcn/ui/command.tsx @@ -90,7 +90,7 @@ const CommandList = React.forwardRef< >(({ className, ...props }, ref) => ( )) From 41c6c3db16b6cc362b6d6e1c9f5f759b6573678d Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Tue, 16 Jun 2026 10:14:49 +0200 Subject: [PATCH 007/488] Revert "docs: Set path on 404 errors" (#46970) Reverts supabase/supabase#46848 ## Summary by CodeRabbit ## Release Notes **Refactor** * Standardized 404 error handling across documentation pages to use Next.js built-in features instead of custom utilities * Enhanced consistency for missing documentation entries, guides, and references when users navigate to unavailable pages --- .../ai-tools/ai-prompts/[slug]/page.tsx | 4 ++-- .../docs/app/guides/ai/python/[slug]/page.tsx | 11 ++++++----- .../extensions/wrappers/[[...slug]]/page.tsx | 9 +++------ .../app/guides/deployment/ci/[slug]/page.tsx | 11 ++++++----- .../deployment/terraform/[[...slug]]/page.tsx | 4 ++-- .../app/guides/graphql/[[...slug]]/page.tsx | 4 ++-- .../guides/troubleshooting/[slug]/page.tsx | 5 +++-- apps/docs/app/reference/[...slug]/page.tsx | 10 +++++----- apps/docs/features/docs/GuidesMdx.utils.tsx | 8 ++++---- apps/docs/features/docs/notFound.utils.ts | 19 ------------------- apps/docs/resources/guide/guideModelLoader.ts | 6 +++--- 11 files changed, 36 insertions(+), 55 deletions(-) delete mode 100644 apps/docs/features/docs/notFound.utils.ts diff --git a/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx index df39f6ee8eb..7eaf8f3ceae 100644 --- a/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx @@ -6,8 +6,8 @@ import { wrapInMarkdownCodeBlock, } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { source } from 'common-tags' +import { notFound } from 'next/navigation' export const dynamicParams = false @@ -18,7 +18,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str const prompt = await getAiPrompt(slug) if (!prompt) { - notFoundWithPathname(`/guides/ai-tools/ai-prompts/${slug}`) + notFound() } let { heading, content } = prompt diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/ai/python/[slug]/page.tsx index e64133a1c39..e31bc6b1595 100644 --- a/apps/docs/app/guides/ai/python/[slug]/page.tsx +++ b/apps/docs/app/guides/ai/python/[slug]/page.tsx @@ -1,13 +1,14 @@ +import { notFound } from 'next/navigation' import { relative } from 'path' +import rehypeSlug from 'rehype-slug' + import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' -import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { getGitHubFileContents } from '~/lib/octokit' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' -import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -75,7 +76,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFoundWithPathname(`/guides/ai/python/${slug}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index f85355e895a..ebafbd690b7 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -5,7 +5,6 @@ import { genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { newEditLink } from '~/features/helpers.edit-link' import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide' // End of third-party imports @@ -21,6 +20,7 @@ import type { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { isFeatureEnabled } from 'common' import matter from 'gray-matter' import Link from 'next/link' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' import { Button } from 'ui' @@ -334,14 +334,11 @@ interface Params { } const WrappersDocs = async (props: { params: Promise }) => { - const params = await props.params - if (!isFeatureEnabled('docs:fdw')) { - notFoundWithPathname( - `/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` - ) + notFound() } + const params = await props.params const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params) // Create a combined URL transformer that handles both regular URLs and asset URLs diff --git a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx index 2a2cf31e917..4053787f65e 100644 --- a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx +++ b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx @@ -1,14 +1,15 @@ +import { notFound } from 'next/navigation' import { relative } from 'node:path' +import rehypeSlug from 'rehype-slug' + import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' -import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { getGitHubFileContents } from '~/lib/octokit' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -74,7 +75,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFoundWithPathname(`/guides/deployment/ci/${slug}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index 2fafd8ea780..153c814eff3 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -1,6 +1,5 @@ import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { isValidGuideFrontmatter } from '~/lib/docs' @@ -11,6 +10,7 @@ import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import matter from 'gray-matter' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import { @@ -106,7 +106,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === requestedSlug) if (!page) { - notFoundWithPathname(`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}`) + notFound() } const { meta, remoteFile, useRoot } = page diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index dda40f712a9..eb0c01b48f5 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -1,7 +1,6 @@ import { isAbsolute, relative } from 'path' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' @@ -10,6 +9,7 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' // We fetch these docs at build time from an external repo @@ -128,7 +128,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === slug?.at(0)) if (!page) { - notFoundWithPathname(`/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx index f0868e8c167..9f15cac7d53 100644 --- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -1,4 +1,5 @@ -import { notFoundWithPathname } from '~/features/docs/notFound.utils' +import { notFound } from 'next/navigation' + import TroubleshootingPage from '~/features/docs/Troubleshooting.page' import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils' import { PROD_URL } from '~/lib/constants' @@ -19,7 +20,7 @@ export default async function TroubleshootingEntryPage(props: { const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug) if (!entry) { - notFoundWithPathname(`/guides/troubleshooting/${slug}`) + notFound() } return diff --git a/apps/docs/app/reference/[...slug]/page.tsx b/apps/docs/app/reference/[...slug]/page.tsx index d224a81cb7e..0ed5497704c 100644 --- a/apps/docs/app/reference/[...slug]/page.tsx +++ b/apps/docs/app/reference/[...slug]/page.tsx @@ -1,5 +1,6 @@ +import { notFound } from 'next/navigation' + import { REFERENCES } from '~/content/navigation.references' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { ApiReferencePage } from '~/features/docs/Reference.apiPage' import { CliReferencePage } from '~/features/docs/Reference.cliPage' import { ClientSdkReferencePage } from '~/features/docs/Reference.sdkPage' @@ -17,10 +18,9 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr const params = await props.params const { slug } = params - const referencePath = `/reference/${slug.join('/')}` if (!Object.keys(REFERENCES).includes(slug[0].replaceAll('-', '_'))) { - notFoundWithPathname(referencePath) + notFound() } const parsedPath = parseReferencePath(slug) @@ -34,7 +34,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr const sdkData = REFERENCES[sdkId] if (sdkData.enabled === false) { - notFoundWithPathname(referencePath) + notFound() } const latestVersion = sdkData.versions[0] @@ -52,7 +52,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr ) } else { - notFoundWithPathname(referencePath) + notFound() } } diff --git a/apps/docs/features/docs/GuidesMdx.utils.tsx b/apps/docs/features/docs/GuidesMdx.utils.tsx index e719d2caa9f..1cd8e04a9eb 100644 --- a/apps/docs/features/docs/GuidesMdx.utils.tsx +++ b/apps/docs/features/docs/GuidesMdx.utils.tsx @@ -14,10 +14,10 @@ import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' import { gfm } from 'micromark-extension-gfm' import { type Metadata, type ResolvingMetadata } from 'next' +import { notFound } from 'next/navigation' import { newEditLink } from './GuidesMdx.template' import { checkGuidePageEnabled } from './NavigationPageStatus.utils' -import { notFoundWithPathname } from './notFound.utils' const { metadataTitle } = getCustomContent(['metadata:title']) @@ -56,7 +56,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { !fullPath.startsWith(GUIDES_DIRECTORY) || !PUBLISHED_SECTIONS.some((section) => relPath.startsWith(section)) ) { - notFoundWithPathname(guidesPath) + notFound() } /** @@ -65,7 +65,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { */ if (!checkGuidePageEnabled(guidesPath)) { console.log('Page is disabled: %s', guidesPath) - notFoundWithPathname(guidesPath) + notFound() } try { @@ -100,7 +100,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => { ) Sentry.captureException(error) } - notFoundWithPathname(guidesPath) + notFound() } } diff --git a/apps/docs/features/docs/notFound.utils.ts b/apps/docs/features/docs/notFound.utils.ts deleted file mode 100644 index cd090a1e546..00000000000 --- a/apps/docs/features/docs/notFound.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as Sentry from '@sentry/nextjs' -import { notFound } from 'next/navigation' - -/** - * Triggers a Next.js 404 while recording the requested path as a Sentry tag. - * - * `notFound()` itself does not create a Sentry event, but tagging the active - * scope means any error captured during the same request (and any event the - * Sentry SDK records for the render) carries the exact path under - * `404.pathname`. That makes 404s filterable and groupable in Sentry/Discover - * instead of only being inferable from the route's transaction name. - * - * @param pathname - The user-facing path that was not found, e.g. - * `/guides/functions/runtimes/node-22`. - */ -export function notFoundWithPathname(pathname: string): never { - Sentry.setTag('404.pathname', pathname) - notFound() -} diff --git a/apps/docs/resources/guide/guideModelLoader.ts b/apps/docs/resources/guide/guideModelLoader.ts index 1fa7858336a..551bd98abfe 100644 --- a/apps/docs/resources/guide/guideModelLoader.ts +++ b/apps/docs/resources/guide/guideModelLoader.ts @@ -1,13 +1,13 @@ +import matter from 'gray-matter' import { promises as fs } from 'node:fs' import { join, relative, resolve } from 'node:path' + import { extractMessageFromAnyError, FileNotFoundError, MultiError } from '~/app/api/utils' import { preprocessMdxWithDefaults } from '~/features/directives/utils' import { checkGuidePageEnabled } from '~/features/docs/NavigationPageStatus.utils' import { Both, Result } from '~/features/helpers.fn' import { GUIDES_DIRECTORY } from '~/lib/docs' import { processMdx } from '~/scripts/helpers.mdx' -import matter from 'gray-matter' - import { GuideModel } from './guideModel' /** @@ -140,7 +140,7 @@ export class GuideModelLoader { }, (error) => { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { - throw new FileNotFoundError(join(GUIDES_DIRECTORY, relPath), error) + throw new FileNotFoundError('', error) } throw new Error( `Failed to load guide from ${relPath}: ${extractMessageFromAnyError(error)}`, From 43c2229f1e11ac49027578b98bf24690a3dd35dd Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:19:57 -0600 Subject: [PATCH 008/488] fix(studio): align /authorize invalid and edge states with interstitial UI (#46960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix / UI polish ## What is the current behavior? Visiting `/authorize` without an `auth_id` renders a bare `Card` outside the shared Connect interstitial — no centered layout, no Supabase logo, inconsistent with every other `/authorize` state (loading, error, form, approved). Two edge cases also produce poor UX: a blank flash while `router.isReady` is false, and a silent empty page when the authorization query succeeds but returns no requester. ## What is the new behavior? - **Missing `auth_id`**: `ApiAuthorizationInvalidScreen` now uses `InterstitialLayout` with `SupabaseLogo`, a user-facing title ("Missing authorization link"), warning admonition, and "Back to dashboard" — matching the error screen and CLI missing-params pattern. - **Router not ready**: `authorize.tsx` shows `ApiAuthorizationLoadingScreen` instead of `null`. - **Empty requester**: `ApiAuthorization.Valid.tsx` renders `ApiAuthorizationErrorScreen` instead of returning `null`. Tests updated in `ApiAuthorization.test.tsx`; added `authorize.test.tsx` for router-not-ready loading. | Before | After | | --- | --- | | Authorize API Access
Supabase-DCB404EC-7D65-4DD1-A6E0-B720DC765DA7 | Authorize API Access
Supabase-6B642066-D0BE-4EDC-A186-A0290B4B5634 | ## To test Please do a visual check on `http://localhost:8082/authorize` (no `auth_id` or other parameters). ## Summary by CodeRabbit * **Bug Fixes** * Improved the UI and copy shown when the authorization link is missing. * Updated behavior to show an explicit error screen when authorization requester data is unavailable. * **New Features** * Added a loading state for the authorization page while router parameters are initializing. * **Tests** * Updated component expectations for the missing authorization and “unable to load” scenarios. * Added a page test to verify the loading message when the router is not ready. --------- Co-authored-by: Cursor --- .../ApiAuthorization.Invalid.tsx | 36 +++++++------ .../ApiAuthorization.Valid.tsx | 4 +- apps/studio/pages/authorize.tsx | 10 +++- .../components/ApiAuthorization.test.tsx | 20 +++++++- apps/studio/tests/pages/authorize.test.tsx | 50 +++++++++++++++++++ 5 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 apps/studio/tests/pages/authorize.test.tsx diff --git a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Invalid.tsx b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Invalid.tsx index 1ccfd92cea6..52e28b67ff5 100644 --- a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Invalid.tsx +++ b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Invalid.tsx @@ -1,5 +1,9 @@ +import Link from 'next/link' import type { ReactNode } from 'react' -import { Card, CardContent, CardHeader } from 'ui' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns' + +import { InterstitialLayout, SupabaseLogo } from '@/components/layouts/InterstitialLayout' export interface ApiAuthorizationInvalidScreenProps { missingParameters: Array @@ -9,20 +13,24 @@ export function ApiAuthorizationInvalidScreen({ missingParameters, }: ApiAuthorizationInvalidScreenProps): ReactNode { const isPlural = missingParameters.length > 1 - const paragraphFontClass = 'text-sm text-muted-foreground' return ( - - Missing parameters - -

- Cannot authorize this request because the URL is missing the following parameter - {isPlural ? 's' : ''}: {missingParameters.join(', ')}. -

-

- If you followed a link here, please check the link and try again. -

-
-
+ } + title="Missing authorization link" + description="This authorization request cannot be completed" + > +
+ + +
+
) } diff --git a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx index cb85776418a..507b06fa481 100644 --- a/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx +++ b/apps/studio/components/interfaces/ApiAuthorization/ApiAuthorization.Valid.tsx @@ -193,7 +193,9 @@ export function ApiAuthorizationValidScreen({ ? buildStudioPageTitle({ section: `Authorize ${effectiveRequester.name}`, brand: 'Supabase' }) : undefined - if (!effectiveRequester) return null + if (!effectiveRequester) { + return + } if (isApproved) { const approvedOrganization = diff --git a/apps/studio/pages/authorize.tsx b/apps/studio/pages/authorize.tsx index 4dc816778d9..45921d5c34a 100644 --- a/apps/studio/pages/authorize.tsx +++ b/apps/studio/pages/authorize.tsx @@ -3,6 +3,7 @@ import Head from 'next/head' import { useRouter } from 'next/router' import { ApiAuthorizationScreen } from '@/components/interfaces/ApiAuthorization/ApiAuthorization' +import { ApiAuthorizationLoadingScreen } from '@/components/interfaces/ApiAuthorization/ApiAuthorization.Loading' import { withAuth } from '@/hooks/misc/withAuth' import { buildStudioPageTitle } from '@/lib/page-title' import type { NextPageWithLayout } from '@/types' @@ -15,7 +16,14 @@ const APIAuthorizationPage: NextPageWithLayout = () => { const { auth_id, organization_slug } = useParams() if (!routerReady) { - return null + return ( + <> + + {PAGE_TITLE} + + + + ) } return ( diff --git a/apps/studio/tests/components/ApiAuthorization.test.tsx b/apps/studio/tests/components/ApiAuthorization.test.tsx index 2ac4757a4f9..3affecce453 100644 --- a/apps/studio/tests/components/ApiAuthorization.test.tsx +++ b/apps/studio/tests/components/ApiAuthorization.test.tsx @@ -122,10 +122,12 @@ function renderScreen(props: Partial = {}) { describe('ApiAuthorizationScreen', () => { describe('when auth_id is missing', () => { - test('renders invalid screen when auth_id is undefined', () => { + test('renders invalid interstitial when auth_id is undefined', () => { renderScreen({ auth_id: undefined }) - expect(screen.getByText('Missing parameters')).toBeInTheDocument() + expect(screen.getByText('Missing authorization link')).toBeInTheDocument() expect(screen.getByText(/auth_id/)).toBeInTheDocument() + expect(screen.getByAltText('Supabase')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Back to dashboard' })).toHaveAttribute('href', '/') }) }) @@ -153,6 +155,20 @@ describe('ApiAuthorizationScreen', () => { await screen.findByText('Unable to load authorization') }) + test('renders error screen when authorization query returns no requester', async () => { + mockOrgsEndpoint() + addAPIMock({ + method: 'get', + path: '/platform/oauth/authorizations/:id', + response: () => + HttpResponse.json( + null as unknown as GetOAuthAuthorizationResponse + ), + }) + renderScreen() + await screen.findByText('Unable to load authorization') + }) + describe('when already approved', () => { test('renders approved screen with matching organization name', async () => { mockBothEndpoints( diff --git a/apps/studio/tests/pages/authorize.test.tsx b/apps/studio/tests/pages/authorize.test.tsx new file mode 100644 index 00000000000..7561cce3406 --- /dev/null +++ b/apps/studio/tests/pages/authorize.test.tsx @@ -0,0 +1,50 @@ +import { screen } from '@testing-library/react' +import { describe, expect, test, vi } from 'vitest' + +import type { ProfileContextType } from '@/lib/profile' +import APIAuthorizationPage from '@/pages/authorize' +import { customRender } from '@/tests/lib/custom-render' + +vi.mock('@/hooks/misc/withAuth', () => ({ + withAuth: (Component: React.ComponentType) => Component, +})) + +const routerPushMock = vi.fn() + +vi.mock('next/router', () => ({ + useRouter: () => ({ + isReady: false, + push: routerPushMock, + query: {}, + }), +})) + +const DEFAULT_PROFILE_CONTEXT: ProfileContextType = { + profile: { + id: 1, + auth0_id: 'auth0|test', + gotrue_id: 'gotrue-test', + username: 'testuser', + primary_email: 'test@example.com', + first_name: null, + last_name: null, + mobile: null, + is_alpha_user: false, + is_sso_user: false, + disabled_features: [], + free_project_limit: null, + }, + error: null, + isLoading: false, + isError: false, + isSuccess: true, +} + +describe('APIAuthorizationPage', () => { + test('renders loading interstitial while router is not ready', () => { + customRender(, { + profileContext: DEFAULT_PROFILE_CONTEXT, + }) + expect(screen.getByText('Loading...')).toBeInTheDocument() + }) +}) From 4de3cc0c68a25b4e84b7b00ae7cb272d7933e0c7 Mon Sep 17 00:00:00 2001 From: Nik Richers Date: Tue, 16 Jun 2026 01:31:46 -0700 Subject: [PATCH 009/488] Add Nik Richers to humans.txt (#46964) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Adds Nik Richers to humans.txt. ## What is the current behavior? N/A ## What is the new behavior? N/A ## Additional context As requested in our onboarding documentation. ## Summary by CodeRabbit * **Chores** * Updated the team roster to include a new team member in the public team information. Co-authored-by: Nik Richers --- 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 41d2978b64b..9324b96b44e 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -207,6 +207,7 @@ Nick Littman Nick Nowlan Nicola Ferraro Nicolas Nosenzo +Nik Richers Noah Moss Nyannyacha Oli R From 5b46264cdbb03db066b15475c71868ae941e84d0 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:15:53 +0200 Subject: [PATCH 010/488] fix: avoid showing navigation item as disabled while loading (#46942) ## Problem On pages that needs to check conditions for which data is loaded asynchronously, the navigation links in the sidebar appear disabled for a split second: image ## Solution Check whether the required data is loading and override the styles so that the links are still disabled to avoid errors but appear as normal. ## How to test - Open the project setting page - Check the styles of the sidebar items carefully while it load (might require to refresh a few times to be sure) ## Summary by CodeRabbit ## Release Notes * **New Features** * Navigation buttons, menu items, and settings menus now display loading states while project data is being fetched, providing visual feedback and preventing interaction during data loading operations. --- apps/studio/components/interfaces/Sidebar.tsx | 21 ++++++++++++++++--- .../SettingsMenu.utils.tsx | 11 +++++++++- .../ui/ProductMenu/ProductMenu.types.ts | 1 + .../ui/ProductMenu/ProductMenuItem.tsx | 18 +++++++++++++++- .../ui/src/components/shadcn/ui/sidebar.tsx | 11 +++++++++- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Sidebar.tsx b/apps/studio/components/interfaces/Sidebar.tsx index 4db35a2cb05..e2695958b08 100644 --- a/apps/studio/components/interfaces/Sidebar.tsx +++ b/apps/studio/components/interfaces/Sidebar.tsx @@ -159,6 +159,7 @@ export function SideBarNavLink({ route, active, onClick, + isLoading, ...props }: { route: Route @@ -183,6 +184,7 @@ export function SideBarNavLink({ const buttonProps = { disabled: route.disabled, isActive: active, + isLoading, className: cn('text-sm', sidebarBehaviour === 'open' ? 'px-2!' : ''), size: 'default' as const, onClick: onClick, @@ -237,7 +239,7 @@ const ActiveDot = ({ hasErrors, hasWarnings }: { hasErrors: boolean; hasWarnings const ProjectLinks = () => { const router = useRouter() const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() + const { data: project, isPending: isProjectPending } = useSelectedProjectQuery() const { securityLints, errorLints } = useLints() const showReports = useIsFeatureEnabled('reports:all') const showLogs = useIsFeatureEnabled('logs:all') @@ -295,6 +297,7 @@ const ProjectLinks = () => { key={`tools-routes-${i}`} route={route} active={activeRoute === route.key} + isLoading={isProjectPending} /> ))} @@ -305,6 +308,7 @@ const ProjectLinks = () => { key={`product-routes-${i}`} route={route} active={activeRoute === route.key} + isLoading={isProjectPending} /> ))} @@ -320,12 +324,22 @@ const ProjectLinks = () => { hasWarnings={securityLints.length > 0} /> )} - +
) } else { return ( - + ) } })} @@ -338,6 +352,7 @@ const ProjectLinks = () => { key={`settings-routes-${i}`} route={route} active={activeRoute === route.key} + isLoading={isProjectPending} /> ))} diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx index ef22a78dcd7..354a4b603f8 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.tsx @@ -10,7 +10,7 @@ import { SHORTCUT_IDS } from '@/state/shortcuts/registry' export const useGenerateSettingsMenu = () => { const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() + const { data: project, isPending } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() const showDashboardPreferences = useFlag('dashboardPreferences') @@ -112,6 +112,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/compute-and-disk`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_COMPUTE_AND_DISK, }, { @@ -120,6 +121,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/infrastructure`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INFRASTRUCTURE, }, @@ -129,6 +131,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/integrations`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_INTEGRATIONS, }, ...(platformWebhooksEnabled @@ -139,6 +142,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/webhooks`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_WEBHOOKS, }, ] @@ -150,6 +154,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/api-keys`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_API_KEYS, }, { @@ -160,6 +165,7 @@ export const useGenerateSettingsMenu = () => { : `/project/${ref}/settings/jwt/signing-keys`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_JWT_KEYS, }, @@ -171,6 +177,7 @@ export const useGenerateSettingsMenu = () => { url: `/project/${ref}/settings/log-drains`, items: [], disabled: !isProjectActive, + isLoading: isPending, shortcutId: SHORTCUT_IDS.NAV_PROJECT_SETTINGS_LOG_DRAINS, }, ] @@ -205,6 +212,7 @@ export const useGenerateSettingsMenu = () => { items: [], rightIcon: , disabled: !isProjectActive, + isLoading: isPending, }, { name: 'Vault', @@ -214,6 +222,7 @@ export const useGenerateSettingsMenu = () => { rightIcon: , label: 'Beta', disabled: !isProjectActive, + isLoading: isPending, }, ], }, diff --git a/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts b/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts index 26b84896e6f..3b911dee4aa 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts +++ b/apps/studio/components/ui/ProductMenu/ProductMenu.types.ts @@ -30,6 +30,7 @@ export interface ProductMenuGroupItem { childItems?: ProductMenuGroupItem[] pages?: string[] shortcutId?: ShortcutId + isLoading?: boolean } /** diff --git a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx index 5f3ab05ad0d..d509f5ffd2e 100644 --- a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -19,7 +19,17 @@ export const ProductMenuItem = ({ hoverText = '', onClick, }: ProductMenuItemProps) => { - const { name = '', url = '', icon, rightIcon, isExternal, label, disabled, shortcutId } = item + const { + name = '', + url = '', + icon, + rightIcon, + isExternal, + label, + disabled, + shortcutId, + isLoading, + } = item const menuItem = ( @@ -45,6 +55,12 @@ export const ProductMenuItem = ({ ) + // When data necessary to check whether an item should be disabled is not yet available, override the styles to avoid + // showing the disabled state just for a moment + if (isLoading) { + return
{menuItem}
+ } + if (disabled) { return
{menuItem}
} diff --git a/packages/ui/src/components/shadcn/ui/sidebar.tsx b/packages/ui/src/components/shadcn/ui/sidebar.tsx index 1b7521c8b4e..0f5842a90d8 100644 --- a/packages/ui/src/components/shadcn/ui/sidebar.tsx +++ b/packages/ui/src/components/shadcn/ui/sidebar.tsx @@ -521,6 +521,13 @@ const sidebarMenuButtonVariants = cva( true: 'group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:pl-1.5! group-data-[collapsible=icon]:pr-2!', false: '', }, + isLoading: { + // When data necessary to check whether an item should be disabled is not yet available, override the styles to avoid + // showing the disabled state just for a moment + true: 'disabled:opacity-100 aria-disabled:opacity-100', + // If the item is not loading, fallback to the default styles so that disabled state is handled properly + false: '', + }, }, defaultVariants: { variant: 'default', @@ -537,6 +544,7 @@ const SidebarMenuButton = React.forwardRef< isActive?: boolean tooltip?: string | React.ComponentProps hasIcon?: boolean + isLoading?: boolean } & VariantProps >( ( @@ -546,6 +554,7 @@ const SidebarMenuButton = React.forwardRef< variant = 'default', size = 'default', hasIcon = true, + isLoading = false, tooltip, className, ...props @@ -570,7 +579,7 @@ const SidebarMenuButton = React.forwardRef< data-active={isActive} data-has-icon={hasIcon} tabIndex={computedTabIndex} - className={cn(sidebarMenuButtonVariants({ variant, size, hasIcon }), className)} + className={cn(sidebarMenuButtonVariants({ variant, size, hasIcon, isLoading }), className)} {...props} /> ) From 726e226f666a9da7e03be6592df5af0814c72ca6 Mon Sep 17 00:00:00 2001 From: "supabase-supabase-autofixer[bot]" <248690971+supabase-supabase-autofixer[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:34:58 +0000 Subject: [PATCH 011/488] [bot] Sync from supabase/troubleshooting (#46971) This PR syncs the latest troubleshooting guides from the supabase/troubleshooting repository. --------- Co-authored-by: github-docs-bot Co-authored-by: Chris Chinchilla Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- ...t-exist-when-using-or-operators-46ff23.mdx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx diff --git a/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx new file mode 100644 index 00000000000..09c1c7314bf --- /dev/null +++ b/apps/docs/content/troubleshooting/postgrest-error-400-column-example_tableexample_column-does-not-exist-when-using-or-operators-46ff23.mdx @@ -0,0 +1,46 @@ +--- +title = "PostgREST error: 400 'column example_table.example_column does not exist' when using OR operators" +date_created = "2026-06-15T04:04:26+00:00" +topics = [ "platform", "api" ] +keywords = [ "postgrest", "column does not exist" ] +[[errors]] +http_status_code = 400 +message = "column example_table.example_column does not exist" + +--- + +If you receive a `400` error with the message `column example_table.example_column does not exist` only on mutation requests (`PATCH`, `POST`, `DELETE`) — while `SELECT` queries on the same column work fine — this is a known bug in PostgREST versions before 14.4 ([issue #3707](https://github.com/PostgREST/postgrest/issues/3707)). + +## Root cause + +The bug is triggered when an `or()` filter is included in a mutation request. PostgREST incorrectly applies the OR filter's column resolution logic to the mutation plan, causing it to fail to locate columns that exist and are otherwise accessible. + +## How to confirm + +1. **Reproduce the asymmetry** — run the same filter as a `GET` request. If it succeeds but the `PATCH`/`POST`/`DELETE` fails with the same column reference, the bug is the likely cause. +2. **Check your PostgREST version** — go to [Project Settings > Infrastructure](/dashboard/project/_/settings/infrastructure) and note the PostgreSQL version. PostgREST 14.1 and earlier are affected; 14.4+ includes the fix. +3. **Check Postgres logs** — if you have logging enabled, you should see `column "example_column" does not exist` errors correlating with the timestamps of the failed mutation requests. + +## Resolution + +### Option 1: Upgrade (permanent fix) + +Navigate to [Project Settings > Infrastructure](/dashboard/project/_/settings/infrastructure) and upgrade to the latest Postgres version. This automatically upgrades PostgREST to 14.5+, where the bug is resolved. + +### Option 2: Query workaround (immediate) + +Explicitly include the column referenced in the error in the `select` parameter of your mutation request. This forces PostgREST to resolve the column correctly during the mutation plan. + +For example, if your request looks like: + +``` +PATCH /rest/v1/example_table?or=(example_column.eq.value1,example_column.eq.value2) +``` + +Add `select=` with the affected column: + +``` +PATCH /rest/v1/example_table?or=(example_column.eq.value1,example_column.eq.value2)&select=id,example_column +``` + +This workaround applies per-request and does not require any schema changes or infrastructure modifications. From b4a15bb0c897de87bbd7c775fe47608a05cc93b5 Mon Sep 17 00:00:00 2001 From: Guillaume Faas <59444272+Tr00d@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:18:56 +0200 Subject: [PATCH 012/488] chores(docs): add Guillaume Faas to humans.txt (#46968) Contributed through a fork as I'm not yet in the GitHub Org ## Summary by CodeRabbit * **Chores** * Updated the public team roster by adding a new team member. --- 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 9324b96b44e..a2765dcef91 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -111,6 +111,7 @@ Greg Marzouka Greg P Greg Richardson Gregor Vand +Guillaume Faas Guilherme Souza Hanefi Önaldı Hannah Bowers From 30ee68964fb4df072df899906524ffc9be7b8ffe Mon Sep 17 00:00:00 2001 From: Jeremias Menichelli Date: Tue, 16 Jun 2026 12:47:19 +0200 Subject: [PATCH 013/488] chore:new AST based script supporting current markdown for LLMs (#46949) --- .../internals/generate-guides-markdown.ts | 323 ++++++++---------- .../internals/markdown-schema/Admonition.ts | 13 + apps/docs/internals/markdown-schema/Link.ts | 12 + apps/docs/internals/markdown-schema/Panel.ts | 3 + .../internals/markdown-schema/StepHike.ts | 16 + .../internals/markdown-schema/TabPanel.ts | 9 + 6 files changed, 187 insertions(+), 189 deletions(-) create mode 100644 apps/docs/internals/markdown-schema/Admonition.ts create mode 100644 apps/docs/internals/markdown-schema/Link.ts create mode 100644 apps/docs/internals/markdown-schema/Panel.ts create mode 100644 apps/docs/internals/markdown-schema/StepHike.ts create mode 100644 apps/docs/internals/markdown-schema/TabPanel.ts diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts index 6758ba073ac..856ec2c79c7 100644 --- a/apps/docs/internals/generate-guides-markdown.ts +++ b/apps/docs/internals/generate-guides-markdown.ts @@ -1,198 +1,160 @@ -import fs from 'node:fs' +import fs from 'node:fs/promises' import path from 'node:path' import { globby } from 'globby' import matter from 'gray-matter' +import type { Content, Parent, Root } from 'mdast' +import { fromMarkdown } from 'mdast-util-from-markdown' +import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm' +import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx' +import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx' +import { toMarkdown } from 'mdast-util-to-markdown' +import { gfm } from 'micromark-extension-gfm' +import { mdxjs } from 'micromark-extension-mdxjs' -import { getInternalLinkBaseUrl, prefixInternalLinks, withDocsBasePath } from './internal-links' +import { getInternalLinkBaseUrl, prefixInternalLinks } from './internal-links' +import { Link } from './markdown-schema/Link' +import { Panel } from './markdown-schema/Panel' +import { StepHike } from './markdown-schema/StepHike' +import { TabPanel } from './markdown-schema/TabPanel' +import { Admonition } from './markdown-schema/Admonition' const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials') +type JsxNode = MdxJsxFlowElement | MdxJsxTextElement +type Props = Record + /** - * Reads <$Partial path="..." /> tags and replaces them with the file contents. - * Recurses to handle nested partials. + * A handler converts a single MDX component into a markdown string. It receives + * the component's props, the already-serialized markdown of its children, and + * the raw AST node (escape hatch for handlers that need to inspect structure). + * + * Any component not in the schema is treated as `({ children }) => children`, + * i.e. the wrapper is dropped and its children are kept as-is. */ -async function inlinePartials(content: string): Promise { - const partialRegex = /<\$Partial\s+path="([^"]+)"[^/]*\/>/g - const matches = [...content.matchAll(partialRegex)] - for (const [fullMatch, partialPath] of matches) { - try { - const raw = await fs.promises.readFile(path.join(PARTIALS_DIR, partialPath), 'utf8') - const { content: partialBody } = matter(raw) - const inlined = await inlinePartials(partialBody) - content = content.replace(fullMatch, inlined) - } catch { - content = content.replace(fullMatch, '') - } +type ComponentHandler = (ctx: { props: Props; children: string; node: JsxNode }) => string +type ComponentSchema = Record + +const PARSE_OPTIONS = { + extensions: [mdxjs(), gfm()], + mdastExtensions: [mdxFromMarkdown(), gfmFromMarkdown()], +} +const SERIALIZE_OPTIONS = { + extensions: [mdxToMarkdown(), gfmToMarkdown()], + bullet: '-' as const, + listItemIndent: 'one' as const, +} + +const parseMdx = (source: string): Root => fromMarkdown(source, PARSE_OPTIONS) +const serializeMdx = (tree: Parent): string => toMarkdown(tree as Root, SERIALIZE_OPTIONS) + +const defaultHandler: ComponentHandler = ({ children }) => children + +const isJsx = (n: Content): n is JsxNode => + n.type === 'mdxJsxFlowElement' || n.type === 'mdxJsxTextElement' + +function propsFrom(node: JsxNode): Props { + const props: Props = {} + for (const attr of node.attributes) { + if (attr.type !== 'mdxJsxAttribute') continue + if (attr.value == null) props[attr.name] = true + else if (typeof attr.value === 'string') props[attr.name] = attr.value + else props[attr.name] = attr.value.value } - return content -} - -/** Remove the minimum common leading whitespace from all non-empty lines. */ -function dedentBlock(text: string): string { - const lines = text.split('\n') - const nonEmpty = lines.filter((l) => /\S/.test(l)) - if (!nonEmpty.length) return text - const minIndent = Math.min(...nonEmpty.map((l) => (l.match(/^([ \t]*)/) ?? ['', ''])[1].length)) - if (!minIndent) return text - return lines.map((l) => l.slice(minIndent)).join('\n') + return props } /** - * Converts StepHikeCompact components to markdown ordered lists. - * Each step becomes a numbered item: the title (from Details) is bolded on the item - * line, and the full step body (Details + Code) is dedented and appended below. - * Remaining JSX tags inside the body are later stripped by stripJsxTags. + * Replaces every `<$Partial path="..." />` in the tree with the parsed AST of + * the referenced file. Recurses so partials may include other partials. */ -function convertStepHike(content: string): string { - return content.replace(/([\s\S]*?)<\/StepHikeCompact>/g, (_, body) => { - const items: string[] = [] - const stepRe = /]*>([\s\S]*?)<\/StepHikeCompact\.Step>/g - let stepNum = 1 - let m: RegExpExecArray | null - while ((m = stepRe.exec(body)) !== null) { - const stepBody = m[1] - const titleMatch = stepBody.match(/]+title="([^"]*)"/) - const title = titleMatch ? titleMatch[1] : '' - // Dedent the entire step body so nested JSX indentation is removed. - // Remaining component tags (Details, Code, Admonition…) are stripped later. - const inner = dedentBlock(stepBody).trim() - const item = title ? `${stepNum}. **${title}**\n\n${inner}` : `${stepNum}. ${inner}` - items.push(item) - stepNum++ +async function inlinePartials(parent: Parent): Promise { + const next: Content[] = [] + for (const child of parent.children as Content[]) { + if (isJsx(child) && child.name === '$Partial') { + const partialPath = String(propsFrom(child).path ?? '') + try { + const raw = await fs.readFile(path.join(PARTIALS_DIR, partialPath), 'utf8') + const subtree = parseMdx(matter(raw).content) + await inlinePartials(subtree) + next.push(...(subtree.children as Content[])) + } catch { + // missing or broken partials are silently dropped + } + continue } - return items.join('\n\n') - }) -} - -/** - * Extracts a `title` prop value from a JSX opening-tag's attribute text. - * Supports `title="..."`, `title='...'`, and `title={...}` forms. - * For JSX titles, strips inline tags and collapses whitespace so the text - * content remains. - */ -function extractTitleAttr(attrs: string): string | null { - const strMatch = attrs.match(/\btitle=(?:"([^"]+)"|'([^']+)')/) - if (strMatch) return strMatch[1] ?? strMatch[2] - - const exprIdx = attrs.indexOf('title={') - if (exprIdx === -1) return null - - let i = exprIdx + 'title={'.length - const inner: string[] = [] - let depth = 1 - while (i < attrs.length && depth > 0) { - const ch = attrs[i] - if (ch === '{') depth++ - else if (ch === '}') { - depth-- - if (depth === 0) break - } - inner.push(ch) - i++ + if ('children' in child) await inlinePartials(child as Parent) + next.push(child) } - return inner - .join('') - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim() + parent.children = next as Parent['children'] } /** - * Converts `desc` - * blocks into markdown bullet lines: `- [title](href). description`. Without - * this, the rendered output keeps prop syntax (className, passHref, etc.) since - * stripping JSX tags discards inner text from props but leaves panel children - * floating without context. Applied to all guides so resource-card grids render - * consistently in the markdown view. + * Walks the tree bottom-up. For each JSX element, runs its schema handler (or + * the default) and replaces the node with an `html` node holding the result. + * The `html` type passes through `mdast-util-to-markdown` verbatim, so whatever + * markdown the handler returns lands in the final output unchanged. */ -function convertLinkPanels(content: string): string { - return content.replace(/([\s\S]*?)<\/Link>/g, (full, linkAttrs, body) => { - const hrefMatch = linkAttrs.match(/\bhref="([^"]+)"/) - if (!hrefMatch) return full - const href = withDocsBasePath(hrefMatch[1]) - - const panelOpen = body.match(/<(GlassPanel|IconPanel)\b/) - if (!panelOpen) return full - const panelName = panelOpen[1] - const panelStart = panelOpen.index ?? 0 - - // Walk past the opening tag, respecting JSX expression braces so attribute - // values like `title={...}` don't terminate parsing early. - let i = panelStart + panelOpen[0].length - let depth = 0 - while (i < body.length) { - const ch = body[i] - if (ch === '{') depth++ - else if (ch === '}') depth-- - else if (ch === '>' && depth === 0) break - i++ +function applySchema(parent: Parent, schema: ComponentSchema): void { + for (const child of parent.children as Content[]) { + if ('children' in child) applySchema(child as Parent, schema) + } + const next: Content[] = [] + for (const child of parent.children as Content[]) { + if ( + child.type === 'mdxFlowExpression' || + child.type === 'mdxTextExpression' || + child.type === 'mdxjsEsm' + ) { + continue } - if (i >= body.length) return full - - const panelAttrs = body.slice(panelStart + panelOpen[0].length, i) - const closingTag = `` - const closeIdx = body.indexOf(closingTag, i) - if (closeIdx === -1) return full - - const title = extractTitleAttr(panelAttrs) - if (!title) return full - - const description = body - .slice(i + 1, closeIdx) - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim() - - return `- [${title}](${href})${description ? `. ${description}` : ''}` - }) + if (isJsx(child)) { + const handler = schema[child.name ?? ''] ?? defaultHandler + const children = serializeMdx({ + type: 'root', + children: child.children as Root['children'], + }).trim() + const value = handler({ props: propsFrom(child), children, node: child }) + next.push({ type: 'html', value } as Content) + continue + } + next.push(child) + } + parent.children = next as Parent['children'] } /** - * Strips JSX component tags (capitalized names, dot-notation, or $-prefixed) - * while keeping their inner content. Also strips wrapper div and a elements. - * Removes MDX JSX comment blocks. Strips unnecessary leading indentation from - * non-code-block lines. + * Per-component overrides. Each handler receives `{ props, children, node }` + * and returns the markdown string that should replace the JSX element. Any + * component not listed is unwrapped (children are kept, wrapper is dropped). */ -function stripJsxTags(content: string): string { - // Remove MDX/JSX comments {/* ... */} - content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, '') +const SCHEMA: ComponentSchema = { + Admonition, + Link, + GlassPanel: Panel, + IconPanel: Panel, + ...StepHike, + TabPanel, +} - // Remove self-closing JSX components: or <$Directive ... /> - content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*\/>/gs, '') +async function generateOne(filePath: string, linkBaseUrl: string): Promise { + const raw = await fs.readFile(filePath, 'utf8') + const { content, data } = matter(raw) - // Remove opening JSX component tags (possibly multi-line): - content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*>/gs, '') + const tree = parseMdx(content) + await inlinePartials(tree) + applySchema(tree, SCHEMA) + const body = prefixInternalLinks(serializeMdx(tree), linkBaseUrl) - // Remove closing JSX component tags: - content = content.replace(/<\/[\$A-Z][\w.]*>/g, '') + const headerParts: string[] = [] + if (data.title) headerParts.push(`# ${data.title}`) + if (data.subtitle) headerParts.push(String(data.subtitle)) + // Add description only when differs from subtitle. + if (data.description && String(data.description) !== String(data.subtitle)) + headerParts.push(String(data.description)) - // Remove wrapper div and a elements used structurally in MDX (carry JSX props - // like className which are not valid HTML; inner content such as img is preserved) - content = content.replace(/]*)?\s*>/g, '') - content = content.replace(/<\/div>/g, '') - content = content.replace(/]*)?\s*>/g, '') - content = content.replace(/<\/a>/g, '') + const header = headerParts.join('\n\n') - // Split on fenced code blocks to handle prose and code separately. - // For prose (even segments): strip leading whitespace (removes JSX nesting indent). - // For code blocks (odd segments): dedent the body to remove JSX nesting indent while - // preserving relative code structure, then normalize the closing fence. - const segments = content.split(/(```[\s\S]*?```)/g) - content = segments - .map((seg, i) => { - if (i % 2 === 0) return seg.replace(/^[ \t]+/gm, '') - return seg.replace( - /^(```[^\n]*\n)([\s\S]*?)(\n[ \t]*```)$/, - (_, open, body) => open + dedentBlock(body) + '\n```' - ) - }) - .join('') - - // Collapse lines that are only whitespace to empty lines, then deduplicate blank lines - content = content.replace(/^[^\S\n]+$/gm, '') - content = content.replace(/\n{3,}/g, '\n\n').trim() - - return content + return header ? `${header}\n\n${body}` : body } async function generate() { @@ -202,47 +164,30 @@ async function generate() { await Promise.all( files.map(async (filePath) => { + // content/guides/ai/vector-columns.mdx → public/markdown/guides/ai/vector-columns.md + // Placing under public/markdown/ ensures the file is served at /docs/guides/... + // matching the URL of the rendered page. const outPath = filePath .replace(/^content\/guides\//, 'public/markdown/guides/') .replace(/\.mdx$/, '.md') let output: string try { - const raw = await fs.promises.readFile(filePath, 'utf8') - const { content: rawContent, data } = matter(raw) - - const withPartials = await inlinePartials(rawContent) - const withSteps = convertStepHike(withPartials) - const withLinks = convertLinkPanels(withSteps) - const stripped = stripJsxTags(withLinks) - const processed = prefixInternalLinks(stripped, linkBaseUrl) - - const header = [ - data.title ? `# ${data.title}` : '', - data.subtitle || data.description ? `\n${data.subtitle ?? data.description}` : '', - ] - .filter(Boolean) - .join('\n') - - output = header ? `${header}\n\n${processed}` : processed + output = await generateOne(filePath, linkBaseUrl) } catch (err) { warnings++ console.warn( `[warn] Failed to process ${filePath}: ${err instanceof Error ? err.message : err}` ) - // Fall back to raw file content so the route still serves something try { - output = await fs.promises.readFile(filePath, 'utf8') + output = await fs.readFile(filePath, 'utf8') } catch { output = `` } } - // content/guides/ai/vector-columns.mdx → public/markdown/guides/ai/vector-columns.md - // Placing under public/markdown/ ensures the file is served at /docs/guides/... - // matching the exact URL of the rendered page. - await fs.promises.mkdir(path.dirname(outPath), { recursive: true }) - await fs.promises.writeFile(outPath, output) + await fs.mkdir(path.dirname(outPath), { recursive: true }) + await fs.writeFile(outPath, output) }) ) diff --git a/apps/docs/internals/markdown-schema/Admonition.ts b/apps/docs/internals/markdown-schema/Admonition.ts new file mode 100644 index 00000000000..06d90d5875b --- /dev/null +++ b/apps/docs/internals/markdown-schema/Admonition.ts @@ -0,0 +1,13 @@ +export const Admonition = ({ + props, + children, +}: { + props: Record + children: string +}): string => { + const type = props.type ? String(props.type) : 'note' + return `${capitalize(type)}: ${children}` +} + +const capitalize = (s: T) => + (s[0].toUpperCase() + s.slice(1)) as Capitalize diff --git a/apps/docs/internals/markdown-schema/Link.ts b/apps/docs/internals/markdown-schema/Link.ts new file mode 100644 index 00000000000..3d0888c956a --- /dev/null +++ b/apps/docs/internals/markdown-schema/Link.ts @@ -0,0 +1,12 @@ +import { withDocsBasePath } from '../internal-links' + +export const Link = ({ + props, + children, +}: { + props: Record + children: string +}): string => { + const href = withDocsBasePath(String(props.href ?? '')) + return `[${children}](${href})` +} diff --git a/apps/docs/internals/markdown-schema/Panel.ts b/apps/docs/internals/markdown-schema/Panel.ts new file mode 100644 index 00000000000..2962e24d403 --- /dev/null +++ b/apps/docs/internals/markdown-schema/Panel.ts @@ -0,0 +1,3 @@ +export const Panel = ({ props }: { props: Record }): string => { + return String(props.title) +} diff --git a/apps/docs/internals/markdown-schema/StepHike.ts b/apps/docs/internals/markdown-schema/StepHike.ts new file mode 100644 index 00000000000..89435a7e054 --- /dev/null +++ b/apps/docs/internals/markdown-schema/StepHike.ts @@ -0,0 +1,16 @@ +type Props = Record +type Ctx = { props: Props; children: string } + +const Step = ({ props, children }: Ctx): string => { + const step = String(props.step ?? '').trim() + return step ? `${step}. ${children}` : children +} + +const Details = ({ props, children }: Ctx): string => { + return props.title ? `**${String(props.title).trim()}**\n\n${children}` : children +} + +export const StepHike = { + 'StepHikeCompact.Step': Step, + 'StepHikeCompact.Details': Details, +} diff --git a/apps/docs/internals/markdown-schema/TabPanel.ts b/apps/docs/internals/markdown-schema/TabPanel.ts new file mode 100644 index 00000000000..3ffbd801e69 --- /dev/null +++ b/apps/docs/internals/markdown-schema/TabPanel.ts @@ -0,0 +1,9 @@ +export const TabPanel = ({ + props, + children, +}: { + props: Record + children: string +}): string => { + return props.label ? `**${String(props.label)}**\n\n${children}` : children +} From a10bc9a88ae0ce1b19c77fd811e4fb7abc61426c Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:50:52 +0200 Subject: [PATCH 014/488] feat(unified-logs): compact toggle banner and ArrowDown selects first log (#46812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual cleanup of the Logs sidebar (unified logs): - Restyle the "Go back to old logs" and "Try Unified Logs" banners as full-bleed bordered strips (no card), with an icon-only switch-back button and a tooltip - Add a "Beta" badge next to the "Logs" sidebar title - Rename the product from "Logs & Analytics" to "Logs" ## Summary by CodeRabbit * **New Features** * Added Unified Logs banner with “Try Unified Logs” and “More information” actions, plus a “Go back to old logs” utility layout. * Show a **“Beta”** badge for Unified Logs eligibility. * **UX Updates** * Updated the Logs experience to consistently display under the **“Logs”** product layout. * **Tests / Documentation** * Updated end-to-end checks and examples to expect the **“Logs”** heading text exactly. --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: kemal --- .../UnifiedLogs/UnifiedLogsBanner.tsx | 63 +++++++++++++++++++ .../layouts/LogsLayout/LogsLayout.tsx | 10 ++- .../layouts/LogsLayout/LogsSidebarMenuV2.tsx | 37 +++-------- .../components/ui/DataTable/FilterSideBar.tsx | 38 ++++------- e2e/studio/README.md | 2 +- e2e/studio/features/logs.spec.ts | 6 +- 6 files changed, 93 insertions(+), 63 deletions(-) create mode 100644 apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsBanner.tsx diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsBanner.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsBanner.tsx new file mode 100644 index 00000000000..15cc4bcba18 --- /dev/null +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogsBanner.tsx @@ -0,0 +1,63 @@ +import { CircleHelpIcon, Undo2 } from 'lucide-react' +import { Button, cn } from 'ui' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' + +interface UnifiedLogsBannerProps { + variant: 'promo' | 'utility' + onEnable?: () => void + onMoreInfo?: () => void + onSwitchBack?: () => void + className?: string +} + +export function UnifiedLogsBanner({ + variant, + onEnable, + onMoreInfo, + onSwitchBack, + className, +}: UnifiedLogsBannerProps) { + const stripClassName = cn( + 'flex items-center justify-between gap-3 border-b border-border px-4 py-3', + className + ) + + if (variant === 'utility') { + return ( +
+
+

Go back to old logs

+

Use the traditional interface

+
+ } + onClick={onSwitchBack} + tooltip={{ content: { side: 'bottom', text: 'Switch back' } }} + /> +
+ ) + } + + return ( +
+

+ Try Unified Logs +

+
+ + } + onClick={onMoreInfo} + tooltip={{ content: { side: 'bottom', text: 'More information' } }} + /> +
+
+ ) +} diff --git a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx index 36176eb3da8..9d2af4613f3 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsLayout.tsx @@ -25,7 +25,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => if (!logsEnabled) { return ( - + ) @@ -33,14 +33,12 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => if (!canUseLogsExplorer) { if (isLoading) { - return ( - - ) + return } if (!isLoading && !canUseLogsExplorer) { return ( - + ) @@ -49,7 +47,7 @@ const LogsLayout = ({ title, children }: PropsWithChildren) => return ( } > diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index b2daa4da0f3..d2d135e867d 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -1,5 +1,5 @@ import { IS_PLATFORM, useFlag, useParams } from 'common' -import { ChevronRight, CircleHelpIcon, Plus } from 'lucide-react' +import { ChevronRight, Plus } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import React, { useState } from 'react' @@ -29,7 +29,7 @@ import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replicati import { LOG_DRAIN_TYPES } from '@/components/interfaces/LogDrains/LogDrains.constants' import SavedQueriesItem from '@/components/interfaces/Settings/Logs/Logs.SavedQueriesItem' import { LogsSidebarItem } from '@/components/interfaces/Settings/Logs/SidebarV2/SidebarItem' -import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { UnifiedLogsBanner } from '@/components/interfaces/UnifiedLogs/UnifiedLogsBanner' import { useContentQuery } from '@/data/content/content-query' import { useReplicationSourcesQuery } from '@/data/replication/sources-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' @@ -241,32 +241,13 @@ export function LogsSidebarMenuV2() { /> )} {isUnifiedLogsEligible && ( - New} - actions={ - <> - - } - onClick={() => selectFeaturePreview('supabase-ui-preview-unified-logs')} - tooltip={{ content: { side: 'bottom', text: 'More information' } }} - /> - - } + { + enableUnifiedLogs() + router.push(`/project/${ref}/logs`) + }} + onMoreInfo={() => selectFeaturePreview('supabase-ui-preview-unified-logs')} /> )} diff --git a/apps/studio/components/ui/DataTable/FilterSideBar.tsx b/apps/studio/components/ui/DataTable/FilterSideBar.tsx index e4297f5a50b..6ed57d3e663 100644 --- a/apps/studio/components/ui/DataTable/FilterSideBar.tsx +++ b/apps/studio/components/ui/DataTable/FilterSideBar.tsx @@ -1,10 +1,9 @@ -import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' import React, { useEffect, type ReactNode } from 'react' -import { Button, cn, ResizablePanel, usePanelRef } from 'ui' +import { Badge, Button, cn, ResizablePanel, usePanelRef } from 'ui' -import { FeaturePreviewBadge } from '../FeaturePreviewBadge' import { FeaturePreviewSidebarPanel } from '../FeaturePreviewSidebarPanel' import { DateRangeDisabled } from './DataTable.types' import { DataTableFilterControls } from './DataTableFilters/DataTableFilterControls' @@ -12,6 +11,7 @@ import { DataTableResetButton } from './DataTableResetButton' import { useDataTable } from './providers/DataTableProvider' import { useUnifiedLogsPreview } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { LOG_DRAIN_TYPES } from '@/components/interfaces/LogDrains/LogDrains.constants' +import { UnifiedLogsBanner } from '@/components/interfaces/UnifiedLogs/UnifiedLogsBanner' interface FilterSideBarProps { isFilterBarOpen: boolean @@ -30,11 +30,7 @@ export function FilterSideBar({ const { ref } = useParams() const { table } = useDataTable() - const { - disable: disableUnifiedLogs, - isEligible: isUnifiedLogsEligible, - isEnabled: isUnifiedLogsEnabled, - } = useUnifiedLogsPreview() + const { disable: disableUnifiedLogs, isEligible: isUnifiedLogsEligible } = useUnifiedLogsPreview() const handleGoBackToOldLogs = () => { disableUnifiedLogs() @@ -70,29 +66,19 @@ export function FilterSideBar({ >
-

Logs

+
+

Logs

+ {isUnifiedLogsEligible && Beta} +
{table.getState().columnFilters.length ? : null}
+ {isUnifiedLogsEligible && ( + + )} +
- {isUnifiedLogsEligible && ( - - ) : undefined - } - actions={ - - } - /> - )} {afterFilters} { */ await page.goto(toUrl(`/project/${ref}/logs/${logPage.route}`)) - await expect(page.getByRole('heading', { name: 'Logs & Analytics' }), { - message: 'Logs & Analytics heading should be visible', + await expect(page.getByRole('heading', { name: 'Logs', exact: true }), { + message: 'Logs heading should be visible', }).toBeVisible() /** From c1fcec6901217d87d5972b487847ce833887bcbb Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 16 Jun 2026 21:21:03 +0900 Subject: [PATCH 015/488] chore: Update stats on careers page (#46978) ## 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 the career page stats ## Summary by CodeRabbit * **Chores** * Updated the career page company metrics: team members increased to 350+, countries expanded to 60+, and total capital raised updated to $1B. --------- Co-authored-by: Ivan Vasilov --- apps/www/data/career.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/www/data/career.json b/apps/www/data/career.json index 7d1672ab0f4..42349510741 100644 --- a/apps/www/data/career.json +++ b/apps/www/data/career.json @@ -1,15 +1,15 @@ { "company": [ { - "number": "280+", - "text": "team members\n\nin 55+ countries" + "number": "350+", + "text": "team members\n\nin 60+ countries" }, { "number": "20+", "text": "languages spoken" }, { - "number": "$500M", + "number": "$1B", "text": "raised" }, { @@ -17,7 +17,7 @@ "text": "community members" }, { - "number": "30,000+", + "number": "40,000+", "text": "memes posted\n\n(and counting)" } ], From ccf18fe9044b6eac45740a0a6c66d562be69c768 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Jun 2026 22:02:04 +0800 Subject: [PATCH 016/488] Prevent refetch on focus and reconnect for /query requests that failed due to statement timeouts (#46972) ## Context As per PR title - prevents refetch on focus and reconnect for /query requests that failed due to statement timeouts, presumably that those requests will run into the same problem either way so this minimizes unnecessary impact to the database ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of statement timeouts to prevent automatic retry attempts after window focus or reconnection. * Enhanced query execution request identification for better query tracking. --- apps/studio/data/query-client.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/studio/data/query-client.ts b/apps/studio/data/query-client.ts index 43306d39139..6f7b578c6a5 100644 --- a/apps/studio/data/query-client.ts +++ b/apps/studio/data/query-client.ts @@ -20,6 +20,16 @@ const SKIP_RETRY_PATHNAME_MATCHERS = [ export const MAX_RETRY_FAILURE_COUNT = 3 +function isQueryEndpointStatementTimeout(error: unknown) { + const PG_META_QUERY_PATHNAME_MATCHER = match('/platform/pg-meta/:ref/query') + return ( + error instanceof ResponseError && + !!error.requestPathname && + !!PG_META_QUERY_PATHNAME_MATCHER(error.requestPathname) && + !!error.message?.includes('canceling statement due to statement timeout') + ) +} + let queryClient: QueryClient | undefined export function getQueryClient() { @@ -71,6 +81,17 @@ export function getQueryClient() { // react-query default: doubles, starting at 1000ms, with each attempt, but will not exceed 30 seconds return Math.min(1000 * 2 ** failureCount, 30000) }, + refetchOnWindowFocus(query) { + // [Joshen] Opting to not refetch a /query request that failed due to a statement + // timeout, presumably that it'll just run into the same issue. Can however be overriden + // on a per case basis on each individual query hook + if (isQueryEndpointStatementTimeout(query.state.error)) return false + return true + }, + refetchOnReconnect(query) { + if (isQueryEndpointStatementTimeout(query.state.error)) return false + return true + }, }, }, }) From 0c9eb15cbab161e4af4f099b73c8a5c9be549547 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:17:28 +0200 Subject: [PATCH 017/488] refactor(studio): remove legacy homepage usage section (V1) (#46994) ## Problem The homepage usage section had two implementations gated by the `newHomepageUsageV2` ConfigCat flag, with the legacy V1 as the fallback. That flag has been at 100% in production for months, so V1 is dead code and the flag branch is unnecessary. ## Fix - Make V2 the default by removing the `newHomepageUsageV2` flag check in `Home.tsx`. - Delete the V1 section (`Home/ProjectUsageSection.tsx`), its chart (`Home/ProjectUsage.tsx`), and the now-orphaned `project-log-requests-count-query` plus its query key. - Shared code (`useProjectLogStatsQuery`, `UsageApiCounts`, `ProjectLogStatsVariables`) is kept since V2 and other modules still use it. The `newHomepageUsageV2` flag can be removed from ConfigCat after this merges. ## How to test - Open a project homepage on platform. - Confirm the usage section still renders (the V2 layout) with no flag dependency. - Verify no console errors and no broken imports. - Expected result: identical homepage usage section to what production shows today. ## Notes - This is independent of the in-flight service-health usage charts work (PR #46373), which is behind its own `newHomepageUsageDeltas` flag. Whichever merges second will resolve a small conflict on the `UsageSection` selection in `Home.tsx`. ## Summary by CodeRabbit * **Refactor** * Removed project usage statistics section from the home page. * Simplified the home page experience by consolidating feature flag variants into a standardized implementation. Co-authored-by: Claude Opus 4.8 --- .../interfaces/Home/ProjectUsage.tsx | 238 ------------------ .../interfaces/Home/ProjectUsageSection.tsx | 38 --- .../interfaces/ProjectHome/Home.tsx | 11 +- apps/studio/data/analytics/keys.ts | 3 - .../project-log-requests-count-query.ts | 58 ----- 5 files changed, 3 insertions(+), 345 deletions(-) delete mode 100644 apps/studio/components/interfaces/Home/ProjectUsage.tsx delete mode 100644 apps/studio/components/interfaces/Home/ProjectUsageSection.tsx delete mode 100644 apps/studio/data/analytics/project-log-requests-count-query.ts diff --git a/apps/studio/components/interfaces/Home/ProjectUsage.tsx b/apps/studio/components/interfaces/Home/ProjectUsage.tsx deleted file mode 100644 index abf36066ec2..00000000000 --- a/apps/studio/components/interfaces/Home/ProjectUsage.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { useParams } from 'common' -import dayjs from 'dayjs' -import { Auth, Database, Realtime, Storage } from 'icons' -import sumBy from 'lodash/sumBy' -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { Loading } from 'ui' - -import BarChart from '@/components/ui/Charts/BarChart' -import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown' -import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils' -import Panel from '@/components/ui/Panel' -import { - ProjectLogStatsVariables, - UsageApiCounts, - useProjectLogStatsQuery, -} from '@/data/analytics/project-log-stats-query' -import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted' -import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' -import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' - -type ChartIntervalKey = ProjectLogStatsVariables['interval'] - -const ProjectUsage = () => { - const router = useRouter() - const { ref: projectRef } = useParams() - const { data: organization } = useSelectedOrganizationQuery() - - const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([ - 'project_auth:all', - 'project_storage:all', - ]) - - const { getEntitlementMax } = useCheckEntitlements('log.retention_days') - const retentionDays = getEntitlementMax() - - const DEFAULT_INTERVAL: ChartIntervalKey = - retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day' - - const [interval, setInterval] = useState(DEFAULT_INTERVAL) - - useEffect(() => { - setInterval(retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day') - }, [retentionDays]) - - const { data, isPending: isLoading } = useProjectLogStatsQuery({ projectRef, interval }) - - const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1] - const startDateLocal = dayjs().subtract( - selectedInterval.startValue, - selectedInterval.startUnit as dayjs.ManipulateType - ) - const endDateLocal = dayjs() - const { data: charts } = useFillTimeseriesSorted({ - data: data?.result ?? [], - timestampKey: 'timestamp', - valueKey: [ - 'total_auth_requests', - 'total_rest_requests', - 'total_storage_requests', - 'total_realtime_requests', - ], - defaultValue: 0, - startDate: startDateLocal.toISOString(), - endDate: endDateLocal.toISOString(), - minPointsToFill: 5, - }) - const datetimeFormat = selectedInterval.format || 'MMM D, ha' - - const handleBarClick = ( - value: UsageApiCounts, - _type: 'rest' | 'realtime' | 'storage' | 'auth' - ) => { - const unit = selectedInterval.startUnit - const selectedStart = dayjs(value?.timestamp) - const selectedEnd = selectedStart.add(1, unit) - - if (_type === 'rest') { - router.push( - `/project/${projectRef}/logs/edge-logs?its=${selectedStart.toISOString()}&ite=${selectedEnd.toISOString()}` - ) - return - } - - router.push( - `/project/${projectRef}/logs/edge-logs?its=${selectedStart.toISOString()}&ite=${selectedEnd.toISOString()}&f=${JSON.stringify( - { - product: { - [_type]: true, - }, - } - )}` - ) - } - - return ( -
-
- setInterval(interval as ProjectLogStatsVariables['interval'])} - organizationSlug={organization?.slug} - dropdownAlign="start" - tooltipSide="right" - /> - - Statistics for {selectedInterval.label.toLowerCase()} - -
-
- - - - -
- } - title="Database" - href={`/project/${projectRef}/editor`} - /> - - - handleBarClick(v as UsageApiCounts, 'rest')} - customDateFormat={datetimeFormat} - highlightedValue={sumBy(charts, 'total_rest_requests')} - /> - - - - {authEnabled && ( - - - - -
- } - title="Auth" - href={`/project/${projectRef}/auth/users`} - /> - - handleBarClick(v as UsageApiCounts, 'auth')} - customDateFormat={datetimeFormat} - highlightedValue={sumBy(charts || [], 'total_auth_requests')} - /> - - - - )} - {storageEnabled && ( - - - - -
- } - title="Storage" - href={`/project/${projectRef}/storage/buckets`} - /> - - - handleBarClick(v as UsageApiCounts, 'storage')} - customDateFormat={datetimeFormat} - highlightedValue={sumBy(charts, 'total_storage_requests')} - /> - - - - )} - - - - -
- } - title="Realtime" - /> - - - handleBarClick(v as UsageApiCounts, 'realtime')} - customDateFormat={datetimeFormat} - highlightedValue={sumBy(charts, 'total_realtime_requests')} - /> - - - -
-
- ) -} -export default ProjectUsage - -const PanelHeader = (props: any) => { - const Tag = props?.href ? Link : 'div' - return ( - -
-
{props.icon}
- -

{props.title}

-
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Home/ProjectUsageSection.tsx b/apps/studio/components/interfaces/Home/ProjectUsageSection.tsx deleted file mode 100644 index a59bc01245d..00000000000 --- a/apps/studio/components/interfaces/Home/ProjectUsageSection.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { AlertCircle } from 'lucide-react' - -import ProjectUsage from './ProjectUsage' -import { ProjectUsageLoadingState } from '@/components/layouts/ProjectLayout/LoadingState' -import InformationBox from '@/components/ui/InformationBox' -import { useProjectLogRequestsCountQuery } from '@/data/analytics/project-log-requests-count-query' -import { useProjectLogStatsQuery } from '@/data/analytics/project-log-stats-query' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' - -export const ProjectUsageSection = () => { - const { data: project } = useSelectedProjectQuery() - const { error, isPending: isLoading } = useProjectLogRequestsCountQuery({ - projectRef: project?.ref, - }) - - // wait for the stats to load before showing the usage section to eliminate multiple spinners - const { isPending: isLogsStatsLoading } = useProjectLogStatsQuery({ - projectRef: project?.ref, - interval: '1hr', - }) - - if (isLoading || isLogsStatsLoading) { - return - } - - if (error) { - return ( - } - title="There was an issue loading the usage details of your project" - /> - ) - } - - return -} diff --git a/apps/studio/components/interfaces/ProjectHome/Home.tsx b/apps/studio/components/interfaces/ProjectHome/Home.tsx index bf906404a4e..6186810f5ea 100644 --- a/apps/studio/components/interfaces/ProjectHome/Home.tsx +++ b/apps/studio/components/interfaces/ProjectHome/Home.tsx @@ -1,6 +1,6 @@ import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' -import { useFlag, useParams } from 'common' +import { useParams } from 'common' import dayjs from 'dayjs' import { useEffect, useRef } from 'react' import { cn } from 'ui' @@ -9,8 +9,7 @@ import { AdvisorSection } from './AdvisorSection' import { ConnectSection } from './ConnectSection' import { CustomReportSection } from './CustomReportSection' import { DEFAULT_SECTION_ORDER, mergeSectionOrder } from './Home.utils' -import { ProjectUsageSection as ProjectUsageSectionV2 } from './ProjectUsageSection' -import { ProjectUsageSection as ProjectUsageSectionV1 } from '@/components/interfaces/Home/ProjectUsageSection' +import { ProjectUsageSection } from './ProjectUsageSection' import { SortableSection } from '@/components/interfaces/ProjectHome/SortableSection' import { TopSection } from '@/components/interfaces/ProjectHome/TopSection' import { ProjectNeedsSecuring } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring' @@ -27,8 +26,6 @@ export const ProjectHome = () => { const { data: project } = useSelectedProjectQuery() const track = useTrack() - const showHomepageUsageV2 = useFlag('newHomepageUsageV2') - const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day')) const hasShownEnableBranchingModalRef = useRef(false) @@ -40,8 +37,6 @@ export const ProjectHome = () => { DEFAULT_SECTION_ORDER ) - const UsageSection = showHomepageUsageV2 ? ProjectUsageSectionV2 : ProjectUsageSectionV1 - const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) const handleDragEnd = (event: DragEndEvent) => { @@ -109,7 +104,7 @@ export const ProjectHome = () => { className={cn(isComingUp && 'opacity-60 pointer-events-none')} > - +
) diff --git a/apps/studio/data/analytics/keys.ts b/apps/studio/data/analytics/keys.ts index 966d2f80419..d95f98d4da9 100644 --- a/apps/studio/data/analytics/keys.ts +++ b/apps/studio/data/analytics/keys.ts @@ -153,9 +153,6 @@ export const analyticsKeys = { usageApiCounts: (projectRef: string | undefined, interval: string | undefined) => ['projects', projectRef, 'usage.api-counts', interval] as const, - usageApiRequestsCount: (projectRef: string | undefined) => - ['projects', projectRef, 'usage.api-requests-count'] as const, - serviceHealth: ( projectRef: string | undefined, { diff --git a/apps/studio/data/analytics/project-log-requests-count-query.ts b/apps/studio/data/analytics/project-log-requests-count-query.ts deleted file mode 100644 index 9c4697495df..00000000000 --- a/apps/studio/data/analytics/project-log-requests-count-query.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { QueryClient, useQuery } from '@tanstack/react-query' - -import { analyticsKeys } from './keys' -import { get, handleError } from '@/data/fetchers' -import type { ResponseError, UseCustomQueryOptions } from '@/types' - -export type ProjectLogRequestsCountVariables = { - projectRef?: string -} - -export async function getProjectLogRequestsCountStats( - { projectRef }: ProjectLogRequestsCountVariables, - signal?: AbortSignal -) { - if (!projectRef) { - throw new Error('projectRef is required') - } - - const { data, error } = await get( - '/platform/projects/{ref}/analytics/endpoints/usage.api-requests-count', - { - params: { path: { ref: projectRef } }, - signal, - } - ) - - if (error) handleError(error) - return data -} - -export type ProjectLogRequestsCountData = Awaited< - ReturnType -> -export type ProjectLogRequestsCountError = ResponseError - -export const useProjectLogRequestsCountQuery = ( - { projectRef }: ProjectLogRequestsCountVariables, - { - enabled = true, - ...options - }: UseCustomQueryOptions = {} -) => - useQuery({ - queryKey: analyticsKeys.usageApiRequestsCount(projectRef), - queryFn: ({ signal }) => getProjectLogRequestsCountStats({ projectRef }, signal), - enabled: enabled && typeof projectRef !== 'undefined', - ...options, - }) - -export function prefetchProjectLogRequestsCount( - client: QueryClient, - { projectRef }: ProjectLogRequestsCountVariables -) { - return client.fetchQuery({ - queryKey: analyticsKeys.usageApiRequestsCount(projectRef), - queryFn: ({ signal }) => getProjectLogRequestsCountStats({ projectRef }, signal), - }) -} From 13efb6bccf9b624357a3e564031f87558694b81c Mon Sep 17 00:00:00 2001 From: Chris Chinchilla Date: Tue, 16 Jun 2026 16:26:16 +0200 Subject: [PATCH 018/488] chore: Add self-hosted label to documentation (#46975) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## Summary by CodeRabbit * **Chores** * Updated GitHub workflow configuration for improved pull request management and categorization. --- .github/labeler.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index ee8e16372e6..24471e243d5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,6 +4,11 @@ documentation: - changed-files: - any-glob-to-any-file: 'apps/docs/**/*' +# Add 'self-hosted' to any change in the self-hosting docs +self-hosted: + - changed-files: + - any-glob-to-any-file: 'apps/docs/content/guides/self-hosting/**/*' + # Add 'api-deploy-required' to any change in packages/api-types/types api-deploy-required: - changed-files: From cb6c361f2553b1a5bcdf8cde620e46c7d2a5fe95 Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Tue, 16 Jun 2026 16:38:25 +0200 Subject: [PATCH 019/488] feat(replication): Improve replication validation and fix scroll (#46943) --- .../DestinationForm/PublicationsComboBox.tsx | 17 ++--- .../ReplicationDisclaimerDialog.tsx | 73 ------------------- .../ValidationFailuresSection.tsx | 8 +- .../ValidationWarningsDialog.tsx | 53 ++++++++++++++ .../DestinationForm/index.tsx | 64 ++++++++++------ .../DestinationPanel/DestinationPanel.tsx | 2 +- .../DestinationTypeSelection.tsx | 39 +++++----- .../validate-destination-mutation.ts | 35 ++++++++- packages/api-types/types/platform.d.ts | 31 ++++++++ 9 files changed, 193 insertions(+), 129 deletions(-) delete mode 100644 apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ReplicationDisclaimerDialog.tsx create mode 100644 apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationWarningsDialog.tsx diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PublicationsComboBox.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PublicationsComboBox.tsx index dd4e9fea9a3..a2a948e3262 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PublicationsComboBox.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PublicationsComboBox.tsx @@ -81,11 +81,6 @@ export const PublicationsComboBox = ({ value={searchTerm} onValueChange={setSearchTerm} /> -
-

- Publications with no tables are hidden -

-
{isLoadingPublications ? ( @@ -100,11 +95,15 @@ export const PublicationsComboBox = ({ {publications.length === 0 && ( -

- No publications available -

+
+

No publications available

+

Publications with no tables are hidden

+
)} - 7 ? 'h-[210px]' : ''}> + 7 ? 'h-[210px]' : ''} + onWheel={(e) => e.stopPropagation()} + > {publications.map((pub) => ( void - onConfirm: () => void -} - -export const ReplicationDisclaimerDialog = ({ - open, - isLoading, - onOpenChange, - onConfirm, -}: ReplicationDisclaimerDialogProps) => { - return ( - - - - Replication limitations - - - -

- Creating this replication pipeline will immediately start syncing data from your - publication into an external destination. Review the current limitations before - proceeding. -

- -
-
    -
  • - Custom data types replicate as strings.{' '} - Check that the destination can interpret those string values correctly. -
  • -
  • - Generated columns are skipped. Replace - them with triggers or materialized views if you need the derived values downstream. -
  • -
  • - - FULL replica identity is strongly recommended. - {' '} - With FULL replica identity deletes and updates include the payload that is needed to - correctly apply those changes. -
  • -
-
-
- - - - - - - -
-
- ) -} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationFailuresSection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationFailuresSection.tsx index d0c4ac0c299..5389e1d2b4a 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationFailuresSection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationFailuresSection.tsx @@ -41,10 +41,14 @@ export const ValidationFailuresSection = ({ {validationIssues.map((failure, idx) => ( - +

{failure.name} - {failure.failure_type === 'critical' && Required} + {failure.failure_type === 'critical' ? ( + Required + ) : ( + Warning + )}

diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationWarningsDialog.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationWarningsDialog.tsx new file mode 100644 index 00000000000..ac9051a2848 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/ValidationWarningsDialog.tsx @@ -0,0 +1,53 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from 'ui' + +interface ValidationWarningsDialogProps { + open: boolean + isLoading: boolean + warningCount: number + onOpenChange: (value: boolean) => void + onConfirm: () => void +} + +export const ValidationWarningsDialog = ({ + open, + isLoading, + warningCount, + onOpenChange, + onConfirm, +}: ValidationWarningsDialogProps) => { + return ( + + + + + Create destination with {warningCount} {warningCount === 1 ? 'warning' : 'warnings'}? + + + Replication can start, but the warnings listed above may affect how some changes are + applied downstream. Review them before proceeding. + + + + Cancel + + Proceed to create + + + + + ) +} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index c7902c1ae61..33ae182f427 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -35,8 +35,8 @@ import { import { NewPublicationPanel } from './NewPublicationPanel' import { NoDestinationsAvailable } from './NoDestinationsAvailable' import { PublicationSelection } from './PublicationSelection' -import { ReplicationDisclaimerDialog } from './ReplicationDisclaimerDialog' import { ValidationFailuresSection } from './ValidationFailuresSection' +import { ValidationWarningsDialog } from './ValidationWarningsDialog' import { CreateAnalyticsBucketSheet } from '@/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet' import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' @@ -119,7 +119,7 @@ export const DestinationForm = ({ const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') const [isFormInteracting, setIsFormInteracting] = useState(false) - const [showDisclaimerDialog, setShowDisclaimerDialog] = useState(false) + const [showValidationWarningsDialog, setShowValidationWarningsDialog] = useState(false) const [publicationPanelVisible, setPublicationPanelVisible] = useState(false) const [newBucketSheetVisible, setNewBucketSheetVisible] = useState(false) const [pendingFormValues, setPendingFormValues] = useState | null>( @@ -339,6 +339,7 @@ export const DestinationForm = ({ const allValidationFailures = [...destinationValidationFailures, ...pipelineValidationFailures] const hasValidationFailures = allValidationFailures.some((f) => f.failure_type === 'critical') + const validationWarnings = allValidationFailures.filter((f) => f.failure_type === 'warning') const isSaving = creatingDestinationPipeline || @@ -377,7 +378,7 @@ export const DestinationForm = ({ // Helper function to validate configuration const validateConfiguration = async (data: z.infer) => { - if (!projectRef || !sourceId) return false + if (!projectRef || !sourceId) return { canContinue: false, warnings: [] } setHasRunValidation(true) @@ -387,6 +388,12 @@ export const DestinationForm = ({ validateDestination({ projectRef, destinationConfig: buildDestinationConfigForValidation({ projectRef, selectedType, data }), + sourceId, + publicationName: data.publicationName, + maxFillMs: data.maxFillMs, + maxTableSyncWorkers: data.maxTableSyncWorkers, + maxCopyConnectionsPerTable: data.maxCopyConnectionsPerTable, + invalidatedSlotBehavior: data.invalidatedSlotBehavior, }), validatePipeline({ projectRef, @@ -413,7 +420,7 @@ export const DestinationForm = ({ rejected?.reason instanceof Error ? rejected.reason.message : 'Please try again.' toast.error(`Failed to validate configuration: ${reason}`) setHasRunValidation(false) - return false + return { canContinue: false, warnings: [] } } // Both requests succeeded, extract validation failures @@ -432,15 +439,16 @@ export const DestinationForm = ({ ] const hasCriticalFailures = allFailures.some((f) => f.failure_type === 'critical') const hasAnyFailures = allFailures.length > 0 + const warnings = allFailures.filter((f) => f.failure_type === 'warning') - // Scroll to validation section if there are any failures + // Scroll to validation section so the user sees failures (both critical and warnings) inline if (hasAnyFailures) { setTimeout(() => { validationSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, 100) } - return !hasCriticalFailures + return { canContinue: !hasCriticalFailures, warnings } } const submitPipeline = async (data: z.infer) => { @@ -529,39 +537,48 @@ export const DestinationForm = ({ const onSubmit = async (data: z.infer) => { if (!editMode) { - // For new pipelines, validate configuration first if not already validated - // OR if user has critical failures and clicks "Validate again" if (!hasRunValidation || isValidating || hasValidationFailures) { - const isValid = await validateConfiguration(data) - if (!isValid) { - // Validation failed with critical errors, show inline and stop + const validationResult = await validateConfiguration(data) + if (!validationResult.canContinue) { + // Critical failures shown inline — stop so user can fix them return } - // Validation passed or only has warnings, continue to disclaimer + if (validationResult.warnings.length > 0) { + // Warnings shown inline — stop so user can review, then re-submit to confirm + return + } + await submitPipeline(data) + return } - // Validation passed or only warnings, proceed to disclaimer - setPendingFormValues(data) - setShowDisclaimerDialog(true) + // Validation already passed; warnings are visible inline — ask user to confirm + if (validationWarnings.length > 0) { + setPendingFormValues(data) + setShowValidationWarningsDialog(true) + return + } + + await submitPipeline(data) return } await submitPipeline(data) } - const handleDisclaimerDialogChange = (open: boolean) => { - setShowDisclaimerDialog(open) + const handleValidationWarningsDialogChange = (open: boolean) => { + setShowValidationWarningsDialog(open) if (!open) { setPendingFormValues(null) + setHasRunValidation(false) } } - const handleDisclaimerConfirm = async () => { + const handleValidationWarningsConfirm = async () => { if (!pendingFormValues) return const values = pendingFormValues setPendingFormValues(null) - setShowDisclaimerDialog(false) + setShowValidationWarningsDialog(false) await submitPipeline(values) } @@ -689,11 +706,12 @@ export const DestinationForm = ({ onOpenChange={setNewBucketSheetVisible} /> - ) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index eee3e2f9b0e..be1c0ac86a6 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -97,7 +97,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane return ( <> - +
{editMode ? 'Edit destination' : 'Create a new destination'} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index 01abedb6c6b..14dfc1b8b0c 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -10,6 +10,7 @@ import { SelectLabel, SelectTrigger, } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { useDestinationInformation } from '../useDestinationInformation' import { @@ -135,14 +136,23 @@ export const DestinationTypeSelection = () => { .find((option) => option.value === destinationType) return ( -
-
-

Type

-

- The destination type cannot be changed after creation. -

-
- + + This destination type is in alpha and may change while we iterate.{' '} + + Leave feedback + +

+ ) + } + > - - {selectedOption?.isAlpha && ( -

- This destination type is in alpha and may change while we iterate.{' '} - - Leave feedback - -

- )} -
+ ) } diff --git a/apps/studio/data/replication/validate-destination-mutation.ts b/apps/studio/data/replication/validate-destination-mutation.ts index 1bd7aeaecd0..848879e6e50 100644 --- a/apps/studio/data/replication/validate-destination-mutation.ts +++ b/apps/studio/data/replication/validate-destination-mutation.ts @@ -8,13 +8,28 @@ import type { ResponseError, UseCustomMutationOptions } from '@/types' type ValidateDestinationParams = { projectRef: string destinationConfig: DestinationConfig + sourceId?: number + publicationName?: string + maxFillMs?: number + maxTableSyncWorkers?: number + maxCopyConnectionsPerTable?: number + invalidatedSlotBehavior?: 'error' | 'recreate' } type ValidateDestinationResponse = components['schemas']['ValidateDestinationResponse'] export type ValidationFailure = ValidateDestinationResponse['validation_failures'][number] async function validateDestination( - { projectRef, destinationConfig }: ValidateDestinationParams, + { + projectRef, + destinationConfig, + sourceId, + publicationName, + maxFillMs, + maxTableSyncWorkers, + maxCopyConnectionsPerTable, + invalidatedSlotBehavior, + }: ValidateDestinationParams, signal?: AbortSignal ): Promise { if (!projectRef) throw new Error('projectRef is required') @@ -108,9 +123,25 @@ async function validateDestination( ) } + const batchConfig = maxFillMs !== undefined ? { max_fill_ms: maxFillMs } : undefined + const pipelineConfig = + publicationName === undefined + ? undefined + : { + publication_name: publicationName, + max_table_sync_workers: maxTableSyncWorkers, + max_copy_connections_per_table: maxCopyConnectionsPerTable, + invalidated_slot_behavior: invalidatedSlotBehavior, + batch: batchConfig, + } + const { data, error } = await post('/platform/replication/{ref}/destinations/validate', { params: { path: { ref: projectRef } }, - body: { config }, + body: { + config, + source_id: sourceId, + pipeline_config: pipelineConfig, + }, signal, }) diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 1c0146a8ea0..71533fadd4a 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -12818,6 +12818,37 @@ export interface components { user: string } } + /** @description Pipeline configuration */ + pipeline_config?: { + /** @description Batch configuration */ + batch?: { + /** + * @description Maximum fill time in milliseconds + * @example 200 + */ + max_fill_ms?: number + } + /** + * @description Behavior when the replication slot is invalidated + * @example error + * @enum {string} + */ + invalidated_slot_behavior?: 'error' | 'recreate' + /** @description Maximum number of copy connections per table */ + max_copy_connections_per_table?: number + /** @description Maximum number of table sync workers */ + max_table_sync_workers?: number + /** + * @description Publication name + * @example pub_orders + */ + publication_name: string + } + /** + * @description Source id + * @example 1 + */ + source_id?: number } ValidateReplicationPipelineBody: { /** @description Pipeline configuration */ From 42ca11f89e7649402a303fd561bddaa9982c4107 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:58:48 +0200 Subject: [PATCH 020/488] fix(support): handle rate-limited support submissions gracefully (#46928) ## Problem When a support ticket submission is rejected by the API's rate limiter, the form surfaced the raw server exception text to the user and reported every rejection as an application error. This produced a steady stream of noisy error reports for what is actually expected, recoverable behavior. The rejections are not random: the submit endpoint allows only a small number of requests in a short window, so a quick second submission (a fast retry or a follow-up ticket moments later) gets rejected. The first submission usually succeeds; it's the immediate follow-up that fails. Surfacing the raw error and logging it made this look worse than it is. Separately, the success screen had only top padding, leaving its actions flush against the bottom edge of the card. ## Fix - Detect the rate-limit response and show a clear, friendly message that tells the user how long to wait before trying again, instead of the raw exception text. - Stop reporting rate-limit rejections as errors to our monitoring. They are expected and recoverable, so they no longer add noise. - Give the success state the same vertical padding as the rest of the form so its actions are not flush against the card edge. ## How to test - Open the support form and simulate a 429 from the submit endpoint. - Expected: a friendly message telling the user when they can retry, and no error reported to monitoring. - Submit a ticket successfully and confirm the success screen has even padding above and below its content. ## Notes This covers the user-facing handling. The rate-limit threshold itself is tuned conservatively on the API and can be revisited separately so that ordinary, legitimate resubmissions are not caught. ## Summary by CodeRabbit * **Bug Fixes** * Improved support form handling for rate-limited (429) submissions by suppressing unnecessary error reporting while still showing the error and returning the form to editing. * Fixed inconsistent support form spacing so padding is consistent regardless of submission outcome. * **Improvements** * Propagated backend error `code` through the support-ticket submission flow so the UI can react more intelligently to failures (including 429 retry-window messaging). * Enhanced retry timing extraction for rate-limited errors by using `Retry-After` with a fallback to rate-limit reset data. --------- Co-authored-by: Claude Sonnet 4.6 --- .../interfaces/Support/SupportForm.state.ts | 4 +++- .../interfaces/Support/SupportFormPage.tsx | 7 ++++--- .../interfaces/Support/SupportFormV2.tsx | 1 + .../interfaces/Support/SupportFormV3.tsx | 1 + .../interfaces/Support/SupportSidebarForm.tsx | 4 +++- .../data/feedback/support-ticket-send.ts | 21 +++++++++++++++++-- apps/studio/data/fetchers.ts | 12 ++++++----- 7 files changed, 38 insertions(+), 12 deletions(-) diff --git a/apps/studio/components/interfaces/Support/SupportForm.state.ts b/apps/studio/components/interfaces/Support/SupportForm.state.ts index c9895322cbc..3caac18083d 100644 --- a/apps/studio/components/interfaces/Support/SupportForm.state.ts +++ b/apps/studio/components/interfaces/Support/SupportForm.state.ts @@ -32,6 +32,7 @@ export type SupportFormState = | { type: 'error' message: string + code?: number } export type SupportFormActions = @@ -45,7 +46,7 @@ export type SupportFormActions = submittedRequest: SubmittedSupportRequest debugSource?: string } - | { type: 'ERROR'; message: string; debugSource?: string } + | { type: 'ERROR'; message: string; code?: number; debugSource?: string } | { type: 'RETURN_TO_EDITING'; debugSource?: string } export function createInitialSupportFormState(): SupportFormState { @@ -89,6 +90,7 @@ export function supportFormReducer( return { type: 'error', message: action.message, + code: action.code, } } console.warn( diff --git a/apps/studio/components/interfaces/Support/SupportFormPage.tsx b/apps/studio/components/interfaces/Support/SupportFormPage.tsx index 0f675c3f165..8181283f5b7 100644 --- a/apps/studio/components/interfaces/Support/SupportFormPage.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormPage.tsx @@ -82,7 +82,9 @@ function SupportFormPageContent() { useStateTransition(state, 'submitting', 'error', (_, curr) => { toast.error(`Failed to submit support ticket: ${curr.message}`) - Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`) + if (curr.code !== 429) { + Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`) + } dispatch({ type: 'RETURN_TO_EDITING' }) }) @@ -258,8 +260,7 @@ function SupportFormBody({ return (
diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx index 5b60aa92a1d..17d6ad0e884 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV2.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -114,6 +114,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo dispatch({ type: 'ERROR', message: error.message, + code: error.code, }) }, }) diff --git a/apps/studio/components/interfaces/Support/SupportFormV3.tsx b/apps/studio/components/interfaces/Support/SupportFormV3.tsx index c86f463cc09..553dd433d88 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV3.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV3.tsx @@ -122,6 +122,7 @@ export const SupportFormV3 = ({ dispatch({ type: 'ERROR', message: error.message, + code: error.code, }) }, }) diff --git a/apps/studio/components/interfaces/Support/SupportSidebarForm.tsx b/apps/studio/components/interfaces/Support/SupportSidebarForm.tsx index 40b0288d6ea..fa910e1db52 100644 --- a/apps/studio/components/interfaces/Support/SupportSidebarForm.tsx +++ b/apps/studio/components/interfaces/Support/SupportSidebarForm.tsx @@ -70,7 +70,9 @@ export function SupportForm({ initialParams }: SupportFormProps) { useStateTransition(state, 'submitting', 'error', (_, curr) => { toast.error(`Failed to submit support ticket: ${curr.message}`) - Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`) + if (curr.code !== 429) { + Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`) + } dispatch({ type: 'RETURN_TO_EDITING' }) }) diff --git a/apps/studio/data/feedback/support-ticket-send.ts b/apps/studio/data/feedback/support-ticket-send.ts index 8e14bf50b40..37957dce243 100644 --- a/apps/studio/data/feedback/support-ticket-send.ts +++ b/apps/studio/data/feedback/support-ticket-send.ts @@ -5,7 +5,8 @@ import { toast } from 'sonner' import type { ExtendedSupportCategories } from '@/components/interfaces/Support/Support.constants' import { handleError, post } from '@/data/fetchers' -import type { ResponseError, UseCustomMutationOptions } from '@/types' +import { ResponseError } from '@/types' +import type { UseCustomMutationOptions } from '@/types' export type sendSupportTicketVariables = { subject: string @@ -25,6 +26,8 @@ export type sendSupportTicketVariables = { dashboardStudioVersion?: string } +const RATE_LIMIT_FALLBACK_SECONDS = 60 + export async function sendSupportTicket({ subject, message, @@ -42,7 +45,7 @@ export async function sendSupportTicket({ dashboardLogs, dashboardStudioVersion, }: sendSupportTicketVariables) { - const { data, error } = await post('/platform/feedback/send', { + const { data, error, response } = await post('/platform/feedback/send', { body: { subject, message, @@ -65,6 +68,20 @@ export async function sendSupportTicket({ }) if (error) { + const httpResponse: unknown = response + if (httpResponse instanceof Response && httpResponse.status === 429) { + const resetHeader = + httpResponse.headers.get('Retry-After') ?? httpResponse.headers.get('X-RateLimit-Reset') + const parsedReset = resetHeader ? parseInt(resetHeader, 10) : NaN + const waitSeconds = Number.isFinite(parsedReset) ? parsedReset : RATE_LIMIT_FALLBACK_SECONDS + throw new ResponseError( + `You have submitted too many support requests. Please try again in ${waitSeconds} second${waitSeconds === 1 ? '' : 's'}.`, + 429, + undefined, + waitSeconds + ) + } + handleError(error, { alwaysCapture: true, sentryContext: { diff --git a/apps/studio/data/fetchers.ts b/apps/studio/data/fetchers.ts index 7e71e9dd686..1ce51f26e80 100644 --- a/apps/studio/data/fetchers.ts +++ b/apps/studio/data/fetchers.ts @@ -140,7 +140,8 @@ client.use( body.requestId = request.headers.get('X-Request-Id') - const retryAfterHeader = response.headers.get('Retry-After') + const retryAfterHeader = + response.headers.get('Retry-After') ?? response.headers.get('X-RateLimit-Reset') body.retryAfter = retryAfterHeader ? parseInt(retryAfterHeader) : undefined const requestUrl = new URL(request.url) @@ -296,10 +297,11 @@ async function handleFetchError(response: unknown): Promise { resJson.msg ?? resJson.error ?? `An error has occurred: ${status ?? 'Unknown error'}` - const retryAfter = - response instanceof Response && response.headers.get('Retry-After') - ? parseInt(response.headers.get('Retry-After')!) - : undefined + const retryAfterHeader = + response instanceof Response + ? (response.headers.get('Retry-After') ?? response.headers.get('X-RateLimit-Reset')) + : null + const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader) : undefined let error = new ResponseError(message, status, undefined, retryAfter) From f70e9c49ff6af86c9d77295c6803f50e14a7fcfb Mon Sep 17 00:00:00 2001 From: mo khan Date: Tue, 16 Jun 2026 09:03:15 -0600 Subject: [PATCH 021/488] docs: Add @xlgmokha to humans.txt (#46995) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? docs update ## What is the current behavior? N/A ## What is the new behavior? N/A ## Additional context Following the instructions from the onboarding guide. ## 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 a2765dcef91..f419811b211 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -199,6 +199,7 @@ Mert Yerekapan Michal Kleczek Michelle Jubrey Miranda Limonczenko +Mo Khan Monica Khoury Mrinal Paliwal Mykhailo Mischa Lieibenson From 79ce3b4ec7c8426cae82c8b5eb7ff98c3eddf1da Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:39:39 +0100 Subject: [PATCH 022/488] fix(studio): hide throughput row on review modal for io2 (#46997) ## 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? When a user selects IO2 on compute upgrade, the `0/MB` isn't quite correct, whilst the costa delta of $0 is correct. We will hide the row as it adds noise. ## Summary by CodeRabbit * **Bug Fixes** * Fixed disk management UI to correctly display the throughput configuration row only when selecting GP3 storage type, ensuring the interface accurately reflects available options for the selected storage type. --- .../DiskManagementReviewAndSubmitDialog.hooks.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts index 9505d1d6cbb..954a1ac0a5d 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts @@ -120,7 +120,6 @@ export function useDiskManagementReviewChanges( // --- Derived predicates --- - const storageTypeBefore = (form.formState.defaultValues?.storageType ?? '') as DiskType const storageTypeAfter = form.getValues('storageType') as DiskType // Show hero whenever any line-item price actually changes, not just compute @@ -132,12 +131,14 @@ export function useDiskManagementReviewChanges( // Show cooldown warning whenever any disk attribute that enforces the 4-hour lock changes const anyDiskAttributeChange = hasIOPSChanges || hasStorageTypeChanges || hasTotalSizeChanges - // Show throughput row whenever either the before or after storage type is GP3 - // (covers GP3→IO2 where the throughput charge drops to zero) + // Throughput is only a user-configurable, separately-billed attribute for GP3. For IO2 it is + // derived from provisioned IOPS (0.256 MiB/s per IOPS) and isn't surfaced as its own value, so + // the form clears it to 0 — rendering a misleading "→ 0 MB/s". Only show the row when the + // resulting storage type is GP3; any GP3→IO2 throughput price delta still lands in the total. const showThroughputRow = !isAwsK8sProject && !isAwsNimbus && - (storageTypeBefore === 'gp3' || storageTypeAfter === 'gp3') && + storageTypeAfter === 'gp3' && (hasThroughputChanges || hasStorageTypeChanges) const hasAnyBreakdownRows = From 92e79e67d9f573157fd7f60869214db113e7d478 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 16 Jun 2026 19:18:17 +0300 Subject: [PATCH 023/488] feat: Add error state to charts (#46991) This PR adds an error state to the usage charts on the Project home page. Screenshot 2026-06-16 at 18 10 20 I also added an error state to the charts in the [design system](https://design-system-git-feat-chart-error-state-supabase.vercel.app/design-system/docs/ui-patterns/charts#chart-states). ## Summary by CodeRabbit * **New Features** * Charts now display error states when data retrieval or processing fails, providing users with clear feedback about issues. * Improved error visibility and handling across analytics and charting components for better transparency. --- .../default/block/chart-composed-states.tsx | 31 ++++++- .../ProjectHome/ProjectUsageSection.tsx | 78 +++++++++------- .../analytics/useFillTimeseriesSorted.ts | 88 ++++++++++--------- packages/ui-patterns/src/Chart/index.tsx | 16 +++- .../ui-patterns/src/LogsBarChart/index.tsx | 9 ++ 5 files changed, 147 insertions(+), 75 deletions(-) diff --git a/apps/design-system/registry/default/block/chart-composed-states.tsx b/apps/design-system/registry/default/block/chart-composed-states.tsx index c4e25d545c2..2d3657cca2c 100644 --- a/apps/design-system/registry/default/block/chart-composed-states.tsx +++ b/apps/design-system/registry/default/block/chart-composed-states.tsx @@ -1,7 +1,7 @@ 'use client' import { BarChart2, ExternalLink } from 'lucide-react' -import { Badge } from 'ui' +import { Badge, CriticalIcon } from 'ui' import { Chart, ChartActions, @@ -44,6 +44,35 @@ export default function ChartComposedStates() { + + + + Response Errors + + + } + title="Some error happened" + description="Error happened why trying to fetch data. Please try again later." + /> + } + emptyState={ + } + title="No data to show" + description="It may take up to 24 hours for data to refresh" + /> + } + loadingState={} + > + My chart here... + + + + diff --git a/apps/studio/components/interfaces/ProjectHome/ProjectUsageSection.tsx b/apps/studio/components/interfaces/ProjectHome/ProjectUsageSection.tsx index 54c02299725..db61af89df2 100644 --- a/apps/studio/components/interfaces/ProjectHome/ProjectUsageSection.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ProjectUsageSection.tsx @@ -3,15 +3,16 @@ import dayjs from 'dayjs' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' -import { Card, CardContent, CardHeader, CardTitle, Loading } from 'ui' +import { Card, CardContent, CardHeader, CardTitle, Loading, WarningIcon } from 'ui' import { Row } from 'ui-patterns' +import { ChartEmptyState } from 'ui-patterns/Chart' import { LogsBarChart } from 'ui-patterns/LogsBarChart' import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder' import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown' import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils' import { UsageApiCounts, useProjectLogStatsQuery } from '@/data/analytics/project-log-stats-query' -import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted' +import { fillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' @@ -71,33 +72,42 @@ export const ProjectUsageSection = () => { }, [selectedInterval]) // Use V1 data fetching - const { data: logStatsData, isPending: isLoading } = useProjectLogStatsQuery({ - projectRef, - interval, - }) + const { + data: filledCharts, + isPending: isLoading, + error, + } = useProjectLogStatsQuery( + { + projectRef, + interval, + }, + { + select: (data) => { + // Calculate date range for gap filling + const startDateLocal = dayjs().subtract( + selectedInterval.startValue, + selectedInterval.startUnit as dayjs.ManipulateType + ) + const endDateLocal = dayjs() - // Calculate date range for gap filling - const startDateLocal = dayjs().subtract( - selectedInterval.startValue, - selectedInterval.startUnit as dayjs.ManipulateType + return fillTimeseriesSorted({ + data: data.result, + timestampKey: 'timestamp', + valueKey: [ + 'total_auth_requests', + 'total_rest_requests', + 'total_storage_requests', + 'total_realtime_requests', + ], + defaultValue: 0, + startDate: startDateLocal.toISOString(), + endDate: endDateLocal.toISOString(), + minPointsToFill: 5, + }) + }, + refetchOnWindowFocus: false, + } ) - const endDateLocal = dayjs() - - // Fill gaps in timeseries data - const { data: filledCharts } = useFillTimeseriesSorted({ - data: logStatsData?.result ?? [], - timestampKey: 'timestamp', - valueKey: [ - 'total_auth_requests', - 'total_rest_requests', - 'total_storage_requests', - 'total_realtime_requests', - ], - defaultValue: 0, - startDate: startDateLocal.toISOString(), - endDate: endDateLocal.toISOString(), - minPointsToFill: 5, - }) const serviceBase: ServiceEntry[] = useMemo( () => [ @@ -147,7 +157,7 @@ export const ProjectUsageSection = () => { // Transform V1 data to LogsBarChart format // Since V1 doesn't have error/warning breakdown, we show everything as "ok" - const transformedData: LogsBarChartDatum[] = (filledCharts || []).map((item) => ({ + const transformedData: LogsBarChartDatum[] = (filledCharts?.data || []).map((item) => ({ timestamp: item.timestamp, error_count: 0, warning_count: 0, @@ -162,10 +172,10 @@ export const ProjectUsageSection = () => { data: transformedData, total, isLoading, - error: null, + error: error || filledCharts?.error || null, } }), - [serviceBase, filledCharts, isLoading] + [serviceBase, filledCharts, isLoading, error] ) const handleBarClick = @@ -239,6 +249,7 @@ export const ProjectUsageSection = () => { { label: 'Requests', }, }} + ErrorState={ + } + title="Failed to load project usage" + description="Check our Status Page or try again later." + /> + } EmptyState={ (data: T[], timestampKey: stri */ export const useFillTimeseriesSorted = ( options: FillTimeseriesOptions +): FillTimeseriesResult => { + return useMemo(() => { + return fillTimeseriesSorted(options) + }, [ + JSON.stringify(options.data), + options.timestampKey, + JSON.stringify(options.valueKey), + options.defaultValue, + options.startDate, + options.endDate, + options.minPointsToFill, + options.interval, + ]) +} + +export const fillTimeseriesSorted = ( + options: FillTimeseriesOptions ): FillTimeseriesResult => { const { data, @@ -78,50 +95,39 @@ export const useFillTimeseriesSorted = ( interval, } = options - return useMemo(() => { - // Early return if no valid timestamp - if (!hasValidTimestamp(data, timestampKey)) { - return { - data, - error: null, - isError: false, - } + // Early return if no valid timestamp + if (!hasValidTimestamp(data, timestampKey)) { + return { + data, + error: null, + isError: false, } + } - try { - const filled = fillTimeseries( - data, - timestampKey, - valueKey, - defaultValue, - startDate, - endDate, - minPointsToFill, - interval - ) as T[] + try { + const filled = fillTimeseries( + data, + timestampKey, + valueKey, + defaultValue, + startDate, + endDate, + minPointsToFill, + interval + ) as T[] - const sorted = sortByTimestamp(filled, timestampKey) + const sorted = sortByTimestamp(filled, timestampKey) - return { - data: sorted, - error: null, - isError: false, - } - } catch (error: unknown) { - return { - data: [], - error: error instanceof Error ? error : new Error(String(error)), - isError: true, - } + return { + data: sorted, + error: null, + isError: false, } - }, [ - JSON.stringify(data), - timestampKey, - JSON.stringify(valueKey), - defaultValue, - startDate, - endDate, - minPointsToFill, - interval, - ]) + } catch (error: unknown) { + return { + data: [], + error: error instanceof Error ? error : new Error(String(error)), + isError: true, + } + } } diff --git a/packages/ui-patterns/src/Chart/index.tsx b/packages/ui-patterns/src/Chart/index.tsx index ca9773a529a..3c0937a6159 100644 --- a/packages/ui-patterns/src/Chart/index.tsx +++ b/packages/ui-patterns/src/Chart/index.tsx @@ -43,11 +43,13 @@ export type ChartConfig = { interface ChartContextValue { isLoading?: boolean isDisabled?: boolean + isErrored?: boolean } const ChartContext = React.createContext({ isLoading: false, isDisabled: false, + isErrored: false, }) export const useChart = () => { @@ -59,15 +61,19 @@ interface ChartProps extends React.HTMLAttributes { children: React.ReactNode isLoading?: boolean isDisabled?: boolean + isErrored?: boolean className?: string } const chartTableClasses = `[&_tr]:border-b [&_tr]:border-border [&_thead_tr]:bg-transparent! [&_thead_th]:py-2! [&_thead_th]:!px-card [&_thead_th]:h-auto [&_tbody_td]:py-2.5 [&_tbody_td]:px-card [&_tbody_td]:text-xs [&_table]:mb-1 [&_table]:border-b [&_table]:border-border` const Chart = React.forwardRef( - ({ children, isLoading = false, isDisabled = false, className, ...props }, ref) => { + ( + { children, isLoading = false, isDisabled = false, isErrored = false, className, ...props }, + ref + ) => { return ( - +
{children}
@@ -306,6 +312,7 @@ interface ChartContentProps extends React.HTMLAttributes { isEmpty?: boolean emptyState?: React.ReactNode loadingState?: React.ReactNode + errorState?: React.ReactNode disabledState?: React.ReactNode disabledActions?: ChartAction[] } @@ -318,19 +325,22 @@ const ChartContent = React.forwardRef( isEmpty = false, emptyState, loadingState, + errorState, disabledState, disabledActions, ...props }, ref ) => { - const { isLoading, isDisabled } = useChart() + const { isLoading, isDisabled, isErrored } = useChart() let content: React.ReactNode if (isDisabled) { content = disabledState } else if (isLoading) { content = loadingState + } else if (isErrored) { + content = errorState } else if (isEmpty) { content = emptyState } else { diff --git a/packages/ui-patterns/src/LogsBarChart/index.tsx b/packages/ui-patterns/src/LogsBarChart/index.tsx index 20316fc438f..c0ad0476d8b 100644 --- a/packages/ui-patterns/src/LogsBarChart/index.tsx +++ b/packages/ui-patterns/src/LogsBarChart/index.tsx @@ -26,8 +26,10 @@ type LogsBarChartDatum = { export const LogsBarChart = ({ data, + error, onBarClick, EmptyState, + ErrorState, DateTimeFormat = 'MMM D, YYYY, hh:mma', isFullHeight = false, chartConfig, @@ -36,8 +38,10 @@ export const LogsBarChart = ({ hideXAxis = false, }: { data: LogsBarChartDatum[] + error?: unknown | null onBarClick?: (datum: LogsBarChartDatum, tooltipData?: CategoricalChartState) => void EmptyState?: ReactNode + ErrorState?: ReactNode DateTimeFormat?: string isFullHeight?: boolean chartConfig?: ChartConfig @@ -47,6 +51,11 @@ export const LogsBarChart = ({ }) => { const [focusDataIndex, setFocusDataIndex] = useState(null) + if (error) { + if (ErrorState) return ErrorState + return null + } + if (data.length === 0) { if (EmptyState) return EmptyState return null From 6a6b7c90858b6b868c6d05bd3263a957357fa84a Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:59:37 -0600 Subject: [PATCH 024/488] fix(docs): remove redundant icons (#47002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? UI fix. Resolves DOCS-1051. ## What is the current behavior? Two unnecessary icons on docs homepage add more confusion than help. ## What is the new behavior? Removed and tightened surrounding spacing + rhythm. | Before | After | | --- | --- | | Supabase
Docs-5A137B99-AC36-4A59-85D9-B581307FFE6D | Supabase
Docs-1220594B-29B1-48B0-B976-39CCFB009857 | | Supabase
Docs-23849B84-EE41-45EB-A585-BEE71AB8FFB2 | Supabase
Docs-B5A3344F-32DD-4A49-B7DF-E5B10C8CDD13 | ## Summary by CodeRabbit * **Style** * Simplified documentation page header and section visuals by removing icon-accented wrappers and using cleaner text-based layouts. * **Accessibility / Content** * Updated section heading hierarchy for improved structure (e.g., “Additional resources” and “Self-Hosting”) by promoting headings while keeping existing context and styling. * Refreshed the “Getting Started” section to be text-focused without the prior play icon treatment. --- apps/docs/app/page.tsx | 20 ++++++++------------ apps/docs/components/HomePageCover.tsx | 17 +++++++---------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 186f3548a79..31eba97aef4 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -1,11 +1,10 @@ import { isFeatureEnabled } from 'common' import { type Metadata, type ResolvingMetadata } from 'next' import Link from 'next/link' -import { cn, IconBackground } from 'ui' +import { cn } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' import { TextLink } from 'ui-patterns/TextLink' -import MenuIconPicker from '@/components/Navigation/NavigationMenu/MenuIconPicker' import { MIGRATION_PAGES } from '@/components/Navigation/NavigationMenu/NavigationMenu.constants' import { GlassPanelWithIconPicker } from '@/features/ui/GlassPanelWithIconPicker' import { IconPanelWithIconPicker } from '@/features/ui/IconPanelWithIconPicker' @@ -324,10 +323,10 @@ const HomePage = () => ( )}
-
-

+
+

Additional resources -

+

    @@ -351,17 +350,14 @@ const HomePage = () => (
{isFeatureEnabled('docs:full_getting_started') && (
-
+
- - - -

+

Self-Hosting -

+
-

+

Get started with self-hosting Supabase.

{ >
-
- - -

Getting Started

+
+

Getting Started

+

+ Set up and connect a database in just a few minutes. +

-

- Set up and connect a database in just a few minutes. -

From c54168842b36835e50299ef8fd3700371b239e6a Mon Sep 17 00:00:00 2001 From: Matt Rossman <22670878+mattrossman@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:20:06 -0400 Subject: [PATCH 025/488] fix(assistant): prevent parallel approval-required tool calls (#47008) Addresses the issue of parallel tool approvals freezing Assistant and causing UX ambiguity around what happens if only part of multiple dependent invocations is approved. Two layers at which this is addressed: 1. Tightens prompt to clarify approval tools must be issued one per step, not in parallel. 2. In case something slips past the prompt, this also auto-denies all but the first `approval-required` tool call when the model issues multiple in the same step, so the model is forced to reissue them sequentially. **Demo** The following chats demo me explicitly asking Assistant to run those approval-gated tools in parallel, and the Assistant correctly invokes them sequentially instead. | Parallel `execute_sql` request | Parallel `deploy_edge_function` request | |--------|--------| | CleanShot 2026-06-16 at 16 20
23@2x | CleanShot 2026-06-16 at 16 22
59@2x | As shown in [this trace](https://www.braintrust.dev/app/supabase.io/p/Assistant/trace?object_type=project_logs&object_id=5a8d02e5-b3b6-40cc-ba76-ecee286478f4&r=a3a37857-95df-4a95-a5a8-818ea305b2a5&s=a3a37857-95df-4a95-a5a8-818ea305b2a5), parallel tool calls are still allowed for context gathering tools that don't require approval: CleanShot 2026-06-16 at 16 25 42@2x Closes AI-803 ## Summary by CodeRabbit * **Improvements** * Approval-required operations are now processed sequentially rather than in parallel within the AI Assistant. * **Tests** * Added comprehensive test coverage for the parallel approval prevention logic. --- .../ui/AIAssistantPanel/AIAssistant.tsx | 13 ++++ apps/studio/lib/ai/message-utils.test.ts | 78 ++++++++++++++++++- apps/studio/lib/ai/message-utils.ts | 16 +++- apps/studio/lib/ai/prompts.ts | 2 +- 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 2184f94a8e0..6b8848e44d7 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -39,6 +39,7 @@ import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { getParallelApprovalIdsToReject } from '@/lib/ai/message-utils' import { DEFAULT_ASSISTANT_BASE_MODEL_ID, defaultAssistantModelId, @@ -366,6 +367,18 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { } }, [isResubmitting, chatStatus, error]) + useEffect(() => { + // Approval-required tools can't run in parallel. Auto-deny extras so the model reissues them sequentially. + for (const id of getParallelApprovalIdsToReject(chatMessages)) { + addToolApprovalResponse?.({ + id, + approved: false, + reason: + 'Only one approval-required tool call is allowed per turn. Please reissue this tool call after the current one completes.', + }) + } + }, [chatMessages, addToolApprovalResponse]) + useEffect(() => { setValue(snap.initialInput || '') if (inputRef.current && snap.initialInput) { diff --git a/apps/studio/lib/ai/message-utils.test.ts b/apps/studio/lib/ai/message-utils.test.ts index 34502f43d2a..0125996ddc7 100644 --- a/apps/studio/lib/ai/message-utils.test.ts +++ b/apps/studio/lib/ai/message-utils.test.ts @@ -1,7 +1,81 @@ -import type { UIMessage } from 'ai' +import type { DynamicToolUIPart, UIMessage } from 'ai' import { describe, expect, it } from 'vitest' -import { prepareMessagesForAPI } from './message-utils' +import { getParallelApprovalIdsToReject, prepareMessagesForAPI } from './message-utils' + +const makeApprovalPart = (id: string): DynamicToolUIPart => ({ + type: 'dynamic-tool', + toolName: 'test_tool', + toolCallId: id, + state: 'approval-requested', + input: {}, + approval: { id }, +}) + +const makeResultPart = (id: string): DynamicToolUIPart => ({ + type: 'dynamic-tool', + toolName: 'test_tool', + toolCallId: id, + state: 'output-available', + input: {}, + output: {}, +}) + +describe('getParallelApprovalIdsToReject', () => { + it('returns [] for empty messages', () => { + expect(getParallelApprovalIdsToReject([])).toEqual([]) + }) + + it('returns [] when there are no assistant messages', () => { + const messages: UIMessage[] = [{ id: '1', role: 'user', parts: [] }] + expect(getParallelApprovalIdsToReject(messages)).toEqual([]) + }) + + it('returns [] when last assistant message has no pending approvals', () => { + const messages: UIMessage[] = [{ id: '1', role: 'assistant', parts: [makeResultPart('r1')] }] + expect(getParallelApprovalIdsToReject(messages)).toEqual([]) + }) + + it('returns [] when there is only one pending approval', () => { + const messages: UIMessage[] = [{ id: '1', role: 'assistant', parts: [makeApprovalPart('a1')] }] + expect(getParallelApprovalIdsToReject(messages)).toEqual([]) + }) + + it('returns all but the first id when there are multiple pending approvals', () => { + const messages: UIMessage[] = [ + { + id: '1', + role: 'assistant', + parts: [makeApprovalPart('a1'), makeApprovalPart('a2'), makeApprovalPart('a3')], + }, + ] + expect(getParallelApprovalIdsToReject(messages)).toEqual(['a2', 'a3']) + }) + + it('only inspects the last assistant message', () => { + const messages: UIMessage[] = [ + { + id: '1', + role: 'assistant', + parts: [makeApprovalPart('old1'), makeApprovalPart('old2')], + }, + { id: '2', role: 'user', parts: [] }, + { id: '3', role: 'assistant', parts: [makeApprovalPart('new1')] }, + ] + expect(getParallelApprovalIdsToReject(messages)).toEqual([]) + }) + + it('ignores non-approval tool parts', () => { + const messages: UIMessage[] = [ + { + id: '1', + role: 'assistant', + parts: [makeResultPart('r1'), makeApprovalPart('a1'), makeApprovalPart('a2')], + }, + ] + expect(getParallelApprovalIdsToReject(messages)).toEqual(['a2']) + }) +}) describe('prepareMessagesForAPI', () => { it('should limit messages to last 7 entries', () => { diff --git a/apps/studio/lib/ai/message-utils.ts b/apps/studio/lib/ai/message-utils.ts index 2b0cbd62924..d04e25b5c1d 100644 --- a/apps/studio/lib/ai/message-utils.ts +++ b/apps/studio/lib/ai/message-utils.ts @@ -1,4 +1,4 @@ -import type { UIMessage } from 'ai' +import { isToolUIPart, type UIMessage } from 'ai' /** * Prepares messages for API transmission by cleaning and limiting history @@ -23,3 +23,17 @@ export function prepareMessagesForAPI(messages: UIMessage[]): UIMessage[] { return cleanedMessages } + +/** + * Returns approval IDs to auto-deny when the model issues multiple approval-required + * tool calls in the same turn — all but the first, so the model reissues them sequentially. + */ +export function getParallelApprovalIdsToReject(messages: UIMessage[]): string[] { + const lastMessage = messages.findLast((m) => m.role === 'assistant') + if (!lastMessage) return [] + + const pendingIds = (lastMessage.parts ?? []).flatMap((part) => + isToolUIPart(part) && part.state === 'approval-requested' ? [part.approval.id] : [] + ) + return pendingIds.slice(1) +} diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts index 09f71fb4f5d..81c6f0435f6 100644 --- a/apps/studio/lib/ai/prompts.ts +++ b/apps/studio/lib/ai/prompts.ts @@ -733,7 +733,7 @@ export const CHAT_PROMPT = ` - On execution error, explain succinctly and attempt to correct if possible, validating each outcome briefly (1–2 lines) after execution. - If a user skips execution, acknowledge and suggest alternatives. - Use markdown code blocks (\`\`\`sql\`\`\`) for illustrative SQL only if requested by the user or when providing non-executable examples. -- Execute multiple queries separately via \`execute_sql\` and briefly validate outcomes. +- Never call \`execute_sql\` or \`deploy_edge_function\` in parallel within the same step. Each requires user approval, so issue one per step and wait for its result before calling the next. - After execution, summarize outcomes concisely without duplicating results, as the client will present these. ## Edge Functions - Deploy Edge Functions by calling \`deploy_edge_function\` directly with \`name\` and \`code\`; the client handles confirmation and result presentation. From 608040b8cb3f553d142d43d3f91eb6de89624559 Mon Sep 17 00:00:00 2001 From: Miranda Limonczenko Date: Tue, 16 Jun 2026 14:45:55 -0700 Subject: [PATCH 026/488] chore(docs) Resolve 'simple' style warnings where applicable (#46966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributes to DOCS-1052 ## 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? Resolves MDX linting errors related to "simple" where it applies. There was a couple cases that did not apply. For example, a product with "Simple" in the name. These changes are made in context, either by removing or using a more descriptive synonym like "minimal" or "basic". ## Tophatting 1. Read each of the diffs. 2. See that the text still makes sense in context. For extra due diligence, you can run `pnpm lint:mdx` locally and see the 'simple' errors that remain and whether they are worth addressing. ## Summary by CodeRabbit ## Summary by CodeRabbit * **Documentation** * Updated many guide, tutorial, and troubleshooting pages with clearer “basic”/“minimal” wording across setup steps, local testing instructions, security cautions, and RLS guidance. * Refined headings, example descriptions, and inline comments for consistency (including deployment, MCP, metrics API, and search/function phrasing). * Improved readability with small snippet formatting tweaks (whitespace plus import/comment ordering) and added a self-hosting debugging note for Envoy admin endpoints via a short-lived `curl` container. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla Co-authored-by: Nik Richers --- apps/docs/content/_partials/database_setup.mdx | 2 +- apps/docs/content/_partials/kotlin_project_setup.mdx | 2 +- apps/docs/content/_partials/metrics_access.mdx | 2 +- apps/docs/content/guides/ai-tools/byo-mcp.mdx | 6 ++++-- apps/docs/content/guides/ai.mdx | 8 ++++---- apps/docs/content/guides/ai/engineering-for-scale.mdx | 4 ++-- .../guides/ai/examples/building-chatgpt-plugins.mdx | 2 +- apps/docs/content/guides/ai/keyword-search.mdx | 2 +- apps/docs/content/guides/ai/langchain.mdx | 2 +- apps/docs/content/guides/ai/rag-with-permissions.mdx | 2 +- apps/docs/content/guides/auth/auth-mfa.mdx | 2 +- apps/docs/content/guides/auth/auth-mfa/totp.mdx | 2 +- apps/docs/content/guides/auth/auth-smtp.mdx | 2 +- apps/docs/content/guides/auth/server-side.mdx | 2 +- .../connecting-to-postgres/serverless-drivers.mdx | 2 +- apps/docs/content/guides/database/extensions/http.mdx | 4 ++-- .../docs/content/guides/database/extensions/hypopg.mdx | 2 +- .../content/guides/database/extensions/postgis.mdx | 4 ++-- .../content/guides/database/extensions/timescaledb.mdx | 2 +- apps/docs/content/guides/database/functions.mdx | 6 +++--- apps/docs/content/guides/database/partitions.mdx | 2 +- apps/docs/content/guides/database/postgres/indexes.mdx | 2 +- apps/docs/content/guides/database/tables.mdx | 2 +- apps/docs/content/guides/database/testing.mdx | 2 +- apps/docs/content/guides/deployment.mdx | 6 +++--- .../guides/deployment/shared-responsibility-model.mdx | 2 +- apps/docs/content/guides/functions.mdx | 2 +- apps/docs/content/guides/functions/architecture.mdx | 2 +- .../content/guides/functions/examples/discord-bot.mdx | 5 +++-- .../guides/functions/examples/mcp-server-mcp-lite.mdx | 2 +- .../content/guides/functions/recursive-functions.mdx | 4 ++-- apps/docs/content/guides/functions/routing.mdx | 2 +- apps/docs/content/guides/functions/unit-test.mdx | 2 +- apps/docs/content/guides/functions/wasm.mdx | 2 +- apps/docs/content/guides/getting-started/api-keys.mdx | 2 +- .../content/guides/getting-started/architecture.mdx | 4 ++-- apps/docs/content/guides/getting-started/features.mdx | 2 +- .../guides/getting-started/quickstarts/laravel.mdx | 2 +- .../guides/getting-started/quickstarts/sveltekit.mdx | 2 +- .../getting-started/tutorials/with-redwoodjs.mdx | 9 ++++----- .../guides/integrations/partner-integration-guide.mdx | 4 ++-- .../content/guides/integrations/vercel-marketplace.mdx | 2 +- .../local-development/cli/testing-and-linting.mdx | 2 +- .../guides/local-development/testing/overview.mdx | 4 ++-- .../local-development/testing/pgtap-extended.mdx | 4 ++-- apps/docs/content/guides/platform.mdx | 2 +- apps/docs/content/guides/realtime/broadcast.mdx | 6 +++--- apps/docs/content/guides/realtime/protocol.mdx | 2 +- .../realtime/subscribing-to-database-changes.mdx | 5 ++++- apps/docs/content/guides/resources/glossary.mdx | 2 +- apps/docs/content/guides/security.mdx | 2 +- .../content/guides/self-hosting/self-hosted-envoy.mdx | 2 +- .../guides/storage/uploads/standard-uploads.mdx | 4 +++- apps/docs/content/guides/telemetry/logs.mdx | 2 +- ...xceeds-btree-version-4-maximum-for-index-LMmoeU.mdx | 2 +- ...rom-supabase-auth-helpers-to-ssr-package-5NRunM.mdx | 2 +- apps/docs/content/troubleshooting/http-api-issues.mdx | 2 +- ...esponse-or-zgotmplz-in-magic-link-emails-433665.mdx | 2 +- .../troubleshooting/prisma-error-management-Cm5P_o.mdx | 2 +- .../troubleshooting/realtime-heartbeat-messages.mdx | 10 +++++----- .../troubleshooting/soft-deletes-with-supabase-js.mdx | 2 +- 61 files changed, 95 insertions(+), 88 deletions(-) diff --git a/apps/docs/content/_partials/database_setup.mdx b/apps/docs/content/_partials/database_setup.mdx index 92112438ddc..09cac33af3c 100644 --- a/apps/docs/content/_partials/database_setup.mdx +++ b/apps/docs/content/_partials/database_setup.mdx @@ -1,6 +1,6 @@ ## Project setup -Let's create a new Postgres database. This is as simple as starting a new Project in Supabase: +To create a new Postgres database, start a new Project in Supabase: 1. [Create a new project](https://database.new/) in the Supabase dashboard. 1. Enter your project details. Remember to store your password somewhere safe. diff --git a/apps/docs/content/_partials/kotlin_project_setup.mdx b/apps/docs/content/_partials/kotlin_project_setup.mdx index 1f32521eefd..91b95ad86ec 100644 --- a/apps/docs/content/_partials/kotlin_project_setup.mdx +++ b/apps/docs/content/_partials/kotlin_project_setup.mdx @@ -1,6 +1,6 @@ ## Project setup -Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database. +Before building, you must set up your Database and API with a new Project in Supabase and then create a "schema" inside the database. ### Create a project diff --git a/apps/docs/content/_partials/metrics_access.mdx b/apps/docs/content/_partials/metrics_access.mdx index 853dcaac14a..2aa32d58aee 100644 --- a/apps/docs/content/_partials/metrics_access.mdx +++ b/apps/docs/content/_partials/metrics_access.mdx @@ -25,7 +25,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground - **Username**: `service_role` - **Password**: a **Secret API key** (`sb_secret_...`). You can create/copy it in [**Project Settings → API Keys**](/dashboard/project/_/settings/api-keys). For more context, see [Understanding API keys](/docs/guides/getting-started/api-keys). - Testing locally is as simple as running `curl` with your Secret API key: + To test locally, run `curl` with your Secret API key: ```bash curl /customer/v1/privileged/metrics \ diff --git a/apps/docs/content/guides/ai-tools/byo-mcp.mdx b/apps/docs/content/guides/ai-tools/byo-mcp.mdx index 88e708a8c4b..2df8f11ed23 100644 --- a/apps/docs/content/guides/ai-tools/byo-mcp.mdx +++ b/apps/docs/content/guides/ai-tools/byo-mcp.mdx @@ -75,7 +75,7 @@ const server = new McpServer({ version: '0.1.0', }) -// Register a simple addition tool +// Register an addition tool server.registerTool( 'add', { @@ -218,7 +218,9 @@ After this step, you have a fully deployed MCP server accessible from anywhere. You can find ready-to-use MCP server implementations here: -- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Basic unauthenticated example +{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} + +- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Unauthenticated example ## Resources diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index 4fb8c15b66c..5226d8849aa 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -109,8 +109,8 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
- OpenAI is an AI research and deployment company. Supabase provides a simple way to use - OpenAI in your applications. + OpenAI is an AI research and deployment company. Supabase provides a way to use OpenAI in + your applications.
@@ -125,8 +125,8 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
- Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way - to use Hugging Face's models in your applications. + Hugging Face is an open-source provider of NLP technologies. Supabase provides a way to use + Hugging Face's models in your applications.
diff --git a/apps/docs/content/guides/ai/engineering-for-scale.mdx b/apps/docs/content/guides/ai/engineering-for-scale.mdx index 55e8990507d..a9c74970bd7 100644 --- a/apps/docs/content/guides/ai/engineering-for-scale.mdx +++ b/apps/docs/content/guides/ai/engineering-for-scale.mdx @@ -8,9 +8,9 @@ sidebar_label: 'Engineering for Scale' Content sources for vectors can be extremely large. As you grow you should run your Vector workloads across several secondary databases (sometimes called "pods"), which allows each collection to scale independently. -## Simple workloads +## Small workloads [#simple-workloads] -For small workloads, it's typical to store your data in a single database. +For small workloads, you can typically store your data in a single database. If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views): diff --git a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx index b6dfa676b9d..8d6bc5eb3ee 100644 --- a/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx +++ b/apps/docs/content/guides/ai/examples/building-chatgpt-plugins.mdx @@ -20,7 +20,7 @@ It allows ChatGPT to dynamically pull relevant information into conversations fr ## Example: Chat with Postgres docs -Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation. Although ChatGPT already knows about the Postgres documentation because it is publicly available, this is a simple example which demonstrates how to work with PDF files. +Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation. Although ChatGPT already knows about the Postgres documentation because it is publicly available, this is a basic example which demonstrates how to work with PDF files. This plugin requires several steps: diff --git a/apps/docs/content/guides/ai/keyword-search.mdx b/apps/docs/content/guides/ai/keyword-search.mdx index 9ee03877831..643fa08025f 100644 --- a/apps/docs/content/guides/ai/keyword-search.mdx +++ b/apps/docs/content/guides/ai/keyword-search.mdx @@ -8,7 +8,7 @@ sidebar_label: 'Keyword search' Keyword search involves locating documents or records that contain specific words or phrases, primarily based on the exact match between the search terms and the text within the data. It differs from [semantic search](/docs/guides/ai/semantic-search), which interprets the meaning behind the query to provide results that are contextually related, even if the exact words aren't present in the text. Semantic search considers synonyms, intent, and natural language nuances to provide a more nuanced approach to information retrieval. -In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond simple keyword matching to address linguistic nuances, making it effective for applications that require precise text queries. +In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond keyword matching to address linguistic nuances, making it effective for applications that require precise text queries. ## When and why to use keyword search diff --git a/apps/docs/content/guides/ai/langchain.mdx b/apps/docs/content/guides/ai/langchain.mdx index b089cb4ab5e..09cf1c570fa 100644 --- a/apps/docs/content/guides/ai/langchain.mdx +++ b/apps/docs/content/guides/ai/langchain.mdx @@ -108,7 +108,7 @@ export const run = async () => { } ``` -### Simple metadata filtering +### Basic metadata filtering [#simple-metadata-filtering] Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information. diff --git a/apps/docs/content/guides/ai/rag-with-permissions.mdx b/apps/docs/content/guides/ai/rag-with-permissions.mdx index 08c8ca7b021..4b2df595dce 100644 --- a/apps/docs/content/guides/ai/rag-with-permissions.mdx +++ b/apps/docs/content/guides/ai/rag-with-permissions.mdx @@ -231,7 +231,7 @@ order by document_sections.embedding <#> embedding; {/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} -You might be tempted to discard RLS completely and simply filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future. +You might be tempted to discard RLS completely and filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future. diff --git a/apps/docs/content/guides/auth/auth-mfa.mdx b/apps/docs/content/guides/auth/auth-mfa.mdx index 1cb3576ef18..542884c5aa8 100644 --- a/apps/docs/content/guides/auth/auth-mfa.mdx +++ b/apps/docs/content/guides/auth/auth-mfa.mdx @@ -127,7 +127,7 @@ Unenrolling a factor will downgrade the assurance level from `aal2` to `aal1` on ```tsx /** - * UnenrollMFA shows a simple table with the list of factors together with a button to unenroll. + * UnenrollMFA shows a table with the list of factors together with a button to unenroll. * When a user types in the factorId of the factor that they wish to unenroll and clicks unenroll * the corresponding factor will be unenrolled. */ diff --git a/apps/docs/content/guides/auth/auth-mfa/totp.mdx b/apps/docs/content/guides/auth/auth-mfa/totp.mdx index c645e15f681..c1869fbb097 100644 --- a/apps/docs/content/guides/auth/auth-mfa/totp.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/totp.mdx @@ -75,7 +75,7 @@ Below is an example that creates a new `EnrollMFA` component that illustrates th ```tsx /** - * EnrollMFA shows a simple enrollment dialog. When shown on screen it calls + * EnrollMFA shows an enrollment dialog. When shown on screen it calls * the `enroll` API. Each time a user clicks the Enable button it calls the * `challenge` and `verify` APIs to check if the code provided by the user is * valid. diff --git a/apps/docs/content/guides/auth/auth-smtp.mdx b/apps/docs/content/guides/auth/auth-smtp.mdx index 2e4d9fefd33..b43e261bda7 100644 --- a/apps/docs/content/guides/auth/auth-smtp.mdx +++ b/apps/docs/content/guides/auth/auth-smtp.mdx @@ -13,7 +13,7 @@ If you're using Supabase Auth with the following configuration: You will need to set up a custom SMTP server to handle the delivery of messages to your users. -To get you started and let you explore and set up email message templates for your application, Supabase provides a simple SMTP server for all projects. This server imposes a few important restrictions and is not meant for production use. +To get you started and let you explore and set up email message templates for your application, Supabase provides an SMTP server for all projects. This server imposes a few important restrictions and is not meant for production use. **Send messages only to pre-authorized addresses.** diff --git a/apps/docs/content/guides/auth/server-side.mdx b/apps/docs/content/guides/auth/server-side.mdx index 782db4bc501..81ae770cd4a 100644 --- a/apps/docs/content/guides/auth/server-side.mdx +++ b/apps/docs/content/guides/auth/server-side.mdx @@ -15,7 +15,7 @@ Make sure to use the PKCE flow instructions where those differ from the implicit ## `@supabase/ssr` -We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/ssr) package to make setting up the Supabase client as simple as possible. This package is currently in beta. Adoption is recommended but be aware that the API is still unstable and may have breaking changes in the future. +We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/ssr) package for setting up the Supabase client. This package is currently in beta. Adoption is recommended but be aware that the API is still unstable and may have breaking changes in the future. ## Framework quickstarts diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx index 24591674340..87a78a6f926 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx @@ -35,7 +35,7 @@ Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integra passHref > - Simple Next.js template that uses Supabase as the database and Kysely as the query builder. + Basic Next.js template that uses Supabase as the database and Kysely as the query builder.
diff --git a/apps/docs/content/guides/database/extensions/http.mdx b/apps/docs/content/guides/database/extensions/http.mdx index ed4052e62e0..f987f35493c 100644 --- a/apps/docs/content/guides/database/extensions/http.mdx +++ b/apps/docs/content/guides/database/extensions/http.mdx @@ -88,7 +88,7 @@ A successful call to a web URL from the `http` extension returns a record with t ## Examples -### Simple `GET` example +### Basic `GET` example [#simple-get-example] ```sql select @@ -97,7 +97,7 @@ from extensions.http_get('https://jsonplaceholder.typicode.com/todos/1'); ``` -### Simple `POST` example +### Basic `POST` example [#simple-post-example] ```sql select diff --git a/apps/docs/content/guides/database/extensions/hypopg.mdx b/apps/docs/content/guides/database/extensions/hypopg.mdx index 3a3875ef139..6bea2618227 100644 --- a/apps/docs/content/guides/database/extensions/hypopg.mdx +++ b/apps/docs/content/guides/database/extensions/hypopg.mdx @@ -45,7 +45,7 @@ It's good practice to create the extension within a separate schema (like `exten ### Speeding up a query -Given the following table and a simple query to select from the table by `id`: +Given the following table and a basic query to select from the table by `id`: {/* prettier-ignore */} ```sql diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 599f65be591..61b14e05a4e 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -9,7 +9,7 @@ tocVideo: 'agFsGDJxjwA' ## Overview -While you may be able to store simple lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability. +While you may be able to store lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability. The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [LineString](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html). @@ -47,7 +47,7 @@ drop extension if exists postgis; ## Examples -Now that we are ready to get started with PostGIS, let’s create a table and see how we can utilize PostGIS for some typical use cases. Let’s imagine we are creating a simple restaurant-searching app. +Now that we are ready to get started with PostGIS, let’s create a table and see how we can use PostGIS for some typical use cases. Imagine we are creating a basic restaurant-searching app. Let’s create our table. Each row represents a restaurant with its location stored in `location` column as a `Point` type. diff --git a/apps/docs/content/guides/database/extensions/timescaledb.mdx b/apps/docs/content/guides/database/extensions/timescaledb.mdx index a3aa0303646..abea2efc839 100644 --- a/apps/docs/content/guides/database/extensions/timescaledb.mdx +++ b/apps/docs/content/guides/database/extensions/timescaledb.mdx @@ -60,7 +60,7 @@ It's good practice to create the extension within a separate schema (like `exten ## Usage -To demonstrate how `timescaledb` works, let's consider a simple example where we have a table that stores temperature data from different sensors. We will create a table named "temperatures" and store data for two sensors. +To demonstrate how `timescaledb` works, consider a basic example where we have a table that stores temperature data from different sensors. We will create a table named "temperatures" and store data for two sensors. First we create a hypertable, which is a virtual table that is partitioned into chunks based on time intervals. The hypertable acts as a proxy for the actual table and makes it easy to query and manage time-series data. diff --git a/apps/docs/content/guides/database/functions.mdx b/apps/docs/content/guides/database/functions.mdx index abea38ae77d..5dac1acaa3e 100644 --- a/apps/docs/content/guides/database/functions.mdx +++ b/apps/docs/content/guides/database/functions.mdx @@ -30,7 +30,7 @@ and run the SQL queries yourself. 3. Enter the SQL to create or replace your Database function. 4. Click "Run" or cmd+enter (ctrl+enter). -## Simple functions +## Basic functions [#simple-functions] Let's create a basic Database Function which returns a string "hello world". @@ -53,7 +53,7 @@ At it's most basic a function has the following parts: 2. `returns text`: The type of data that the function returns. If it returns nothing, you can `returns void`. 3. `language sql`: The language used inside the function body. This can also be a procedural language: `plpgsql`, `plpython`, etc. 4. `as $$`: The function wrapper. Anything enclosed inside the `$$` symbols will be part of the function body. -5. `select 'hello world';`: A simple function body. The final `select` statement inside a function body will be returned if there are no statements following it. +5. `select 'hello world';`: A basic function body. The final `select` statement inside a function body will be returned if there are no statements following it. 6. `$$;`: The closing symbols of the function wrapper. @@ -290,7 +290,7 @@ data = supabase.rpc('get_planets').eq('id', 1).execute() ## Passing parameters -Let's create a Function to insert a new planet into the `planets` table and return the new ID. Note that this time we're using the `plpgsql` language. +Create a function to insert a new planet into the `planets` table and return the new ID. Note that this time we're using the `plpgsql` language. ```sql create or replace function add_planet(name text) diff --git a/apps/docs/content/guides/database/partitions.mdx b/apps/docs/content/guides/database/partitions.mdx index 9019ea55e34..73e9f3a2903 100644 --- a/apps/docs/content/guides/database/partitions.mdx +++ b/apps/docs/content/guides/database/partitions.mdx @@ -105,7 +105,7 @@ There is no real threshold to determine when you should use partitions. Partitio ## Examples -Here are simple examples for each of the partitioning types in Postgres. +Here are basic examples for each of the partitioning types in Postgres. ### Range partitioning diff --git a/apps/docs/content/guides/database/postgres/indexes.mdx b/apps/docs/content/guides/database/postgres/indexes.mdx index d0b36b77bf0..a4a33e4ab6a 100644 --- a/apps/docs/content/guides/database/postgres/indexes.mdx +++ b/apps/docs/content/guides/database/postgres/indexes.mdx @@ -53,7 +53,7 @@ Seq Scan on persons (cost=0.00..22.75 rows=x width=y) Filter: (age = 32) ``` -To add a simple B-Tree index you can run: +To add a basic B-Tree index you can run: ```sql create index idx_persons_age on persons (age); diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 4f284bc1dd5..79c3a43e0fa 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -92,7 +92,7 @@ You must define the "data type" when you create a column. ### Data types -Every column is a predefined type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can even design your own (or use extensions) if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. We only support a subset of these in the Table Editor in an effort to keep the experience simple for people with less experience with databases. +Every column is a predefined type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can even design your own (or use extensions) if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. We only support a subset of these in the Table Editor in an effort to keep the experience focused for people with less experience with databases.
Show/Hide default data types diff --git a/apps/docs/content/guides/database/testing.mdx b/apps/docs/content/guides/database/testing.mdx index 8054ecc2b7f..a4e93b38eeb 100644 --- a/apps/docs/content/guides/database/testing.mdx +++ b/apps/docs/content/guides/database/testing.mdx @@ -34,7 +34,7 @@ touch ./supabase/tests/database/hello_world.test.sql All `sql` files use [pgTAP](/docs/guides/database/extensions/pgtap) as the test runner. -Let's write a simple test to check that our `auth.users` table has an ID column. Open `hello_world.test.sql` and add the following code: +Write a test to check that our `auth.users` table has an ID column. Open `hello_world.test.sql` and add the following code: ```sql begin; diff --git a/apps/docs/content/guides/deployment.mdx b/apps/docs/content/guides/deployment.mdx index cea5ad0ff4a..5fc4a55cf77 100644 --- a/apps/docs/content/guides/deployment.mdx +++ b/apps/docs/content/guides/deployment.mdx @@ -4,11 +4,11 @@ title: Deployment & Branching Deploying your app makes it live and accessible to users. Most apps have at least two environments: a production environment for users and one or more staging or preview environments for development. -Supabase provides flexible options for both simple and advanced deployment workflows. +Supabase provides flexible options for deployment workflows of various complexity. ## Recommended workflow -The simplest way to deploy your Supabase project is with GitHub (recommended, but not required): +You can deploy your Supabase project with GitHub: 1. Develop locally using the [Supabase CLI](/docs/guides/local-development) 2. Push changes to your GitHub repository @@ -41,7 +41,7 @@ You can automate deployments using: ### Is GitHub required? -No. GitHub integration is recommended for the simplest setup, but you can deploy using the [Supabase CLI](/docs/guides/local-development) in your own CI/CD pipeline without connecting a GitHub repository. +No. GitHub integration is recommended, but you can deploy using the [Supabase CLI](/docs/guides/local-development) in your own CI/CD pipeline without connecting a GitHub repository. ### Do you need a paid plan? diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index adc1ec62ebd..41c934fd008 100644 --- a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx +++ b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx @@ -69,7 +69,7 @@ If your application architecture relies on such integrations, you should monitor ## You choose your level of comfort with Postgres -Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine. +Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you want to treat Postgres like a basic table-store, that’s perfectly fine. ## You are in control of your database diff --git a/apps/docs/content/guides/functions.mdx b/apps/docs/content/guides/functions.mdx index 71839126532..87d1b7f42e4 100644 --- a/apps/docs/content/guides/functions.mdx +++ b/apps/docs/content/guides/functions.mdx @@ -25,7 +25,7 @@ Edge Functions are server-side TypeScript functions, distributed globally at the ## Quick technical notes -- **Runtime:** Supabase Edge Runtime (Deno compatible runtime with TypeScript first). Functions are simple `.ts` files that export a handler. +- **Runtime:** Supabase Edge Runtime (Deno compatible runtime with TypeScript first). Functions are `.ts` files that export a handler. - **Local dev parity:** Use Supabase CLI for a local runtime similar to production for faster iteration (`supabase functions serve` command). - **Global deployment:** Deploy your Edge Functions via Supabase Dashboard, CLI or MCP. - **Cold starts & concurrency:** cold starts are possible — design for short-lived, idempotent operations. Heavy long-running jobs should be moved to [background workers](/docs/guides/functions/background-tasks). diff --git a/apps/docs/content/guides/functions/architecture.mdx b/apps/docs/content/guides/functions/architecture.mdx index 5bb5eb07f08..219233e37f6 100644 --- a/apps/docs/content/guides/functions/architecture.mdx +++ b/apps/docs/content/guides/functions/architecture.mdx @@ -24,7 +24,7 @@ To illustrate how edge functions operate, consider a photo-sharing app where use - **Why Edge Functions?**: - They handle compute-intensive tasks without burdening the client device or the database. - Execution happens server-side but at the edge, ensuring speed and scalability. - - Developers define the function in a simple JavaScript file within the Supabase functions directory. + - Developers define the function in a JavaScript file within the Supabase functions directory. This example highlights edge functions as lightweight, on-demand code snippets that integrate seamlessly with Supabase services like Storage and Auth. diff --git a/apps/docs/content/guides/functions/examples/discord-bot.mdx b/apps/docs/content/guides/functions/examples/discord-bot.mdx index aa00105e557..085e109716b 100644 --- a/apps/docs/content/guides/functions/examples/discord-bot.mdx +++ b/apps/docs/content/guides/functions/examples/discord-bot.mdx @@ -43,12 +43,13 @@ This will register a Slash Command named `hello` that accepts a parameter named ```ts index.ts // Sift is a small routing library that abstracts away details like starting a -// listener on a port, and provides a simple function (serve) that has an API +// listener on a port, and provides a function (serve) that has an API // to invoke a function for a specific path. -import { json, serve, validateRequest } from 'https://deno.land/x/sift@0.6.0/mod.ts' + // TweetNaCl is a cryptography library that we use to verify requests // from Discord. import nacl from 'https://cdn.skypack.dev/tweetnacl@v1.0.3?dts' +import { json, serve, validateRequest } from 'https://deno.land/x/sift@0.6.0/mod.ts' enum DiscordCommandType { Ping = 1, diff --git a/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx b/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx index f81fe9b86f6..fc22b0f717a 100644 --- a/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx +++ b/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx @@ -21,7 +21,7 @@ This combination offers several advantages: - **Direct database access**: Connect directly to your Supabase Postgres - **Minimal footprint**: mcp-lite has zero runtime dependencies - **Full type safety**: TypeScript support in Deno -- **Simple deployment**: One command to production +- **Basic deployment**: One command to production ## Prerequisites diff --git a/apps/docs/content/guides/functions/recursive-functions.mdx b/apps/docs/content/guides/functions/recursive-functions.mdx index ad28b050c1e..d661924efd1 100644 --- a/apps/docs/content/guides/functions/recursive-functions.mdx +++ b/apps/docs/content/guides/functions/recursive-functions.mdx @@ -256,7 +256,7 @@ export async function save(data: any) { ```typescript // supabase/functions/process-data/index.ts -import { validate, transform, save } from '../_shared/transform.ts' +import { save, transform, validate } from '../_shared/transform.ts' Deno.serve(async (req) => { const data = await req.json() @@ -284,7 +284,7 @@ async function processWithDelay(items: any[]) { | Pattern | Budget consumption | Recommendation | | ------------------------------- | ------------------ | ----------------- | -| Simple chain (A to B to C) | Low | Generally safe | +| Basic chain (A to B to C) | Low | Generally safe | | Fan-out (A to B, C, D, E) | Moderate | Limit concurrency | | Deep recursion (A to A to A...) | High | Set max depth | | Unbounded loops | Very high | Avoid, use queues | diff --git a/apps/docs/content/guides/functions/routing.mdx b/apps/docs/content/guides/functions/routing.mdx index 3e112ee2d20..b6c084fe134 100644 --- a/apps/docs/content/guides/functions/routing.mdx +++ b/apps/docs/content/guides/functions/routing.mdx @@ -27,7 +27,7 @@ To combine multiple endpoints into a single Edge Function, you can use web appli ## Basic routing example -Here's a simple hello world example using some popular web frameworks: +Here's a basic hello world example using some popular web frameworks: { if (!supabaseUrl) throw new Error('supabaseUrl is required.') if (!supabaseKey) throw new Error('supabaseKey is required.') - // Test a simple query to the database + // Test a query to the database const { data: table_data, error: table_error } = await client .from('my_table') .select('*') diff --git a/apps/docs/content/guides/functions/wasm.mdx b/apps/docs/content/guides/functions/wasm.mdx index c8377bc6e10..fe7e5f61ece 100644 --- a/apps/docs/content/guides/functions/wasm.mdx +++ b/apps/docs/content/guides/functions/wasm.mdx @@ -20,7 +20,7 @@ For example, libraries like [magick-wasm](/docs/guides/functions/examples/image- ### Writing a Wasm module -You can use different languages and SDKs to write Wasm modules. For this tutorial, we will write a simple Wasm module in Rust that adds two numbers. +You can use different languages and SDKs to write Wasm modules. For this tutorial, we will write a basic Wasm module in Rust that adds two numbers. diff --git a/apps/docs/content/guides/getting-started/api-keys.mdx b/apps/docs/content/guides/getting-started/api-keys.mdx index 696c9578bbe..3ca3976f52a 100644 --- a/apps/docs/content/guides/getting-started/api-keys.mdx +++ b/apps/docs/content/guides/getting-started/api-keys.mdx @@ -89,7 +89,7 @@ Never expose your secret keys publicly. Your data is at risk. **Do not:** - Never use in a browser, even on `localhost`. - Do not pass in URLs or query params, as these are often logged. - Be careful passing them in request headers without prior log sanitization. -- Take extra care logging even potentially **invalid API keys**. Simple typos might reveal the real key in the future. +- Take extra care logging even potentially **invalid API keys**. Typos might reveal the real key in the future. - Reveal, copy, use or manipulate on hardware devices without full disk encryption and which you do not directly own or control (such as public computers, friend's laptop, etc.) Ensure you handle them with care and using [secure coding practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/stable-en/). diff --git a/apps/docs/content/guides/getting-started/architecture.mdx b/apps/docs/content/guides/getting-started/architecture.mdx index e75cd4ab38b..0e8767f5c3a 100644 --- a/apps/docs/content/guides/getting-started/architecture.mdx +++ b/apps/docs/content/guides/getting-started/architecture.mdx @@ -4,7 +4,7 @@ description: 'Supabase design and architecture' tocVideo: 'T-qAtAKjqwc' --- -Supabase is open source. We choose open source tools which are scalable and make them simple to use. +Supabase is open source. We choose open source tools which are scalable and make them approachable. Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way: our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch. @@ -13,7 +13,7 @@ Most notably, we use Postgres rather than a NoSQL store. This choice was deliber ## Choose your comfort level -Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine. +Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you want to treat Postgres like a basic table-store, that’s perfectly fine. ## Architecture diff --git a/apps/docs/content/guides/getting-started/features.mdx b/apps/docs/content/guides/getting-started/features.mdx index 8dcb43769e0..d27f2ca8a6d 100644 --- a/apps/docs/content/guides/getting-started/features.mdx +++ b/apps/docs/content/guides/getting-started/features.mdx @@ -128,7 +128,7 @@ Helpers for implementing user authentication in popular server-side languages an ### File storage -Supabase Storage makes it simple to store and serve files. [Docs](/docs/guides/storage). +Supabase Storage helps you store and serve files. [Docs](/docs/guides/storage). ### Content Delivery Network diff --git a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx index 381caecf2e5..6e7744279bf 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx @@ -29,7 +29,7 @@ hideToc: true - Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a simple implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication). + Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a basic implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication). diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index 16fff4527d8..30be6126edc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -112,7 +112,7 @@ hideToc: true - Use `load` method to fetch the data server-side and display the query results as a simple list. + Use `load` method to fetch the data server-side and display the query results as a list. Create `+page.server.js` file in the `src/routes` directory with the following code. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx index 18f4af41521..36482c6e839 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx @@ -28,7 +28,7 @@ Important: When this guide refers to "API," that means the Supabase API and when The **`api side`** is an implementation of a GraphQL API. The business logic is organized into "services" that represent their own internal API and can be called both from external GraphQL requests and other internal services. -The **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React "Page" components (and automatically code-split your app on each route). +The **`web side`** is built with React. Redwood's router lets you map URL paths to React "Page" components (and automatically code-split your app on each route). Pages may contain a "Layout" component to wrap content. They also contain "Cells" and regular React components. Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data. @@ -249,8 +249,8 @@ Now, update the `Auth.js` component to contain: <$CodeTabs> ```jsx name=/web/src/components/Auth/Auth.js -import { useState } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useState } from 'react' const Auth = () => { const { logIn } = useAuth() @@ -326,8 +326,8 @@ And then update the file to contain: <$CodeTabs> ```jsx name=web/src/components/Account/Account.js -import { useState, useEffect } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useEffect, useState } from 'react' const Account = () => { const { client: supabase, currentUser, logOut } = useAuth() @@ -464,7 +464,6 @@ With all the components in place, update your `HomePage` page to use them: ```jsx name=web/src/pages/HomePage/HomePage.js import { useAuth } from '@redwoodjs/auth' import { MetaTags } from '@redwoodjs/web' - import Account from 'src/components/Account' import Auth from 'src/components/Auth' @@ -511,8 +510,8 @@ Now, update your Avatar component to contain the following widget: <$CodeTabs> ```jsx name=web/src/components/Avatar/Avatar.js -import { useEffect, useState } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useEffect, useState } from 'react' const Avatar = ({ url, size, onUpload }) => { const { client: supabase } = useAuth() diff --git a/apps/docs/content/guides/integrations/partner-integration-guide.mdx b/apps/docs/content/guides/integrations/partner-integration-guide.mdx index 74ff3e28eeb..3cf4bd56726 100644 --- a/apps/docs/content/guides/integrations/partner-integration-guide.mdx +++ b/apps/docs/content/guides/integrations/partner-integration-guide.mdx @@ -9,12 +9,12 @@ This guide assumes you have already followed [the Build a Supabase Integration g When a Supabase user clicks the **Install Integration** button in the Dashboard, they are redirected to your system to begin the OAuth flow. You can implement this redirect in one of two ways: -- **[Simple redirect](#method-1-simple-redirect)**: Easier to build, but your system cannot verify that the incoming user was sent by Supabase. +- **[Redirect](#method-1-redirect)**: Easier to build, but your system cannot verify that the incoming user was sent by Supabase. - **[Signed redirect](#method-2-signed-redirect)**: More work to build, but cryptographically verifies that the redirect originated from Supabase. **Recommended for production integrations.** Pick the method that fits your security requirements, then follow the matching section below. -## Method 1: Simple redirect +## Method 1: Redirect In this method, you implement a single `GET` endpoint. Supabase redirects the user to this endpoint when they click **Install Integration**, and your endpoint kicks off the OAuth flow. diff --git a/apps/docs/content/guides/integrations/vercel-marketplace.mdx b/apps/docs/content/guides/integrations/vercel-marketplace.mdx index 8471fd1cdd4..35c554d1695 100644 --- a/apps/docs/content/guides/integrations/vercel-marketplace.mdx +++ b/apps/docs/content/guides/integrations/vercel-marketplace.mdx @@ -58,7 +58,7 @@ These variables ensure your applications can connect securely to the database an ## Studio support -Accessing Supabase Studio is simple through the Vercel dashboard. You can open Supabase Studio from either the Integration installation page or the Vercel Storage page. +Open Supabase Studio from the Vercel dashboard. You can access it from either the Integration installation page or the Vercel Storage page. Depending on your entry point, you'll either land on the Supabase dashboard homepage or be redirected to the corresponding Supabase Project. Supabase Studio provides tools such as: diff --git a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx index 017961afb7c..2fc83fe1ee9 100644 --- a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx +++ b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx @@ -44,7 +44,7 @@ By default, Mailpit is available at [localhost:54324](http://localhost:54324) wh ### Going into production -The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). +The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, configure your own email provider by enabling SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). ## Linting your database diff --git a/apps/docs/content/guides/local-development/testing/overview.mdx b/apps/docs/content/guides/local-development/testing/overview.mdx index 8c4624a92a6..9b0e3ac7e09 100644 --- a/apps/docs/content/guides/local-development/testing/overview.mdx +++ b/apps/docs/content/guides/local-development/testing/overview.mdx @@ -17,12 +17,12 @@ Testing is a critical part of database development, especially when working with - Functions and procedures - Data integrity -This example demonstrates setting up and testing RLS policies for a simple todo application: +This example demonstrates setting up and testing RLS policies for a basic todo application: 1. Create a test table with RLS enabled: ```sql - -- Create a simple todos table + -- Create a todos table create table todos ( id uuid primary key default gen_random_uuid(), task text not null, diff --git a/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx b/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx index b7bbefecbea..6401ab340f4 100644 --- a/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx +++ b/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx @@ -83,7 +83,7 @@ The test helpers package provides several advantages over writing raw pgTAP test ## Schema-wide Row Level Security testing -When working with Row Level Security, it's crucial to ensure that RLS is enabled on all tables that need it. Create a simple test to verify RLS is enabled across an entire schema: +When working with Row Level Security, it's crucial to ensure that RLS is enabled on all tables that need it. Create a basic test to verify RLS is enabled across an entire schema: ```sql begin; @@ -112,7 +112,7 @@ This setup file should contain: 1. All shared extensions and dependencies 2. Common test utilities -3. A simple always green test to verify the setup +3. A basic always-green test to verify the setup Here's an example setup file: diff --git a/apps/docs/content/guides/platform.mdx b/apps/docs/content/guides/platform.mdx index f4c58fe84a0..3953d8a1862 100644 --- a/apps/docs/content/guides/platform.mdx +++ b/apps/docs/content/guides/platform.mdx @@ -5,7 +5,7 @@ description: 'Getting started with the Supabase Platform.' sidebar_label: 'Overview' --- -Supabase is a hosted platform which makes it very simple to get started without needing to manage any infrastructure. +Supabase is a hosted platform that allows you to get started without needing to manage any infrastructure. Visit [supabase.com/dashboard](/dashboard) and sign in to start creating projects. diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 6714da11eca..41dc5b96aea 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -153,7 +153,7 @@ Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically f // Join a room/topic. Can be anything except for 'realtime'. const myChannel = supabase.channel('test-channel') - // Simple function to log any messages we receive + // Function to log any messages we receive function messageReceived(payload) { console.log(payload) } @@ -176,7 +176,7 @@ Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically f ```dart final myChannel = supabase.channel('test-channel'); - // Simple function to log any messages we receive + // Log any messages we receive void messageReceived(payload) { print(payload); } @@ -241,7 +241,7 @@ Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically f # Join a room/topic. Can be anything except for 'realtime'. my_channel = supabase.channel('test-channel') - # Simple function to log any messages we receive + # Function to log any messages we receive def message_received(payload): print(f"Broadcast received: {payload}") diff --git a/apps/docs/content/guides/realtime/protocol.mdx b/apps/docs/content/guides/realtime/protocol.mdx index 5170749dfc2..05c524e642b 100644 --- a/apps/docs/content/guides/realtime/protocol.mdx +++ b/apps/docs/content/guides/realtime/protocol.mdx @@ -35,7 +35,7 @@ Messages can be serialized in different formats. The Realtime protocol supports ## 1.0.0 -Version 1.0.0 is extremely simple. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames. +Version 1.0.0 is minimal. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames. Messages contain the following fields: diff --git a/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx b/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx index 9984e94d6c2..a32e5609e26 100644 --- a/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx +++ b/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx @@ -73,6 +73,7 @@ Finally, on the client side, listen to the topic `topic:` to receive ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -90,7 +91,7 @@ const changes = supabase ## Using Postgres Changes -Postgres Changes are simple to use, but have some [limitations](/docs/guides/realtime/postgres-changes#limitations) as your application scales. We recommend using Broadcast for most use cases. +Postgres Changes require minimal setup, but have some [limitations](/docs/guides/realtime/postgres-changes#limitations) as your application scales. We recommend using Broadcast for most use cases.