From b4922199cd22f91da3da8d561d3c84484137f3ec Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Tue, 16 Sep 2025 13:03:13 +0200 Subject: [PATCH 01/13] unblock cms draft mode (#38409) * remove port from cms start script * address cors * 30s revalidation on blog index * fix types * remove duplicate cache strategy * disable graphql * fix cms build * fix ProductDropdown crash * fix env var turbo --- apps/cms/next.config.mjs | 4 - apps/cms/package.json | 2 +- .../(payload)/api/graphql-playground/route.ts | 7 - .../src/app/(payload)/api/graphql/route.ts | 8 -- apps/cms/src/payload.config.ts | 2 +- apps/www/app/api-v2/cms-posts/route.ts | 135 ++++++++++++------ apps/www/app/api-v2/cms/revalidate/route.ts | 39 ++--- apps/www/app/api-v2/ticket-og/route.tsx | 2 +- apps/www/app/blog/[slug]/page.tsx | 2 +- .../app/blog/categories/[category]/page.tsx | 1 + apps/www/app/blog/tags/[tag]/page.tsx | 1 + apps/www/components/Nav/ProductDropdown.tsx | 2 - apps/www/lib/constants.ts | 4 +- apps/www/lib/get-cms-posts.tsx | 8 +- apps/www/pages/launch-week/6/index.tsx | 2 +- .../6/types6.d.ts => types/launch-week-6.ts} | 0 turbo.json | 1 + 17 files changed, 125 insertions(+), 95 deletions(-) delete mode 100644 apps/cms/src/app/(payload)/api/graphql-playground/route.ts delete mode 100644 apps/cms/src/app/(payload)/api/graphql/route.ts rename apps/www/{pages/launch-week/6/types6.d.ts => types/launch-week-6.ts} (100%) diff --git a/apps/cms/next.config.mjs b/apps/cms/next.config.mjs index 3317297c82500..092c737bfb8b1 100644 --- a/apps/cms/next.config.mjs +++ b/apps/cms/next.config.mjs @@ -47,10 +47,6 @@ const nextConfig = { // We are already running linting via GH action, this will skip linting during production build on Vercel ignoreDuringBuilds: true, }, - experimental: { - // Ensure compatibility with Turbopack and Sharp - serverComponentsExternalPackages: ['sharp'], - }, // Configure Sharp as an external package for server-side rendering serverExternalPackages: ['sharp'], } diff --git a/apps/cms/package.json b/apps/cms/package.json index 13e5841a04e9f..b4d5728204fef 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -15,7 +15,7 @@ "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", "migrate": "cross-env NODE_OPTIONS=--no-deprecation tsx scripts/migrate.ts", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", - "start": "cross-env NODE_OPTIONS=--no-deprecation next start --port 3030", + "start": "cross-env NODE_OPTIONS=--no-deprecation next start", "vercel-build": "pnpm migrate && pnpm build", "typecheck_IGNORED": "tsc --noEmit" }, diff --git a/apps/cms/src/app/(payload)/api/graphql-playground/route.ts b/apps/cms/src/app/(payload)/api/graphql-playground/route.ts deleted file mode 100644 index 17d2954ca2d25..0000000000000 --- a/apps/cms/src/app/(payload)/api/graphql-playground/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import config from '@payload-config' -import '@payloadcms/next/css' -import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' - -export const GET = GRAPHQL_PLAYGROUND_GET(config) diff --git a/apps/cms/src/app/(payload)/api/graphql/route.ts b/apps/cms/src/app/(payload)/api/graphql/route.ts deleted file mode 100644 index 2069ff86b0aaf..0000000000000 --- a/apps/cms/src/app/(payload)/api/graphql/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import config from '@payload-config' -import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' - -export const POST = GRAPHQL_POST(config) - -export const OPTIONS = REST_OPTIONS(config) diff --git a/apps/cms/src/payload.config.ts b/apps/cms/src/payload.config.ts index 8858ce58a5e3d..8e7a31c40d250 100644 --- a/apps/cms/src/payload.config.ts +++ b/apps/cms/src/payload.config.ts @@ -102,7 +102,7 @@ export default buildConfig({ // Global configuration for better performance globals: [], graphQL: { - disable: process.env.NODE_ENV !== 'development', // Disable GraphQL in production for better performance + disable: true, }, // Reduce payload init overhead telemetry: false, diff --git a/apps/www/app/api-v2/cms-posts/route.ts b/apps/www/app/api-v2/cms-posts/route.ts index 2243cab04db47..04f64cf768073 100644 --- a/apps/www/app/api-v2/cms-posts/route.ts +++ b/apps/www/app/api-v2/cms-posts/route.ts @@ -6,6 +6,12 @@ import { generateReadingTime } from '~/lib/helpers' // Lightweight runtime for better performance export const runtime = 'edge' +// CORS headers for cross-origin requests +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +} + // Lightweight TOC generation for edge runtime type TocItem = { content: string; slug: string; lvl: number } @@ -115,6 +121,14 @@ function convertRichTextToMarkdown(content: any): string { .join('\n\n') } +// Handle preflight requests +export async function OPTIONS() { + return new Response(null, { + status: 200, + headers: corsHeaders, + }) +} + export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) @@ -212,12 +226,15 @@ export async function GET(request: NextRequest) { _status: latestVersion._status, } - return NextResponse.json({ - success: true, - post: processedPost, - mode, - isDraft: shouldFetchDraft, - }) + return NextResponse.json( + { + success: true, + post: processedPost, + mode, + isDraft: shouldFetchDraft, + }, + { headers: corsHeaders } + ) } } } @@ -289,12 +306,15 @@ export async function GET(request: NextRequest) { _status: post._status, } - return NextResponse.json({ - success: true, - post: processedPost, - mode, - isDraft: shouldFetchDraft, - }) + return NextResponse.json( + { + success: true, + post: processedPost, + mode, + isDraft: shouldFetchDraft, + }, + { headers: corsHeaders } + ) } } @@ -364,12 +384,15 @@ export async function GET(request: NextRequest) { _status: post._status, } - return NextResponse.json({ - success: true, - post: processedPost, - mode, - isDraft: shouldFetchDraft, - }) + return NextResponse.json( + { + success: true, + post: processedPost, + mode, + isDraft: shouldFetchDraft, + }, + { headers: corsHeaders } + ) } } } @@ -452,16 +475,21 @@ export async function GET(request: NextRequest) { toc_depth: post.toc_depth || 3, } - return NextResponse.json({ - success: true, - post: processedPost, - mode, - source: 'versions-api', - }) + return NextResponse.json( + { + success: true, + post: processedPost, + mode, + source: 'versions-api', + }, + { headers: corsHeaders } + ) } } } else { - console.log('[cms-posts] Versions API failed, response:', await versionsResponse.text()) + if (process.env.NODE_ENV !== 'production') { + console.log('[cms-posts] Versions API failed, response:', await versionsResponse.text()) + } } // Strategy 2: If versions API didn't work, try finding the parent post first, then get its latest published version @@ -556,12 +584,15 @@ export async function GET(request: NextRequest) { richContent: mode === 'full' ? post.content : undefined, } - return NextResponse.json({ - success: true, - post: processedPost, - mode, - source: 'versions-by-parent-api', - }) + return NextResponse.json( + { + success: true, + post: processedPost, + mode, + source: 'versions-by-parent-api', + }, + { headers: corsHeaders } + ) } } } @@ -590,6 +621,12 @@ export async function GET(request: NextRequest) { // For individual post requests, don't cache to ensure fresh data cache: slug ? 'no-store' : 'default', next: slug ? undefined : { revalidate: 300 }, + // Add SSL configuration for production + ...(process.env.NODE_ENV === 'production' && { + // Allow self-signed certificates in development, but use proper SSL in production + // This helps with Vercel's internal networking + agent: false, + }), }) if (!response.ok) { @@ -600,7 +637,7 @@ export async function GET(request: NextRequest) { error: 'Failed to fetch posts from CMS', status: response.status, }, - { status: response.status } + { status: response.status, headers: corsHeaders } ) } @@ -613,7 +650,7 @@ export async function GET(request: NextRequest) { error: 'CMS returned non-JSON response', contentType, }, - { status: 502 } + { status: 502, headers: corsHeaders } ) } @@ -695,20 +732,26 @@ export async function GET(request: NextRequest) { // For single post requests, return the post directly if (slug && posts.length > 0) { - return NextResponse.json({ - success: true, - post: posts[0], - mode, - }) + return NextResponse.json( + { + success: true, + post: posts[0], + mode, + }, + { headers: corsHeaders } + ) } - return NextResponse.json({ - success: true, - posts, - total: posts.length, - mode, - cached: true, - }) + return NextResponse.json( + { + success: true, + posts, + total: posts.length, + mode, + cached: true, + }, + { headers: corsHeaders } + ) } catch (error) { console.error('[cms-posts] Error:', error) return NextResponse.json( @@ -717,7 +760,7 @@ export async function GET(request: NextRequest) { error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error', }, - { status: 500 } + { status: 500, headers: corsHeaders } ) } } diff --git a/apps/www/app/api-v2/cms/revalidate/route.ts b/apps/www/app/api-v2/cms/revalidate/route.ts index b743c4870dde7..3ff73f9e8d8af 100644 --- a/apps/www/app/api-v2/cms/revalidate/route.ts +++ b/apps/www/app/api-v2/cms/revalidate/route.ts @@ -1,27 +1,32 @@ -import type { NextApiRequest, NextApiResponse } from 'next' +import { revalidatePath } from 'next/cache' +import { NextRequest, NextResponse } from 'next/server' -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - // Check for secret to confirm this is a valid request - if (req.query.secret !== process.env.CMS_PREVIEW_SECRET) { - return res.status(401).json({ message: 'Invalid token' }) - } +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { secret, path } = body - const { path } = req.query + // Check for secret to confirm this is a valid request + if (secret !== process.env.CMS_PREVIEW_SECRET) { + return NextResponse.json({ message: 'Invalid token' }, { status: 401 }) + } - if (!path) { - return res.status(400).json({ message: 'Missing path parameter' }) - } + if (!path) { + return NextResponse.json({ message: 'Missing path parameter' }, { status: 400 }) + } - try { // This will revalidate the specific page - await res.revalidate(String(path)) + revalidatePath(path) - return res.json({ revalidated: true, path }) + return NextResponse.json({ revalidated: true, path }) } catch (error) { console.error('[Revalidate API] Error during revalidation:', error) - return res.status(500).json({ - message: 'Error revalidating', - error: error instanceof Error ? error.message : 'Unknown error', - }) + return NextResponse.json( + { + message: 'Error revalidating', + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) } } diff --git a/apps/www/app/api-v2/ticket-og/route.tsx b/apps/www/app/api-v2/ticket-og/route.tsx index 6c9ba7807a3db..46b216f66f448 100644 --- a/apps/www/app/api-v2/ticket-og/route.tsx +++ b/apps/www/app/api-v2/ticket-og/route.tsx @@ -24,7 +24,7 @@ const FONT_URLS = { const LW_TABLE = 'tickets' const LW_MATERIALIZED_VIEW = 'tickets_view' -export async function GET(req: Request, res: Response) { +export async function GET(req: Request) { const url = new URL(req.url) // Just here to silence snyk false positives diff --git a/apps/www/app/blog/[slug]/page.tsx b/apps/www/app/blog/[slug]/page.tsx index 7f142ae402609..dd3ce29f66fab 100644 --- a/apps/www/app/blog/[slug]/page.tsx +++ b/apps/www/app/blog/[slug]/page.tsx @@ -28,7 +28,7 @@ async function getCMSPostFromAPI( const fetchOptions = isDraft ? { // For draft mode: always fresh data, no caching - cache: 'no-store' as const, + // cache: 'no-store' as const, next: { revalidate: 0 }, } : { diff --git a/apps/www/app/blog/categories/[category]/page.tsx b/apps/www/app/blog/categories/[category]/page.tsx index edafaa817a448..b0c8f9ab3be2f 100644 --- a/apps/www/app/blog/categories/[category]/page.tsx +++ b/apps/www/app/blog/categories/[category]/page.tsx @@ -15,6 +15,7 @@ export async function generateStaticParams() { return categories.map((category: string) => ({ category })) } +export const revalidate = 30 export const dynamic = 'force-static' export async function generateMetadata({ params }: { params: Params }): Promise { diff --git a/apps/www/app/blog/tags/[tag]/page.tsx b/apps/www/app/blog/tags/[tag]/page.tsx index ca270ca9d630f..edda447f4a136 100644 --- a/apps/www/app/blog/tags/[tag]/page.tsx +++ b/apps/www/app/blog/tags/[tag]/page.tsx @@ -15,6 +15,7 @@ export async function generateStaticParams() { return tags.map((tag: string) => ({ tag })) } +export const revalidate = 30 export const dynamic = 'force-static' export async function generateMetadata({ params }: { params: Params }): Promise { diff --git a/apps/www/components/Nav/ProductDropdown.tsx b/apps/www/components/Nav/ProductDropdown.tsx index 85207759b10ff..83e0e586a7633 100644 --- a/apps/www/components/Nav/ProductDropdown.tsx +++ b/apps/www/components/Nav/ProductDropdown.tsx @@ -14,10 +14,8 @@ import ComparisonsData from 'data/Comparisons' import CustomersData from 'data/CustomerStories' import MainProductsData from 'data/MainProducts' import ProductModulesData from 'data/ProductModules' -import { useRouter } from 'next/router' export const ProductDropdown = () => { - const { basePath } = useRouter() const isTablet = useBreakpoint(1279) return ( diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index b77ad67d3eaba..61a17e59bbbdb 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -34,8 +34,8 @@ export const SITE_ORIGIN = export const CMS_SITE_ORIGIN = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' - ? // In production, require env or fall back to localhost to avoid hitting supabase.com - 'http://localhost:3030' + ? // In production, use the actual CMS domain + process.env.CMS_SITE_ORIGIN || 'https://cms.supabase.com' : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL && typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string' ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL.replace('zone-www-dot-com-git-', 'cms-git-')}` diff --git a/apps/www/lib/get-cms-posts.tsx b/apps/www/lib/get-cms-posts.tsx index 6507b71dd2760..01dd212eeb88d 100644 --- a/apps/www/lib/get-cms-posts.tsx +++ b/apps/www/lib/get-cms-posts.tsx @@ -288,7 +288,7 @@ export async function getCMSPostBySlug(slug: string, preview = false) { 'Content-Type': 'application/json', ...(CMS_API_KEY && { Authorization: `Bearer ${CMS_API_KEY}` }), }, - cache: 'no-store', + // cache: 'no-store', next: { revalidate: 0 }, }) @@ -300,7 +300,7 @@ export async function getCMSPostBySlug(slug: string, preview = false) { 'Content-Type': 'application/json', ...(CMS_API_KEY && { Authorization: `Bearer ${CMS_API_KEY}` }), }, - cache: 'no-store', + // cache: 'no-store', next: { revalidate: 0 }, }) } @@ -490,8 +490,8 @@ export async function getAllCMSPosts({ 'Content-Type': 'application/json', ...(CMS_API_KEY && { Authorization: `Bearer ${CMS_API_KEY}` }), }, - cache: 'no-store', // Ensure we always get fresh data - next: { revalidate: 0 }, // Disable caching for this fetch + // cache: 'no-store', // Ensure we always get fresh data + next: { revalidate: 30 }, // Disable caching for this fetch } ) diff --git a/apps/www/pages/launch-week/6/index.tsx b/apps/www/pages/launch-week/6/index.tsx index f7a2e94ea4adb..22989cff44da9 100644 --- a/apps/www/pages/launch-week/6/index.tsx +++ b/apps/www/pages/launch-week/6/index.tsx @@ -15,7 +15,7 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { Accordion, Badge } from 'ui' import { SITE_ORIGIN } from '~/lib/constants' -import { WeekDayProps } from './types6' +import type { WeekDayProps } from '~/types/launch-week-6' import styles from './styles/launchWeek6.module.css' import styleUtils from './styles/utils6.module.css' diff --git a/apps/www/pages/launch-week/6/types6.d.ts b/apps/www/types/launch-week-6.ts similarity index 100% rename from apps/www/pages/launch-week/6/types6.d.ts rename to apps/www/types/launch-week-6.ts diff --git a/turbo.json b/turbo.json index b1b8c12160fe2..64a448b08abda 100644 --- a/turbo.json +++ b/turbo.json @@ -116,6 +116,7 @@ "ANALYZE", "CMS_API_KEY", "CMS_PREVIEW_SECRET", + "CMS_SITE_ORIGIN", "NEXT_PUBLIC_MISC_USE_URL", "NEXT_PUBLIC_MISC_USE_ANON_KEY", "NEXT_PUBLIC_STUDIO_URL", From 83122eb4c620d1c56428152193b6fed272dd911a Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:58:55 +0200 Subject: [PATCH 02/13] fix query performance links (#38738) * fix links * fix links * rm nextenv * fix formatting changes --- .../troubleshooting/all-about-supabase-egress-a_Sg_e.mdx | 2 +- .../canceling-statement-due-to-statement-timeout-581wFv.mdx | 2 +- ...ficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx | 2 +- .../components/layouts/ReportsLayout/Reports.Commands.tsx | 2 +- apps/studio/next.config.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx index a5b412360017d..00bd84d90a899 100644 --- a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx +++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx @@ -20,7 +20,7 @@ You can read about Unified egress, included quota, and how to check the egress u While pointing out the exact cause for egress may not be straightforward, there are various steps you can take to determine the source of these issues: - Picking the "Top Paths" from the [log explorer](https://app.supabase.com/project/_/logs/explorer/templates) will help you identify heavily queried paths -- By finding the most requested queries from Query performance report: https://app.supabase.com/project/_/reports/query-performance +- By finding the most requested queries from Query performance report: https://app.supabase.com/project/_/advisors/query-performance - Supavisor Egress is independent of client. There is no direct relation between a single query and Supavisor egress, it is harder to debug and identify. But you can make use of frequent queries from the link in above step that also displays average number of rows which will help to identify queries with a large number of rows returned. While this does not display Supavisor queries specifically, it will give an overview of queries with lots of rows that can help. - For Storage Egress, all outgoing traffic for storage-related requests to download/view your Storage items are considered as Storage egress. We have a "Storage Egress Requests" template in logs explorer that you can use to get the number of requests for each Storage object - If you pull 1mb of data out of the database using the Supavisor connection in your edge function, but only sends 100kb back to the user, you will have the Egress from the Supavisor to your Edge function plus from the edge function to your user diff --git a/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx index 29fd5c7f98d94..5986805ee2c96 100644 --- a/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx +++ b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx @@ -13,6 +13,6 @@ You can run this query to check the current settings set for your roles: `SELECT To increase the `statement_timeout` for a specific role, you may follow the instructions [here](/docs/guides/database/timeouts#changing-the-default-timeout). Note that it may require a quick reboot for the changes to take effect. -Additionally, to check how long a query is taking, you can check the Query Performance report which can give you more information on the query's performance: https://app.supabase.com/project/_/reports/query-performance. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: `explain analyze ;`. For supabase-js/ PostgREST queries you can use `.explain()`. +Additionally, to check how long a query is taking, you can check the Query Performance report which can give you more information on the query's performance: https://app.supabase.com/project/_/advisors/query-performance. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: `explain analyze ;`. For supabase-js/ PostgREST queries you can use `.explain()`. You can also make use of Postgres logs that will give you useful information like when the query was executed: https://app.supabase.com/project/_/logs/postgres-logs. diff --git a/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx b/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx index f0e4bd28065c1..14ce81ba1bae1 100644 --- a/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx +++ b/apps/docs/content/troubleshooting/insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ.mdx @@ -10,7 +10,7 @@ database_id = "1da315be-38ac-487a-b178-f865a1a73c09" message = "insufficient privilege" --- -If you see the error "insufficient privilege" when accessing [pg_stat_statements](/docs/guides/platform/performance#postgres-cumulative-statistics-system) or when accessing [Query Performance Report](/dashboard/project/_/reports/query-performance), it means that the Postgres role does not have required permissions. +If you see the error "insufficient privilege" when accessing [pg_stat_statements](/docs/guides/platform/performance#postgres-cumulative-statistics-system) or when accessing [Query Performance Report](/dashboard/project/_/advisors/query-performance), it means that the Postgres role does not have required permissions. In this case, you can run the below command to allow the Postgres role to read all statistics from the system: diff --git a/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx index e1bc9f7b15217..da329451b081c 100644 --- a/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx +++ b/apps/studio/components/layouts/ReportsLayout/Reports.Commands.tsx @@ -41,7 +41,7 @@ export function useReportsGotoCommands(options?: CommandOptions) { { id: 'nav-reports-query-performance', name: 'Query Performance Reports', - route: `/project/${ref}/reports/query-performance`, + route: `/project/${ref}/advisors/query-performance`, defaultHidden: true, }, ] diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 4645351dbf470..c3846e9c7ca08 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -304,7 +304,7 @@ const nextConfig = { }, { permanent: true, - source: '/project/:ref/reports/query-performance', + source: '/project/:ref/query-performance', destination: '/project/:ref/advisors/query-performance', }, { From bc2dac6d68f2ec84c5bea723f32fb9ca67fad137 Mon Sep 17 00:00:00 2001 From: "kemal.earth" <606977+kemaldotearth@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:43:43 +0100 Subject: [PATCH 03/13] feat(studio): add columns and update labels for query performance (#38744) * feat: change calls label to count This changes the column title for calls to count for clarity * feat: add cache hit rate col and number formatting Adds a cache hit rate column for each query as well as tidies up some number formatting. * feat: add styling for 0 numbers This makes anything marked as 0 feint in the table for easier parsing. * chore: remove debug console log * fix: silly next-env again * nit: remove avg rows col * nit: add toFixed to cache hit rate * feat: add tooltip for role column --- .../QueryPerformance.constants.ts | 47 +++++++- .../QueryPerformance/QueryPerformanceGrid.tsx | 113 ++++++++++++++++-- .../interfaces/Reports/Reports.constants.ts | 21 +++- 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts index 3fd1dbfbbeaa0..88bd1b0cf9477 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformance.constants.ts @@ -16,10 +16,51 @@ export const QUERY_PERFORMANCE_COLUMNS = [ { id: 'query', name: 'Query', description: undefined, minWidth: 500 }, { id: 'prop_total_time', name: 'Time consumed', description: undefined, minWidth: 130 }, { id: 'total_time', name: 'Total time', description: 'latency', minWidth: 150 }, - { id: 'calls', name: 'Calls', description: undefined, minWidth: 100 }, + { id: 'calls', name: 'Count', description: undefined, minWidth: 100 }, { id: 'max_time', name: 'Max time', description: undefined, minWidth: 100 }, { id: 'mean_time', name: 'Mean time', description: undefined, minWidth: 100 }, { id: 'min_time', name: 'Min time', description: undefined, minWidth: 100 }, - { id: 'avg_rows', name: 'Avg. Rows', description: undefined, minWidth: 100 }, - { id: 'rolname', name: 'Role', description: undefined, minWidth: 120 }, + { id: 'rows_read', name: 'Rows read', description: undefined, minWidth: 100 }, + { id: 'cache_hit_rate', name: 'Cache hit rate', description: undefined, minWidth: 130 }, + { id: 'rolname', name: 'Role', description: undefined, minWidth: 160 }, +] as const + +export const QUERY_PERFORMANCE_ROLE_DESCRIPTION = [ + { name: 'postgres', description: 'The default Postgres role. This has admin privileges.' }, + { + name: 'anon', + description: + 'For unauthenticated, public access. This is the role which the API (PostgREST) will use when a user is not logged in.', + }, + { + name: 'authenticator', + description: + 'A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then "change into" another role determined by the JWT verification.', + }, + { + name: 'authenticated', + description: + 'For "authenticated access." This is the role which the API (PostgREST) will use when a user is logged in.', + }, + { + name: 'service_role', + description: + 'For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.', + }, + { + name: 'supabase_auth_admin', + description: + 'Used by the Auth middleware to connect to the database and run migration. Access is scoped to the auth schema.', + }, + { + name: 'supabase_storage_admin', + description: + 'Used by the Auth middleware to connect to the database and run migration. Access is scoped to the storage schema.', + }, + { name: 'dashboard_user', description: 'For running commands via the Supabase UI.' }, + { + name: 'supabase_admin', + description: + 'An internal role Supabase uses for administrative tasks, such as running upgrades and automations.', + }, ] as const diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx index acf01e6b997f0..e77aa1aaf62ef 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceGrid.tsx @@ -20,6 +20,7 @@ import { cn, CodeBlock, } from 'ui' +import { InfoTooltip } from 'ui-patterns/info-tooltip' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { hasIndexRecommendations } from './index-advisor.utils' import { IndexSuggestionIcon } from './IndexSuggestionIcon' @@ -28,6 +29,7 @@ import { QueryIndexes } from './QueryIndexes' import { QUERY_PERFORMANCE_COLUMNS, QUERY_PERFORMANCE_REPORT_TYPES, + QUERY_PERFORMANCE_ROLE_DESCRIPTION, } from './QueryPerformance.constants' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' @@ -138,14 +140,6 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance ) } - if (col.id === 'rolname') { - return ( -
-

{value || 'n/a'}

-
- ) - } - if (col.id === 'prop_total_time') { const percentage = props.row.prop_total_time || 0 const fillWidth = Math.min(percentage, 100) @@ -159,7 +153,13 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance opacity: 0.04, }} /> -

{value ? `${value.toFixed(1)}%` : 'n/a'}

+ {value ? ( +

+ {value.toFixed(1)}% +

+ ) : ( +

+ )} ) } @@ -175,8 +175,99 @@ export const QueryPerformanceGrid = ({ queryPerformanceQuery }: QueryPerformance if (col.id === 'total_time') { return (
- {isTime && typeof value === 'number' && !isNaN(value) && isFinite(value) && ( -

{(value / 1000).toFixed(2) + 's' || 'n/a'}

+ {isTime && typeof value === 'number' && !isNaN(value) && isFinite(value) ? ( +

+ {(value / 1000).toFixed(2) + 's'} +

+ ) : ( +

+ )} +
+ ) + } + + if (col.id === 'calls') { + return ( +
+ {typeof value === 'number' && !isNaN(value) && isFinite(value) ? ( +

+ {value.toLocaleString()} +

+ ) : ( +

+ )} +
+ ) + } + + if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') { + return ( +
+ {typeof value === 'number' && !isNaN(value) && isFinite(value) ? ( +

+ {value.toFixed(0)}ms +

+ ) : ( +

+ )} +
+ ) + } + + if (col.id === 'rows_read') { + return ( +
+ {typeof value === 'number' && !isNaN(value) && isFinite(value) ? ( +

+ {value.toLocaleString()} +

+ ) : ( +

+ )} +
+ ) + } + + const cacheHitRateToNumber = (value: number | string) => { + if (typeof value === 'number') return value + return parseFloat(value.toString().replace('%', '')) || 0 + } + + if (col.id === 'cache_hit_rate') { + return ( +
+ {typeof value === 'string' ? ( +

+ {cacheHitRateToNumber(value).toFixed(2)}% +

+ ) : ( +

+ )} +
+ ) + } + + if (col.id === 'rolname') { + return ( +
+ {value ? ( + +

{value}

+ + { + QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((role) => role.name === value) + ?.description + } + +
+ ) : ( +

)}
) diff --git a/apps/studio/components/interfaces/Reports/Reports.constants.ts b/apps/studio/components/interfaces/Reports/Reports.constants.ts index aea68a98aea85..010d309650868 100644 --- a/apps/studio/components/interfaces/Reports/Reports.constants.ts +++ b/apps/studio/components/interfaces/Reports/Reports.constants.ts @@ -369,7 +369,17 @@ select -- min_time, -- max_time, -- mean_time, - statements.rows / statements.calls as avg_rows${ + statements.rows / statements.calls as avg_rows, + statements.rows as rows_read, + case + when (statements.shared_blks_hit + statements.shared_blks_read) > 0 + then round( + (statements.shared_blks_hit * 100.0) / + (statements.shared_blks_hit + statements.shared_blks_read), + 2 + ) + else 0 + end as cache_hit_rate${ runIndexAdvisor ? `, case @@ -513,6 +523,15 @@ select -- max_time, -- mean_time, statements.rows / statements.calls as avg_rows, + statements.rows as rows_read, + statements.shared_blks_hit as debug_hit, + statements.shared_blks_read as debug_read, + case + when (statements.shared_blks_hit + statements.shared_blks_read) > 0 + then (statements.shared_blks_hit::numeric * 100.0) / + (statements.shared_blks_hit + statements.shared_blks_read) + else 0 + end as cache_hit_rate, ((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100 as prop_total_time${ runIndexAdvisor ? `, From 4a69c113e00781c49f28f8d335ce512593249c83 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Tue, 16 Sep 2025 20:57:59 +0800 Subject: [PATCH 04/13] chore: update docs on minting custom jwt (#38653) --- apps/docs/content/guides/auth/jwts.mdx | 10 ++++ .../docs/content/guides/auth/signing-keys.mdx | 59 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index f9f7209ea5fda..576a87819cc9a 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -165,6 +165,16 @@ val supabase = createSupabaseClient( + + +```bash +curl 'https://.supabase.co/rest/v1/my_table?select=id' \ + -H "apikey: $SUPABASE_PUBLISHABLE_KEY" \ + -H "Authorization: Bearer " +``` + + + In the past there was a recommendation to set custom headers on the Supabase client with the `Authorization` header including your custom JWT. This is no longer recommended as it's less flexible and causes confusion when combined with a user session from Supabase Auth. diff --git a/apps/docs/content/guides/auth/signing-keys.mdx b/apps/docs/content/guides/auth/signing-keys.mdx index 6532cf7341e59..f78a3f671ba69 100644 --- a/apps/docs/content/guides/auth/signing-keys.mdx +++ b/apps/docs/content/guides/auth/signing-keys.mdx @@ -176,6 +176,65 @@ supabase gen signing-key --algorithm ES256 Make sure you store this private key in a secure location, as it will not be extractable from Supabase. +To import the generated private key to your project, create a [new standby key](/dashboard/project/_/settings/jwt/signing-keys) from the dashboard: + +```json +{ + "kty": "EC", + "kid": "3a18cfe2-7226-43b0-bbb4-7c5242f2406e", + "d": "RDbwqThwtGP4WnvACvO_0nL0oMMSmMFSYMPosprlAog", + "crv": "P-256", + "x": "gyLVvp9dyEgylYH7nR2E2qdQ_-9Pv5i1tk7c2qZD4Nk", + "y": "CD9RfYOTyjR5U-PC9UDlsthRpc7vAQQQ2FTt8UsX0fY" +} +``` + +Once imported, click **Rotate key** to activate your new signing key. Any JWT signed by your old key will continue to be usable until your old signing key is manually revoked. + +To mint a new JWT using the asymmetric signing key, you need to set the following [JWT headers](/docs/guides/auth/jwts#introduction) to match your generated private key. + +```json +{ + "alg": "ES256", + "kid": "3a18cfe2-7226-43b0-bbb4-7c5242f2406e", + "typ": "JWT" +} +``` + + + +The `kid` header is used to identify your public key for verification. You must use the same value when importing on platform. + + + +In addition, you need to provide the following custom claims as the JWT payload. + +```json +{ + "sub": "ef0493c9-3582-425f-a362-aef909588df7", + "role": "authenticated", + "exp": 1757749466 +} +``` + +- `sub` is an optional UUID that uniquely identifies a user you want to impersonate in `auth.users` table. +- `role` must be set to an existing Postgres role in your database, such as `anon`, `authenticated`, or `service_role`. +- `exp` must be set to a timestamp in the future (seconds since 1970) when this token expires. Prefer shorter-lived tokens. + +For simplicity, use the following CLI command to generate tokens with the desired header and payload. + +```bash +supabase gen bearer-jwt --role authenticated --sub ef0493c9-3582-425f-a362-aef909588df7 +``` + +Finally, you can use your newly minted JWT by setting the `Authorization: Bearer ` header to all [Data API requests](/docs/guides/auth/jwts#using-custom-or-third-party-jwts). + + + +A separate `apikey` header is required to access your project's APIs. This can be a [publishable, secret or the legacy `anon` or `service_role` keys](/docs/guides/api/api-keys). Using your minted JWT is not possible in this header. + + + ### Why is a 5 minute wait imposed when changing signing key states? Changing a JWT signing key's state sets off many changes inside the Supabase platform. To ensure a consistent setup, most actions that change the state of a JWT signing key are throttled for approximately 5 minutes. From e09df40f939d384b57e175c248a4158a41731b7d Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Tue, 16 Sep 2025 15:43:16 +0200 Subject: [PATCH 05/13] docs: update google auth docs (#38740) * docs: update google auth docs * fix issues * fix more issues * more fixes --- .../guides/auth/social-login/auth-google.mdx | 281 +++++++++--------- .../auth-google-consent-screen.png | Bin 81513 -> 0 bytes 2 files changed, 135 insertions(+), 146 deletions(-) delete mode 100644 apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index e73d62e6cf03c..023aa782b0071 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -2,148 +2,124 @@ id: 'auth-google' title: 'Login with Google' description: 'Use Sign in with Google on the web, in native apps or with Chrome extensions' -tocVideo: 'vojHmGUGUGc' --- -Supabase Auth supports Sign in with Google for the web, native Android applications, and Chrome extensions. +Supabase Auth supports [Sign in with Google for the web](https://developers.google.com/identity/gsi/web/guides/overview), native applications ([Android](https://developer.android.com/identity/sign-in/credential-manager-siwg), [macOS and iOS](https://developers.google.com/identity/sign-in/ios/start-integrating)), and [Chrome extensions](https://cloud.google.com/identity-platform/docs/web/chrome-extension). -## Prerequisites - -- A Google Cloud project. Go to the [Google Cloud Platform](https://console.cloud.google.com/home/dashboard) and create a new project if necessary. +You can use Sign in with Google in two ways: -## Configuration +- [By writing application code](#application-code) for the web, native applications or Chrome extensions +- [By using Google's pre-built solutions](#google-pre-built) such as [personalized sign-in buttons](https://developers.google.com/identity/gsi/web/guides/personalized-button), [One Tap](https://developers.google.com/identity/gsi/web/guides/features) or [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) -To support Sign In with Google, you need to configure the Google provider for your Supabase project. - - - - -For web applications, you can set up your signin button two different ways: +## Prerequisites -- Use your own [application code](#application-code) for the button. -- Use [Google's pre-built sign-in or One Tap flows](#google-pre-built). +You need to do some setup to get started with Sign in with Google: -### Application code configuration +- Prepare a Google Cloud project. Go to the [Google Cloud Platform](https://console.cloud.google.com/home/dashboard) and create a new project if necessary. +- Use the [Google Auth Platform console](https://console.cloud.google.com/auth/overview) to register and set up your application's: + - [**Audience**](https://console.cloud.google.com/auth/audience) by configuring which Google users are allowed to sign in to your application. + - [**Data Access (Scopes)**](https://console.cloud.google.com/auth/scopes) define what your application can do with your user's Google data and APIs, such as access profile information or more. + - [**Branding**](https://console.cloud.google.com/auth/branding) and [**Verification**](https://console.cloud.google.com/auth/verification) show a logo and name instead of the Supabase project ID in the consent screen, improving user retention. Brand verification may take a few business days. -To use your own application code: +### Setup required scopes -1. In the Google Cloud console, go to the [Consent Screen configuration page](https://console.cloud.google.com/apis/credentials/consent). The consent screen is the view shown to your users when they consent to signing in to your app. -1. Under **Authorized domains**, add your Supabase project's domain, which has the form `.supabase.co`. -1. Configure the following non-sensitive scopes: - - `.../auth/userinfo.email` - - `...auth/userinfo.profile` - - `openid` -1. Go to the [API Credentials page](https://console.cloud.google.com/apis/credentials). -1. Click `Create credentials` and choose `OAuth Client ID`. -1. For application type, choose `Web application`. -1. Under **Authorized JavaScript origins**, add your site URL. -1. Under **Authorized redirect URLs**, enter the callback URL from the [Supabase dashboard](/dashboard/project/_/auth/providers). Expand the Google Auth Provider section to display it. +Supabase Auth needs a few scopes granting access to profile data of your end users, which you have to configure in the [**Data Access (Scopes)**](https://console.cloud.google.com/auth/scopes) screen: - +- `openid` (add manually) +- `.../auth/userinfo.email` (added by default) +- `...auth/userinfo.profile` (added by default) - The redirect URL is visible to your users. You can customize it by configuring [custom domains](/docs/guides/platform/custom-domains). +If you add more scopes, especially those on the sensitive or restricted list your application might be subject to verification which may take a long time. - +### Setup consent screen branding -1. When you finish configuring your credentials, you will be shown your client ID and secret. Add these to the [Google Auth Provider section of the Supabase Dashboard](/dashboard/project/_/auth/providers). + - Alternatively, you can configure Google authentication using the Management API: +It's strongly recommended you set up a custom domain and optionally verify your brand information with Google, as this makes phishing attempts easier to spot by your users. - ```bash - # First, get your access token from https://supabase.com/dashboard/account/tokens - export SUPABASE_ACCESS_TOKEN="your-access-token" - export PROJECT_REF="your-project-ref" + - # Update auth config to enable Google provider - curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ - -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "external_google_enabled": true, - "external_google_client_id": "your-google-client-id", - "external_google_secret": "your-google-client-secret" - }' - ``` +Google's consent screen is shown to users when they sign in. Optionally configure the following to improve the appearance of the screen, increasing the perception of trust by your users: - +1. Set up a [custom domain for your project](/docs/guides/platform/custom-domains) to present the user with a clear relationship to the website they clicked Sign in with Google on. + - A good approach is to use `auth.example.com` or `api.example.com`, if your application is hosted on `example.com`. + - If you don't set this up, users will see `.supabase.co` which does not inspire trust and can make your application more susceptible to successful phishing attempts. +1. Verify your application's brand (logo and name) by configuring it in the [Branding](https://console.cloud.google.com/auth/branding) section of the Google Auth Platform console. Brand verification is not automatic and may take a few business days. - In local development, you can add the client ID and secret to your `config.toml` file. +## Project setup - +To support Sign In with Google, you need to configure the Google provider for your Supabase project. -### Google pre-built configuration + + -To use Google's pre-built signin buttons: +Regardless of whether you use application code or Google's pre-built solutions to implement the sign in flow, you need to configure your project by obtaining a Client ID and Client Secret in the [Clients](https://console.cloud.google.com/auth/clients) section of the Google Auth Platform console: -1. In the Google Cloud console, go to the [Consent Screen configuration page](https://console.cloud.google.com/apis/credentials/consent). The consent screen is the view shown to your users when they consent to signing in to your app. -1. Configure the screen to your liking, making sure you add links to your app's privacy policy and terms of service. -1. Go to the [API Credentials page](https://console.cloud.google.com/apis/credentials). -1. Click `Create credentials` and choose `OAuth Client ID`. -1. For application type, choose `Web application`. -1. Under **Authorized JavaScript origins** and **Authorized redirect URLs**, add your site URL. This is the URL of the website where the signin button will appear, _not_ your Supabase project domain. If you're testing in localhost, ensure that you have `http://localhost` set in the **Authorized JavaScript origins** section as well. This is important when integrating with Google One-Tap to ensure you can use it locally. -1. When you finish configuring your credentials, you will be shown your client ID. Add this to the **Client IDs** field in the [Google Auth Provider section of the Supabase Dashboard](/dashboard/project/_/auth/providers). Leave the OAuth client ID and secret blank. You don't need them when using Google's pre-built approach. +1. [Create a new OAuth client ID](https://console.cloud.google.com/auth/clients/create) and choose **Web application** for the application type. +1. Under **Authorized JavaScript origins** add your application's URL. These should also be configured as the [Site URL or redirect configuration in your project](/docs/guides/auth/redirect-urls). + - If your app is hosted on `https://example.com/app` add `https://example.com`. + - Add `http://localhost:` while developing locally. Remember to remove this when your application [goes into production](/docs/guides/deployment/going-into-prod). +1. Under **Authorized redirect URIs** add your Supabase project's callback URL. + - Access it from the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google). + - For local development, use `http://localhost:3000/auth/v1/callback`. +1. Click `Create` and make sure you save the Client ID and Client Secret. + - Add these values to the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google). - 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for. - - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. - - You will have a different set of SHA-1 certificate fingerprints for testing locally and going to production. Make sure to add both to the Google Cloud Console, and add all of the Client IDs to the Supabase dashboard. - - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers), under _Client IDs_. - - Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. +1. [Create a new OAuth client ID](https://console.cloud.google.com/auth/clients/create) and choose **Android** or **iOS** depending on the OS you're building the app for. + - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. + - You will have a different set of SHA-1 certificate fingerprints for testing locally and going to production. Make sure to add both to the Google Cloud Console, and add all of the Client IDs to the Supabase dashboard. + - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. +1. Register the Client ID in the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google). - + + +1. [Create a new OAuth client ID](https://console.cloud.google.com/auth/clients/create) and choose **Android** or **iOS** depending on the OS you're building the app for. + - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. + - You will have a different set of SHA-1 certificate fingerprints for testing locally and going to production. Make sure to add both to the Google Cloud Console, and add all of the Client IDs to the Supabase dashboard. + - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. +1. Register the Client ID in the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google). + - For iOS enable the `Skip nonce check` option. + +For iOS add a `CFBundleURLTypes` key in the `/ios/Runner/Info.plist` file: + +```xml + + +CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + + com.googleusercontent.apps.861823949799-vc35cprkp249096uujjn0vvnmcvjppkn + + + + +``` - ### iOS and Android + - 1. Configure Web, Android, and iOS OAuth credentials. Follow the Platform integration instructions on the [README of google_sign_in package](https://pub.dev/packages/google_sign_in#platform-integration) for Android and iOS. - - For both Android and iOS, you will need to create a Web client ID in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials). - - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. - - You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard. - - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers), under _Client IDs_. If you need iOS support, enable the `Skip nonce check` option. - 4. For iOS, add the `CFBundleURLTypes` attributes below into the `/ios/Runner/Info.plist` file. - ```xml - - - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - - - com.googleusercontent.apps.861823949799-vc35cprkp249096uujjn0vvnmcvjppkn - - - - - ``` - - ### Web, macOS, Windows, and Linux - - To use the OAuth 2.0 flow, you will require the following information: - - 1. Obtain OAuth credentials for your Google Cloud project in the [Credentials](https://console.developers.google.com/apis/credentials) page of the console. If you already have a web client ID, no need to create a new one. When creating a new credential, choose _Web application_. In _Authorized redirect URIs_ enter `https://.supabase.co/auth/v1/callback`. This URL will be seen by your users, and you can customize it by configuring [custom domains](/docs/guides/platform/custom-domains). - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. Within _Authorized domains_ make sure you add your Supabase project's domain `.supabase.co`. Configure the non-sensitive scopes by making sure the following ones are selected: `.../auth/userinfo.email`, `.../auth/userinfo.profile`, `openid`. If you're selecting other sensitive scopes, your app may require additional verification. In those cases, it's best to use [custom domains](/docs/guides/platform/custom-domains). - 3. Finally, add the client ID and secret from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers). + - +Follow the same configuration guide as if your app was a Web application when building a desktop Flutter application. + + <$Show if="sdk:swift"> @@ -195,40 +171,53 @@ To use Google's pre-built signin buttons: - ### Using Google sign-in on Android - - 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. You need two client IDs, a web client ID and Android client ID. When creating a new Android OAuth client ID, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the web client ID from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers), under _Client IDs_. - - Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. +1. [Create a new OAuth client ID](https://console.cloud.google.com/auth/clients/create) and choose **Android** or **iOS** if also building an iOS app with Kotlin Multiplatform. + - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. + - You will have a different set of SHA-1 certificate fingerprints for testing locally and going to production. Make sure to add both to the Google Cloud Console, and add all of the Client IDs to the Supabase dashboard. + - For iOS (with Kotlin Multiplatform), use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. +1. Register the Client ID in the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google). - ### Using Google sign-in with Kotlin Multiplatform + - 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for. + - - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. - - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store. +1. [Create a new OAuth client ID](https://console.cloud.google.com/auth/clients/create) and choose **Chrome Extension** for application type. + - Enter your extension's Item ID and optionally verify app ownership. +1. Register the Client ID in the [Google provider page on the Dashboard](/dashboard/project/_/auth/providers?provider=Google) under _Client IDs_. - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers), under _Client IDs_. + + - Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. +### Local development - +To use the Google provider in local development: - +1. Add a new environment variable: + ```env + SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET="" + ``` +2. Configure the provider: + ```toml + [auth.external.google] + enabled = true + client_id = "" + secret = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET)" + skip_nonce_check = false + ``` - You will need to configure a client ID for your Chrome extension: +If you have multiple client IDs, such as one for Web, iOS and Android, concatenate all of the client IDs with a comma but make sure the web's client ID is first in the list. - 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Chrome extension_ for the application type. For _Item ID_ provide the unique ID of your Chrome extension. You can get this by calling `chrome.runtime.id` within the extension, or from the Web Store URL of the extension. For example, the [Google Translate extension](https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb) has the Web Store URL `https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb` and the last part `aapbdbdomjkkjkaonfhkkikfgjllcleb` is its unique ID. - 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](/dashboard/project/_/auth/providers), under _Client IDs_. +### Using the management API - Note that you do not have to configure the OAuth flow in the Supabase Dashboard to sign in with Google inside Chrome extensions. +Use the [PATCH `/v1/projects/{ref}/config/auth` Management API endpoint](/docs/reference/api/v1-update-auth-service-config) to configure the project's Auth settings programmatically. For configuring the Google provider send these options: - - +```json +{ + "external_google_enabled": true, + "external_google_client_id": "your-google-client-id", + "external_google_secret": "your-google-client-secret" +} +``` ## Signing users in @@ -287,7 +276,7 @@ const { data, error } = await supabase.auth.signInWithOAuth({ }) ``` -### Google pre-built +### Google pre-built [#google-pre-built] Most web apps and websites can utilize Google's [personalized sign-in buttons](https://developers.google.com/identity/gsi/web/guides/personalized-button), [One Tap](https://developers.google.com/identity/gsi/web/guides/features) or [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) for the best user experience. @@ -514,9 +503,7 @@ export default function () { - - -### iOS and Android + Google sign-in with Supabase is done through the [google_sign_in](https://pub.dev/packages/google_sign_in) package for iOS and Android. @@ -576,7 +563,18 @@ Future _nativeGoogleSignIn() async { ... ``` -### Web, macOS, Windows, and Linux +
+ +
+ +
+ + Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [`signInWithOAuth`](docs/reference/dart/auth-signinwithoauth) method. @@ -808,12 +806,3 @@ chrome.identity.launchWebAuthFlow( - -## Google consent screen - -![Google Consent Screen](/docs/img/guides/auth-google/auth-google-consent-screen.png) - -By default, the Google consent screen shows the root domain of the callback URL, where Google will send the authentication response. With Supabase Auth, it is your Supabase project's domain `(https://.supabase.co)`. - -You can change this domain in the settings for your Google app. Go to Google App Platform, click on Branding, and then set your application home page, privacy policy, and terms of use. You can also add your logo. -Next, go to Audience and click publish. Google will need to verify the app, which usually takes around 24 hours. diff --git a/apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png b/apps/docs/public/img/guides/auth-google/auth-google-consent-screen.png deleted file mode 100644 index 76942f3b74d5f38741ad9e2e7191930270ebdccf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81513 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 ~K{48;W30xx^7eWcwkMJL!<8vqKJ6di^ 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�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

5_MNV#9Cw-va>F8*AE+qFeQS)1CZ&k*) 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 OK;Y@>=d#Wzp$PymW|uku From c0da4fc689bc966da8ebaeef8dc6535f10b9635a Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 16 Sep 2025 22:06:57 +0800 Subject: [PATCH 06/13] chore: add custom content provider label (#38745) * chore: add custom content provider label * Remove lowercase for desc in ProjectCard --------- Co-authored-by: Joshen Lim --- .../Home/ProjectList/ProjectCard.tsx | 10 ++++-- .../InstanceNode.tsx | 10 ++++-- .../custom-content/CustomContent.types.ts | 1 + .../hooks/custom-content/custom-content.json | 1 + .../custom-content/custom-content.sample.json | 36 +------------------ .../custom-content/custom-content.schema.json | 5 +++ 6 files changed, 24 insertions(+), 39 deletions(-) diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index ada3f39721b7e..35afb66f84174 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -7,6 +7,7 @@ import type { IntegrationProjectConnection } from 'data/integrations/integration import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import { OrgProject } from 'data/projects/projects-infinite-query' import type { ResourceWarning } from 'data/usage/resource-warnings-query' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from 'lib/constants' import { inferProjectStatus } from './ProjectCard.utils' @@ -28,7 +29,12 @@ export const ProjectCard = ({ resourceWarnings, }: ProjectCardProps) => { const { name, ref: projectRef } = project - const desc = `${project.cloud_provider} | ${project.region}` + + const { infraAwsNimbusLabel } = useCustomContent(['infra:aws_nimbus_label']) + const providerLabel = + project.cloud_provider === 'AWS_NIMBUS' ? infraAwsNimbusLabel : project.cloud_provider + + const desc = `${providerLabel} | ${project.region}` const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([ 'project_homepage:show_instance_size', @@ -47,7 +53,7 @@ export const ProjectCard = ({ title={

{name}

- {desc} + {desc}
{project.status !== 'INACTIVE' && projectHomepageShowInstanceSize && ( diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index 07dc3bf06743c..98f1611f4c609 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -14,6 +14,7 @@ import { useReadReplicasStatusesQuery, } from 'data/read-replicas/replicas-status-query' import { formatDatabaseID } from 'data/read-replicas/replicas.utils' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from 'lib/constants' @@ -113,6 +114,8 @@ export const PrimaryNode = ({ data }: NodeProps) => { const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([ 'project_homepage:show_instance_size', ]) + const { infraAwsNimbusLabel } = useCustomContent(['infra:aws_nimbus_label']) + const providerLabel = provider === 'AWS_NIMBUS' ? infraAwsNimbusLabel : provider return ( <> @@ -137,7 +140,7 @@ export const PrimaryNode = ({ data }: NodeProps) => { {region.name}

- {provider} + {providerLabel} {projectHomepageShowInstanceSize && ( <> @@ -231,6 +234,9 @@ export const ReplicaNode = ({ data }: NodeProps) => { ] as string[] ).includes(status) || initStatus === ReplicaInitializationStatus.InProgress + const { infraAwsNimbusLabel } = useCustomContent(['infra:aws_nimbus_label']) + const providerLabel = provider === 'AWS_NIMBUS' ? infraAwsNimbusLabel : provider + return ( <> @@ -298,7 +304,7 @@ export const ReplicaNode = ({ data }: NodeProps) => {

{region.name}

- {provider} + {providerLabel} {projectHomepageShowInstanceSize && !!computeSize && ( <> diff --git a/apps/studio/hooks/custom-content/CustomContent.types.ts b/apps/studio/hooks/custom-content/CustomContent.types.ts index 8d79b162c0387..7fbe87e074151 100644 --- a/apps/studio/hooks/custom-content/CustomContent.types.ts +++ b/apps/studio/hooks/custom-content/CustomContent.types.ts @@ -28,6 +28,7 @@ export type CustomContentTypes = { logsDefaultQuery: string infraCloudProviders: CloudProvider[] + infraAwsNimbusLabel: string sslCertificateUrl: string } diff --git a/apps/studio/hooks/custom-content/custom-content.json b/apps/studio/hooks/custom-content/custom-content.json index 65686e6f1294d..2702c8c2728c8 100644 --- a/apps/studio/hooks/custom-content/custom-content.json +++ b/apps/studio/hooks/custom-content/custom-content.json @@ -50,6 +50,7 @@ "logs:default_query": null, "infra:cloud_providers": ["AWS", "AWS_K8S", "FLY"], + "infra:aws_nimbus_label": "AWS Nimbus", "ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt" } diff --git a/apps/studio/hooks/custom-content/custom-content.sample.json b/apps/studio/hooks/custom-content/custom-content.sample.json index 3573fc9471aa7..b7adc83af68c2 100644 --- a/apps/studio/hooks/custom-content/custom-content.sample.json +++ b/apps/studio/hooks/custom-content/custom-content.sample.json @@ -49,42 +49,8 @@ "logs:default_query": "This is a sample query", - "connect:frameworks": { - "key": "frameworks", - "label": "Frameworks", - "obj": [ - { - "key": "framework-1", - "label": "Framework 1", - "icon": "https://supabase.com/dashboard/img/supabase-logo.svg", - "guideLink": "https://supabase.com/docs", - "children": [], - "files": [ - { - "name": "sample_env", - "content": "NEXT_PUBLIC_SUPABASE_URL={{apiUrl}}\nNEXT_PUBLIC_SUPABASE_ANON_KEY={{anonKey}}" - }, - { - "name": "sample.ts", - "content": "import { createClient } from \"@supabase/supabase-js\";\n\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\nconst supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\nexport const supabase = createClient(supabaseUrl, supabaseKey);" - } - ] - }, - { - "key": "framework-2", - "label": "Framework 2", - "icon": "https://supabase.com/dashboard/img/supabase-logo.svg", - "guideLink": "https://supabase.com/docs", - "children": [], - "files": [ - { "name": "file3.tsx", "content": "Content of File 3" }, - { "name": "file4.tsx", "content": "Content of File 4" } - ] - } - ] - }, - "infra:cloud_providers": ["AWS_NIMBUS"], + "infra:aws_nimbus_label": "AWS Nimbus", "ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt" } diff --git a/apps/studio/hooks/custom-content/custom-content.schema.json b/apps/studio/hooks/custom-content/custom-content.schema.json index bb7f5f8a8a0a0..5569c4d71fa51 100644 --- a/apps/studio/hooks/custom-content/custom-content.schema.json +++ b/apps/studio/hooks/custom-content/custom-content.schema.json @@ -74,6 +74,10 @@ "enum": ["AWS", "AWS_K8S", "AWS_NIMBUS", "FLY"] } }, + "infra:aws_nimbus_label": { + "type": "string", + "description": "The label of the AWS Nimbus provider" + }, "ssl:certificate_url": { "type": "string", @@ -86,6 +90,7 @@ "project_homepage:example_projects", "logs:default_query", "infra:cloud_providers", + "infra:aws_nimbus_label", "ssl:certificate_url" ], "additionalProperties": false From cdde0d574743e6804aa81fefdb9bfc022e5d826e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=83=A5=EB=83=90=EC=B1=A0?= Date: Tue, 16 Sep 2025 23:10:11 +0900 Subject: [PATCH 07/13] fix: functions on the Supabase Preview do not deploy properly (#38729) --- supabase/functions/deno.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 supabase/functions/deno.json diff --git a/supabase/functions/deno.json b/supabase/functions/deno.json new file mode 100644 index 0000000000000..179713338060e --- /dev/null +++ b/supabase/functions/deno.json @@ -0,0 +1,3 @@ +{ + +} From 98b6bff713f253ec19ca7b732670d882e8b99fcd Mon Sep 17 00:00:00 2001 From: Chris Stockton <180184+cstockton@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:10:41 -0700 Subject: [PATCH 08/13] fix(docs/auth): update auth settings links (#38725) Many links were redirecting to a 404, this updates them to point to each links new settings page. Co-authored-by: Chris Stockton --- .../content/guides/auth/auth-anonymous.mdx | 2 +- .../docs/content/guides/auth/auth-captcha.mdx | 2 +- .../guides/auth/auth-identity-linking.mdx | 2 +- .../content/guides/auth/auth-mfa/phone.mdx | 2 +- apps/docs/content/guides/auth/auth-smtp.mdx | 2 +- .../guides/auth/general-configuration.mdx | 22 +++++++++++++------ apps/docs/content/guides/auth/sessions.mdx | 10 ++++----- .../content/guides/auth/third-party/auth0.mdx | 4 ++-- .../guides/auth/third-party/aws-cognito.mdx | 4 ++-- .../guides/auth/third-party/firebase-auth.mdx | 4 ++-- .../guides/auth/third-party/workos.mdx | 4 ++-- .../getting-started/quickstarts/hono.mdx | 2 +- .../cli/testing-and-linting.mdx | 2 +- 13 files changed, 35 insertions(+), 27 deletions(-) diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index f6c16268b0f17..5a32482cefeb7 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -112,7 +112,7 @@ response = supabase.auth.sign_in_anonymously() ## Convert an anonymous user to a permanent user -Converting an anonymous user to a permanent user requires [linking an identity](/docs/guides/auth/auth-identity-linking#manual-linking-beta) to the user. This requires you to [enable manual linking](/dashboard/project/_/settings/auth) in your Supabase project. +Converting an anonymous user to a permanent user requires [linking an identity](/docs/guides/auth/auth-identity-linking#manual-linking-beta) to the user. This requires you to [enable manual linking](/dashboard/project/_/auth/providers) in your Supabase project. ### Link an email / phone identity diff --git a/apps/docs/content/guides/auth/auth-captcha.mdx b/apps/docs/content/guides/auth/auth-captcha.mdx index d28f7df4a6fb4..bc832cf0cb88c 100644 --- a/apps/docs/content/guides/auth/auth-captcha.mdx +++ b/apps/docs/content/guides/auth/auth-captcha.mdx @@ -41,7 +41,7 @@ In the Settings page, look for the **Sitekey** section and copy the key. ## Enable CAPTCHA protection for your Supabase project -Navigate to the **[Auth](/dashboard/project/_/settings/auth)** section of your Project Settings in the Supabase Dashboard and find the **Enable CAPTCHA protection** toggle under Settings > Authentication > Bot and Abuse Protection > Enable CAPTCHA protection. +Navigate to the **[Auth](/dashboard/project/_/auth/protection)** section of your Project Settings in the Supabase Dashboard and find the **Enable CAPTCHA protection** toggle under Settings > Authentication > Bot and Abuse Protection > Enable CAPTCHA protection. Select your CAPTCHA provider from the dropdown, enter your CAPTCHA **Secret key**, and click **Save**. diff --git a/apps/docs/content/guides/auth/auth-identity-linking.mdx b/apps/docs/content/guides/auth/auth-identity-linking.mdx index 8ccdb45aae473..aa8a98b22ec44 100644 --- a/apps/docs/content/guides/auth/auth-identity-linking.mdx +++ b/apps/docs/content/guides/auth/auth-identity-linking.mdx @@ -88,7 +88,7 @@ response = supabase.auth.link_identity({'provider': 'google'}) -In the example above, the user will be redirected to Google to complete the OAuth2.0 flow. Once the OAuth2.0 flow has completed successfully, the user will be redirected back to the application and the Google identity will be linked to the user. You can enable manual linking from your project's authentication [configuration options](/dashboard/project/_/settings/auth) or by setting the environment variable `GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: true` when self-hosting. +In the example above, the user will be redirected to Google to complete the OAuth2.0 flow. Once the OAuth2.0 flow has completed successfully, the user will be redirected back to the application and the Google identity will be linked to the user. You can enable manual linking from your project's authentication [configuration options](/dashboard/project/_/auth/providers) or by setting the environment variable `GOTRUE_SECURITY_MANUAL_LINKING_ENABLED: true` when self-hosting. ## Unlink an identity diff --git a/apps/docs/content/guides/auth/auth-mfa/phone.mdx b/apps/docs/content/guides/auth/auth-mfa/phone.mdx index f29187d7227bb..d1fcc3e86fc2b 100644 --- a/apps/docs/content/guides/auth/auth-mfa/phone.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/phone.mdx @@ -306,7 +306,7 @@ function AuthMFA() { ### Security configuration -Each code is valid for up to 5 minutes, after which a new one can be sent. Successive codes remain valid until expiry. When possible choose the longest code length acceptable to your use case, at a minimum of 6. This can be configured in the [Authentication Settings](/dashboard/project/_/settings/auth). +Each code is valid for up to 5 minutes, after which a new one can be sent. Successive codes remain valid until expiry. When possible choose the longest code length acceptable to your use case, at a minimum of 6. This can be configured in the [Authentication Settings](/dashboard/project/_/auth/mfa). Be aware that Phone MFA is vulnerable to SIM swap attacks where an attacker will call a mobile provider and ask to port the target's phone number to a new SIM card and then use the said SIM card to intercept an MFA code. Evaluate the your application's tolerance for such an attack. You can read more about SIM swapping attacks [here](https://en.wikipedia.org/wiki/SIM_swap_scam) diff --git a/apps/docs/content/guides/auth/auth-smtp.mdx b/apps/docs/content/guides/auth/auth-smtp.mdx index 5f7f05b0ed5d4..2e4d9fefd334e 100644 --- a/apps/docs/content/guides/auth/auth-smtp.mdx +++ b/apps/docs/content/guides/auth/auth-smtp.mdx @@ -48,7 +48,7 @@ A non-exhaustive list of services that work with Supabase Auth is: - [ZeptoMail](https://www.zoho.com/zeptomail/help/smtp-home.html) - [Brevo](https://help.brevo.com/hc/en-us/articles/7924908994450-Send-transactional-emails-using-Brevo-SMTP) -Once you've set up your account with an email sending service, head to the [Authentication settings page](/dashboard/project/_/settings/auth) to enable and configure custom SMTP. +Once you've set up your account with an email sending service, head to the [Authentication settings page](/dashboard/project/_/auth/smtp) to enable and configure custom SMTP. You can also configure custom SMTP using the Management API: diff --git a/apps/docs/content/guides/auth/general-configuration.mdx b/apps/docs/content/guides/auth/general-configuration.mdx index 0eefbd1a922ef..e1619774dcbd5 100644 --- a/apps/docs/content/guides/auth/general-configuration.mdx +++ b/apps/docs/content/guides/auth/general-configuration.mdx @@ -4,13 +4,21 @@ title: 'General configuration' subtitle: 'General configuration options for Supabase Auth' --- -This section covers the [general configuration options](/dashboard/project/_/settings/auth) for Supabase Auth. If you are looking for another type of configuration, you may be interested in one of the following sections: - -- [Provider-specific configuration](/dashboard/project/_/auth/providers) -- [Rate limits](/dashboard/project/_/auth/rate-limits) -- [Email Templates](/dashboard/project/_/auth/templates) -- [Redirect URLs](/dashboard/project/_/auth/url-configuration) -- [Auth Hooks](/dashboard/project/_/auth/hooks) +This section covers the [general configuration options](/dashboard/project/_/auth) for Supabase Auth. If you are looking for another type of configuration, you may be interested in one of the following sections: + +- [Policies](/dashboard/project/_/auth/policies) to manage Row Level Security policies for your tables. +- [Sign In / Providers](/dashboard/project/_/auth/providers) to configure authentication providers and login methods for your users. +- [Third Party Auth](/dashboard/project/_/auth/third-party) to use third-party authentication (TPA) systems based on JWTs to access your project. +- [Sessions](/dashboard/project/_/auth/sessions) to configure settings for user sessions and refresh tokens. +- [Rate limits](/dashboard/project/_/auth/rate-limits) to safeguard against bursts of incoming traffic to prevent abuse and maximize stability. +- [Email Templates](/dashboard/project/_/auth/templates) to configure what emails your users receive. +- [Custom SMTP](/dashboard/project/_/auth/smtp) to configure how emails are sent. +- [Multi-Factor](/dashboard/project/_/auth/mfa) to require users to provide additional verification factors to authenticate. +- [URL Configuration](/dashboard/project/_/auth/url-configuration) to configure site URL and redirect URLs for authentication. +- [Attack Protection](/dashboard/project/_/auth/protection) to configure security settings to protect your project from attacks. +- [Auth Hooks (BETA)](/dashboard/project/_/auth/auth-hooks) to use Postgres functions or HTTP endpoints to customize the behavior of Supabase Auth to meet your needs. +- [Audit Logs (BETA)](/dashboard/project/_/auth/audit-logs) to track and monitor auth events in your project. +- [Advanced](/dashboard/project/_/auth/advanced) to configure advanced authentication server settings. Supabase Auth provides these [general configuration options](/dashboard/project/_/settings/auth) to control user access to your application: diff --git a/apps/docs/content/guides/auth/sessions.mdx b/apps/docs/content/guides/auth/sessions.mdx index b83940ccba2d8..ebaeec2937cc2 100644 --- a/apps/docs/content/guides/auth/sessions.mdx +++ b/apps/docs/content/guides/auth/sessions.mdx @@ -55,11 +55,11 @@ There are three ways to limit the lifetime of a session: - Set an inactivity timeout, which terminates sessions that haven't been refreshed within the timeout duration. - Enforce a single-session per user, which only keeps the most recently active session. -To make sure that users are required to re-authenticate periodically, you can set a positive value for the **Time-box user sessions** option in the [Auth settings](/dashboard/project/_/settings/auth) for your project. +To make sure that users are required to re-authenticate periodically, you can set a positive value for the **Time-box user sessions** option in the [Auth settings](/dashboard/project/_/auth/sessions) for your project. -To make sure that sessions expire after a period of inactivity, you can set a positive duration for the **Inactivity timeout** option in the [Auth settings](/dashboard/project/_/settings/auth). +To make sure that sessions expire after a period of inactivity, you can set a positive duration for the **Inactivity timeout** option in the [Auth settings](/dashboard/project/_/auth/sessions). -You can also enforce only one active session per user per device or browser. When this is enabled, the session from the most recent sign in will remain active, while the rest are terminated. Enable this via the _Single session per user_ option in the [Auth settings](/dashboard/project/_/settings/auth). +You can also enforce only one active session per user per device or browser. When this is enabled, the session from the most recent sign in will remain active, while the rest are terminated. Enable this via the _Single session per user_ option in the [Auth settings](/dashboard/project/_/auth/sessions). Sessions are not proactively destroyed when you change these settings, but rather the check is enforced whenever a session is refreshed next. This can confuse developers because the actual duration of a session is the configured timeout plus the JWT expiration time. For single session per user, the effect will only be noticed at intervals of the JWT expiration time. Make sure you adjust this setting depending on your needs. We do not recommend going below 5 minutes for the JWT expiration time. @@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th ### What are recommended values for access token (JWT) expiration? -Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/auth) in the Advanced Settings section. +Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/auth/sessions) in the Advanced Settings section. Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations. @@ -93,7 +93,7 @@ The general rule is that a refresh token can only be used once. However, strictl - All clients such as browsers, mobile or desktop apps, and even some servers are inherently unreliable due to network issues. A request does not indicate that they received a response or even processed the response they received. - If a refresh token is revoked after being used only once, and the response wasn't received and processed by the client, when the client comes back online, it will attempt to use the refresh token that was already used. Since this might happen outside of the reuse interval, it can cause sudden and unexpected session termination. -Should the reuse attempt not fall under these two exceptions, the whole session is regarded as terminated and all refresh tokens belonging to it are marked as revoked. You can disable this behavior in the Advanced Settings of the [Auth settings](/dashboard/project/_/settings/auth) page, though it is generally not recommended. +Should the reuse attempt not fall under these two exceptions, the whole session is regarded as terminated and all refresh tokens belonging to it are marked as revoked. You can disable this behavior in the Advanced Settings of the [Auth settings](/dashboard/project/_/auth/sessions) page, though it is generally not recommended. The purpose of this mechanism is to guard against potential security issues where a refresh token could have been stolen from the user, for example by exposing it accidentally in logs that leak (like logging cookies, request bodies or URL params) or via vulnerable third-party servers. It does not guard against the case where a user's session is stolen from their device. diff --git a/apps/docs/content/guides/auth/third-party/auth0.mdx b/apps/docs/content/guides/auth/third-party/auth0.mdx index 634378b437ab3..d50f96913f6cc 100644 --- a/apps/docs/content/guides/auth/third-party/auth0.mdx +++ b/apps/docs/content/guides/auth/third-party/auth0.mdx @@ -9,7 +9,7 @@ Auth0 can be used as a third-party authentication provider alongside Supabase Au ## Getting started 1. First you need to add an integration to connect your Supabase project with your Auth0 tenant. You will need your tenant ID (and in some cases region ID). -2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth). +2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/auth/third-party). 3. Assign the `role: 'authenticated'` custom claim to all JWTs by using an Auth0 Action. 4. Finally setup the Supabase client in your application. @@ -128,7 +128,7 @@ val supabase = createSupabaseClient( ## Add a new Third-Party Auth integration to your project -In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/settings/auth) and find the Third-Party Auth section to add a new integration. +In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/auth/third-party) and find the Third-Party Auth section to add a new integration. In the CLI add the following config to your `supabase/config.toml` file: diff --git a/apps/docs/content/guides/auth/third-party/aws-cognito.mdx b/apps/docs/content/guides/auth/third-party/aws-cognito.mdx index 72e99ecf8a860..208f838d2b58a 100644 --- a/apps/docs/content/guides/auth/third-party/aws-cognito.mdx +++ b/apps/docs/content/guides/auth/third-party/aws-cognito.mdx @@ -9,7 +9,7 @@ Amazon Cognito User Pools (via AWS Amplify or on its own) can be used as a third ## Getting started 1. First you need to add an integration to connect your Supabase project with your Amazon Cognito User Pool. You will need the pool's ID and region. -2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth) or configure it in the CLI. +2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/auth/third-party) or configure it in the CLI. 3. Assign the `role: 'authenticated'` custom claim to all JWTs by using a Pre-Token Generation Trigger. 4. Finally setup the Supabase client in your application. @@ -142,7 +142,7 @@ suspend fun getAccessToken(): String? { ## Add a new Third-Party Auth integration to your project -In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/settings/auth) and find the Third-Party Auth section to add a new integration. +In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/auth/third-party) and find the Third-Party Auth section to add a new integration. In the CLI add the following config to your `supabase/config.toml` file: diff --git a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx index a9a2b9246c00e..3d8fd20cf4f28 100644 --- a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx +++ b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx @@ -9,7 +9,7 @@ Firebase Auth can be used as a third-party authentication provider alongside Sup ## Getting started 1. First you need to add an integration to connect your Supabase project with your Firebase project. You will need to get the Project ID in the [Firebase Console](https://console.firebase.google.com/u/0/project/_/settings/general). -2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth). +2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/auth/third-party). 3. If you are using Third Party Auth when self hosting, create and attach restrictive RLS policies to all tables in your public schema, Storage and Realtime to **prevent unauthorized access from unrelated Firebase projects**. 4. Assign the `role: 'authenticated'` [custom user claim](https://firebase.google.com/docs/auth/admin/custom-claims) to all your users. 5. Finally set up the Supabase client in your application. @@ -143,7 +143,7 @@ val supabase = createSupabaseClient( ## Add a new Third-Party Auth integration to your project -In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/settings/auth) and find the Third-Party Auth section to add a new integration. +In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/auth/third-party) and find the Third-Party Auth section to add a new integration. In the CLI add the following config to your `supabase/config.toml` file: diff --git a/apps/docs/content/guides/auth/third-party/workos.mdx b/apps/docs/content/guides/auth/third-party/workos.mdx index 3edbf1d9d92e2..5ca2630ab46b2 100644 --- a/apps/docs/content/guides/auth/third-party/workos.mdx +++ b/apps/docs/content/guides/auth/third-party/workos.mdx @@ -9,7 +9,7 @@ WorkOS can be used as a third-party authentication provider alongside Supabase A ## Getting started 1. First you need to add an integration to connect your Supabase project with your WorkOS tenant. You will need your WorkOS issuer. The issuer is `https://api.workos.com/user_management/`. Substitute your [custom auth domain](https://workos.com/docs/custom-domains/auth-api) for "api.workos.com" if configured. -2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth). +2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/auth/third-party). 3. Set up a JWT template to assign the `role: 'authenticated'` claim to your access token. ## Setup the Supabase client library @@ -43,7 +43,7 @@ const supabase = createClient( ## Add a new Third-Party Auth integration to your project -In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/settings/auth) and find the Third-Party Auth section to add a new integration. +In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/auth/third-party) and find the Third-Party Auth section to add a new integration. ## Set up a JWT template to add the authenticated role. diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index dda758571bd7a..565ee5fae01a7 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -47,7 +47,7 @@ hideToc: true Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and anon key. - Lastly, [enable anonymous sign-ins](/dashboard/project/_/settings/auth) in the Auth settings. + Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings. 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 bd1c44b453d8f..0d0abc5f6414c 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/_/settings/auth). +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). ## Linting your database From 8e1e21f4d6837b6f6a38de2e65068c787bebb1b6 Mon Sep 17 00:00:00 2001 From: hallidayo <22655069+Hallidayo@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:20:28 +0100 Subject: [PATCH 09/13] docs: Fix delete objects limit (#38134) add admonition about deleting object limit --- .../content/guides/storage/management/delete-objects.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/docs/content/guides/storage/management/delete-objects.mdx b/apps/docs/content/guides/storage/management/delete-objects.mdx index 0565ad84ff125..92dacf02ac474 100644 --- a/apps/docs/content/guides/storage/management/delete-objects.mdx +++ b/apps/docs/content/guides/storage/management/delete-objects.mdx @@ -26,6 +26,12 @@ const supabase = createClient('your_project_url', 'your_supabase_api_key') await supabase.storage.from('bucket').remove(['object-path-2', 'folder/avatar2.png']) ``` + + +When deleting objects, there is a limit of 1000 objects at a time using the `remove` method. + + + ## RLS To delete an object, the user must have the `delete` permission on the object. For example: From 2ec5228a587d06aa5c7f4f657c0aeed9f1510f4c Mon Sep 17 00:00:00 2001 From: issuedat <165281975+issuedat@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:20:36 +0200 Subject: [PATCH 10/13] Update humans.txt (#38381) --- 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 e82d57cc990e5..2aa31e79e6f06 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -50,6 +50,7 @@ Emmett Folger Eric Kharitonashvili Etienne Stalmans Fabrizio Fenoglio +Fady A Felipe Stival Francesco Sansalvadore Greg Kress From 699dd6d19e66064d7ce67e5dc005176d543a7446 Mon Sep 17 00:00:00 2001 From: "Yadong (Adam) Zhang" Date: Tue, 16 Sep 2025 22:31:24 +0800 Subject: [PATCH 11/13] docs: update outdated langchain usage. (#38616) * fix(apps/docs): update outdated langchain usage. * fix(apps/docs): update outdated langchain usage. --- apps/docs/content/guides/ai/langchain.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/docs/content/guides/ai/langchain.mdx b/apps/docs/content/guides/ai/langchain.mdx index 4315c88a0f1b3..a25168eb31c19 100644 --- a/apps/docs/content/guides/ai/langchain.mdx +++ b/apps/docs/content/guides/ai/langchain.mdx @@ -76,8 +76,8 @@ $$; You can now search your documents using any Node.js application. This is intended to be run on a secure server route. ```js -import { SupabaseVectorStore } from 'langchain/vectorstores/supabase' -import { OpenAIEmbeddings } from 'langchain/embeddings/openai' +import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase' +import { OpenAIEmbeddings } from '@langchain/openai' import { createClient } from '@supabase/supabase-js' const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY @@ -111,8 +111,8 @@ export const run = async () => { 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. ```js -import { SupabaseVectorStore } from 'langchain/vectorstores/supabase' -import { OpenAIEmbeddings } from 'langchain/embeddings/openai' +import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase' +import { OpenAIEmbeddings } from '@langchain/openai' import { createClient } from '@supabase/supabase-js' // First, follow set-up instructions above @@ -150,8 +150,8 @@ export const run = async () => { You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [PostgREST API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`). ```js -import { SupabaseFilterRPCCall, SupabaseVectorStore } from 'langchain/vectorstores/supabase' -import { OpenAIEmbeddings } from 'langchain/embeddings/openai' +import { SupabaseFilterRPCCall, SupabaseVectorStore } from '@langchain/community/vectorstores/supabase' +import { OpenAIEmbeddings } from '@langchain/openai' import { createClient } from '@supabase/supabase-js' // First, follow set-up instructions above From 87ab63a1d7f84745ed24a2d94ef0ed4daf186a6d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 16 Sep 2025 22:58:41 +0800 Subject: [PATCH 12/13] Chore/optimize auth users UI query (#38739) * only select required columns, instead of * * Use CTE, run agg only within the results block --- apps/studio/data/auth/users-infinite-query.ts | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/apps/studio/data/auth/users-infinite-query.ts b/apps/studio/data/auth/users-infinite-query.ts index f64d68f7711a4..8fbf6d50ba079 100644 --- a/apps/studio/data/auth/users-infinite-query.ts +++ b/apps/studio/data/auth/users-infinite-query.ts @@ -43,9 +43,6 @@ export const getUsersSQL = ({ const hasValidKeywords = keywords && keywords !== '' const conditions: string[] = [] - const baseQueryUsers = ` - select *, coalesce((select array_agg(distinct i.provider) from auth.identities i where i.user_id = auth.users.id), '{}'::text[]) as providers from auth.users - `.trim() if (hasValidKeywords) { // [Joshen] Escape single quotes properly @@ -81,7 +78,52 @@ export const getUsersSQL = ({ const sortOn = sort ?? 'created_at' const sortOrder = order ?? 'desc' - return `${baseQueryUsers}${conditions.length > 0 ? ` where ${combinedConditions}` : ''} order by "${sortOn}" ${sortOrder} nulls last limit ${USERS_PAGE_LIMIT} offset ${offset};` + const usersQuery = ` +with + users_data as ( + select + id, + email, + banned_until, + created_at, + confirmed_at, + confirmation_sent_at, + is_anonymous, + is_sso_user, + invited_at, + last_sign_in_at, + phone, + raw_app_meta_data, + raw_user_meta_data, + updated_at + from + auth.users + ${conditions.length > 0 ? ` where ${combinedConditions}` : ''} + order by + "${sortOn}" ${sortOrder} nulls last + limit + ${USERS_PAGE_LIMIT} + offset + ${offset} + ) +select + *, + coalesce( + ( + select + array_agg(distinct i.provider) + from + auth.identities i + where + i.user_id = users_data.id + ), + '{}'::text[] + ) as providers +from + users_data; + `.trim() + + return usersQuery } export type UsersData = { result: User[] } From 757332fec85548b704f415baf42247b8d8ad2007 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Wed, 17 Sep 2025 01:51:58 +1000 Subject: [PATCH 13/13] docs: add default environment variables available in hosted enviroment (#38666) --- apps/docs/content/guides/functions/secrets.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index d7b556e4047f9..9fd3aadc30657 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -14,6 +14,12 @@ Edge Functions have access to these secrets by default: - `SUPABASE_SERVICE_ROLE_KEY`: The `service_role` key for your Supabase API. This is safe to use in Edge Functions, but it should NEVER be used in a browser. This key will bypass Row Level Security - `SUPABASE_DB_URL`: The URL for your Postgres database. You can use this to connect directly to your database +In a hosted environment, functions have access to the following environment variables: + +- `SB_REGION`: The region function was invoked +- `SB_EXECUTION_ID`: A UUID of function instance ([isolate](/docs/guides/functions/architecture#4-execution-mechanics-fast-and-isolated)) +- `DENO_DEPLOYMENT_ID`: Version of the function code (`{project_ref}_{function_id}_{version}`) + --- ## Accessing environment variables