diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index cf3ecc38199..be170f07d38 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -83,18 +83,30 @@ jobs: if: steps.filter.outputs.studio == 'true' run: rm -rf supabase && pnpm exec supabase init && mkdir supabase/functions + - name: Pre-start diagnostics + run: | + docker ps -a + sudo ss -tlnp | grep 5432 || echo "5432 free" + - name: Start supabase if: steps.filter.outputs.studio == 'true' run: SKIP_ASSET_UPLOAD=1 pnpm run e2e:setup:cli + - name: Failure diagnostics + if: failure() + run: | + docker ps -a + sudo ss -tlnp | grep 5432 + docker logs $(docker ps -aq) 2>&1 || true + - name: Build studio if: steps.filter.outputs.studio == 'true' run: SKIP_ASSET_UPLOAD=1 NODE_ENV=test NODE_OPTIONS="--max-old-space-size=4096" pnpm run build:studio - - name: 🚀 Run Playwright tests against Vercel Preview + - name: 🚀 Run Playwright tests against local studio build if: steps.filter.outputs.studio == 'true' id: playwright - run: pnpm e2e --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: PWTEST_SHARD_WEIGHTS=62:38 pnpm e2e --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report to GitHub Actions Artifacts if: always() && steps.filter.outputs.studio == 'true' diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 5b69704eecc..b442e4c0ecc 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -126,6 +126,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "admonition-description-only": { + name: "admonition-description-only", + type: "components:example", + registryDependencies: ["admonition"], + component: React.lazy(() => import("@/registry/default/example/admonition-description-only")), + source: "", + files: ["registry/default/example/admonition-description-only.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "admonition-warning": { name: "admonition-warning", type: "components:example", @@ -137,6 +148,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "admonition-success": { + name: "admonition-success", + type: "components:example", + registryDependencies: ["admonition"], + component: React.lazy(() => import("@/registry/default/example/admonition-success")), + source: "", + files: ["registry/default/example/admonition-success.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "admonition-destructive": { name: "admonition-destructive", type: "components:example", diff --git a/apps/design-system/app/Providers.tsx b/apps/design-system/app/Providers.tsx index a7fe031c76b..46dce708544 100644 --- a/apps/design-system/app/Providers.tsx +++ b/apps/design-system/app/Providers.tsx @@ -1,20 +1,19 @@ 'use client' +import { ThemeProvider } from 'common' import { Provider as JotaiProvider } from 'jotai' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { ThemeProviderProps } from 'next-themes/dist/types' import { TooltipProvider } from 'ui' import { MobileSidebarProvider } from '@/context/mobile-sidebar-context' -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { +export function Providers({ children }: { children: React.ReactNode }) { return ( - + {children} - + ) } diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 93fa102345d..69176828eb9 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -4,7 +4,7 @@ import '@/styles/globals.css' import type { Metadata, Viewport } from 'next' import { customFont, sourceCodePro } from './fonts' -import { ThemeProvider } from './Providers' +import { Providers } from './Providers' import { Toaster } from './toaster' const className = `${customFont.variable} ${sourceCodePro.variable}` @@ -131,16 +131,12 @@ export default async function Layout({ children }: RootLayoutProps) { /> - +
{children}
-
+ ) diff --git a/apps/design-system/content/docs/fragments/admonition.mdx b/apps/design-system/content/docs/fragments/admonition.mdx index d9c0e89a4a9..c0adf6a7803 100644 --- a/apps/design-system/content/docs/fragments/admonition.mdx +++ b/apps/design-system/content/docs/fragments/admonition.mdx @@ -15,6 +15,12 @@ Admonition provides focus for situations that require a callout. Use Admonition instead of [Alert](../components/alert) unless you specifically need the primitives. If in doubt, stick with Admonition. +Use `title` for the heading slot when the callout needs its own heading. `title` is optional: short callouts may use `description` without a title when nearby content already provides enough context. + +Avoid title-only Admonitions in new code. A callout with `title` should also include `description` or `children` so it does not read like an incomplete heading. + +`label` is still supported for older Docs content, but new code should use `title`. + ## Usage ```tsx @@ -72,6 +78,24 @@ There are several components that wrap the `warning` Admonition type with reusab AlertError for example rolls up consistent error handling and support contact methods. +### Description only + +Use description-only Admonitions for short callouts that sit near a heading or form label with enough context. + + + +### Success + +Use `success` for positive, completed states where the user does not need to take corrective action. + + + ### Destructive + ) +} diff --git a/apps/design-system/registry/default/example/admonition-success.tsx b/apps/design-system/registry/default/example/admonition-success.tsx new file mode 100644 index 00000000000..70c8d25a2d5 --- /dev/null +++ b/apps/design-system/registry/default/example/admonition-success.tsx @@ -0,0 +1,11 @@ +import { Admonition } from 'ui-patterns/admonition' + +export default function AdmonitionSuccess() { + return ( + + ) +} diff --git a/apps/design-system/registry/default/example/data-table-demo.tsx b/apps/design-system/registry/default/example/data-table-demo.tsx index f96c42db7ab..101cc81af01 100644 --- a/apps/design-system/registry/default/example/data-table-demo.tsx +++ b/apps/design-system/registry/default/example/data-table-demo.tsx @@ -24,7 +24,7 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - Input, + Input_Shadcn_ as Input, Table, TableBody, TableCell, @@ -196,7 +196,7 @@ export default function DataTableDemo() { placeholder="Filter by email" value={(table.getColumn('email')?.getFilterValue() as string) ?? ''} onChange={(event) => table.getColumn('email')?.setFilterValue(event.target.value)} - className="max-w-sm" + className="max-w-xs" /> diff --git a/apps/design-system/registry/default/example/field-demo.tsx b/apps/design-system/registry/default/example/field-demo.tsx index 09fe7a26a82..5f8f48dfda2 100644 --- a/apps/design-system/registry/default/example/field-demo.tsx +++ b/apps/design-system/registry/default/example/field-demo.tsx @@ -1,4 +1,4 @@ -import { Button, Checkbox, Input, Textarea } from 'ui' +import { Button, Checkbox, Input_Shadcn_ as Input, Textarea } from 'ui' import { Field, FieldDescription, diff --git a/apps/design-system/registry/default/example/field-fieldset.tsx b/apps/design-system/registry/default/example/field-fieldset.tsx index a91c1d4d86e..1b5f996c8ba 100644 --- a/apps/design-system/registry/default/example/field-fieldset.tsx +++ b/apps/design-system/registry/default/example/field-fieldset.tsx @@ -1,4 +1,4 @@ -import { Input } from 'ui' +import { Input_Shadcn_ as Input } from 'ui' import { Field, FieldDescription, diff --git a/apps/design-system/registry/default/example/field-input.tsx b/apps/design-system/registry/default/example/field-input.tsx index 17eee8e9b2b..56089935bf9 100644 --- a/apps/design-system/registry/default/example/field-input.tsx +++ b/apps/design-system/registry/default/example/field-input.tsx @@ -1,4 +1,4 @@ -import { Input } from 'ui' +import { Input_Shadcn_ as Input } from 'ui' import { Field, FieldDescription, diff --git a/apps/design-system/registry/default/example/field-responsive.tsx b/apps/design-system/registry/default/example/field-responsive.tsx index 1f00b1a2835..96ea6b17989 100644 --- a/apps/design-system/registry/default/example/field-responsive.tsx +++ b/apps/design-system/registry/default/example/field-responsive.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Textarea } from 'ui' +import { Button, Input_Shadcn_ as Input, Textarea } from 'ui' import { Field, FieldContent, diff --git a/apps/design-system/registry/default/example/input-form.tsx b/apps/design-system/registry/default/example/input-form.tsx index d0d229600d6..537d632c9f8 100644 --- a/apps/design-system/registry/default/example/input-form.tsx +++ b/apps/design-system/registry/default/example/input-form.tsx @@ -12,7 +12,7 @@ import { FormItem, FormLabel, FormMessage, - Input, + Input_Shadcn_ as Input, } from 'ui' import { z } from 'zod' diff --git a/apps/design-system/registry/default/example/page-layout-list.tsx b/apps/design-system/registry/default/example/page-layout-list.tsx index f9448ebb9e4..0d0fd3c480c 100644 --- a/apps/design-system/registry/default/example/page-layout-list.tsx +++ b/apps/design-system/registry/default/example/page-layout-list.tsx @@ -8,7 +8,9 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - Input, + InputGroup, + InputGroupAddon, + InputGroupInput, Table, TableBody, TableCell, @@ -68,12 +70,16 @@ export default function PageLayoutList() {
- } - className="w-full lg:w-52" - /> + + + + + +
diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 9f04225b28b..d5f9a19ae22 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -25,12 +25,24 @@ export const examples: Registry = [ registryDependencies: ['admonition'], files: ['example/admonition-button.tsx'], }, + { + name: 'admonition-description-only', + type: 'components:example', + registryDependencies: ['admonition'], + files: ['example/admonition-description-only.tsx'], + }, { name: 'admonition-warning', type: 'components:example', registryDependencies: ['admonition'], files: ['example/admonition-warning.tsx'], }, + { + name: 'admonition-success', + type: 'components:example', + registryDependencies: ['admonition'], + files: ['example/admonition-success.tsx'], + }, { name: 'admonition-destructive', type: 'components:example', diff --git a/apps/design-system/styles/globals.css b/apps/design-system/styles/globals.css index 0160d13280c..0418cb7fedf 100644 --- a/apps/design-system/styles/globals.css +++ b/apps/design-system/styles/globals.css @@ -1,14 +1,19 @@ -@import 'tailwindcss'; - -@import './../../../packages/ui/build/css/source/global.css'; -@import './../../../packages/ui/build/css/themes/dark.css'; +@import 'config/tailwind.config.css'; @import './../../../packages/ui/build/css/themes/classic-dark.css'; -@import './../../../packages/ui/build/css/themes/light.css'; - -@config '../tailwind.config.js'; - @import 'config/typography.css'; +@source '../app/**/*.{js,ts,jsx,tsx}'; +@source '../components/**/*.{js,ts,jsx,tsx}'; +@source '../registry/**/*.{js,ts,jsx,tsx}'; +@source '../content/**/*.mdx'; +@source './../../../packages/ui/src/**/*.{tsx,ts,js}'; +@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}'; + +@theme { + /* added to get max-w-site */ + --container-site: 128rem; +} + @layer base { :root { --chart-1: 12 76% 61%; diff --git a/apps/design-system/tailwind.config.js b/apps/design-system/tailwind.config.js deleted file mode 100644 index e329c8e381e..00000000000 --- a/apps/design-system/tailwind.config.js +++ /dev/null @@ -1,21 +0,0 @@ -const config = require('config/tailwind.config') - -module.exports = config({ - content: [ - './app/**/*.{js,ts,jsx,tsx}', - './components/**/*.{js,ts,jsx,tsx}', - './registry/**/*.{js,ts,jsx,tsx}', - './content/**/*.mdx', - // purge styles from grid library - // - './../../packages/ui/src/**/*.{tsx,ts,js}', - './../../packages/ui-patterns/src/**/*.{tsx,ts,js}', - ], - theme: { - extend: { - maxWidth: { - site: '128rem', - }, - }, - }, -}) diff --git a/apps/docs/app/api/guides-md/[...slug]/route.ts b/apps/docs/app/api/guides-md/[...slug]/route.ts index b942ff2f73c..3e877b3181f 100644 --- a/apps/docs/app/api/guides-md/[...slug]/route.ts +++ b/apps/docs/app/api/guides-md/[...slug]/route.ts @@ -1,14 +1,15 @@ -import { promises as fs } from 'fs' -import path from 'path' +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { BASE_PATH, IS_PRODUCTION, PROD_URL } from '~/lib/constants' import { NextResponse } from 'next/server' -export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) { +export async function GET(request: Request, { params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params const baseDir = path.join(process.cwd(), 'public/docs/guides') const filePath = path.join(baseDir, `${slug.join('/')}.md`) if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) { - return new NextResponse('Not found', { status: 404 }) + return notFoundResponse(request, slug) } try { @@ -20,6 +21,86 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu }, }) } catch { - return new NextResponse('Not found', { status: 404 }) + return notFoundResponse(request, slug) } } + +/** + * Resolves the docs base URL from trusted sources only. + * Avoids host-header-driven SSRF from request origin. + */ +function resolveDocsBaseUrl() { + if (IS_PRODUCTION) return PROD_URL + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}${BASE_PATH}` + if (process.env.NEXT_PUBLIC_SITE_URL) return `${process.env.NEXT_PUBLIC_SITE_URL}${BASE_PATH}` + return undefined +} + +async function notFoundResponse(_request: Request, slug: string[]) { + const baseUrl = resolveDocsBaseUrl() + const query = slug.join(' ').replace(/[_-]/g, ' ') + const suggestions = baseUrl ? await searchForSuggestions(baseUrl, query) : [] + const markdown = buildNotFoundMarkdown(slug, suggestions) + return new NextResponse(markdown, { + status: 404, + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Cache-Control': 'no-store', + }, + }) +} + +interface SearchSuggestion { + title: string + href: string +} + +async function searchForSuggestions(baseUrl: string, query: string): Promise { + try { + const response = await fetch(`${baseUrl}/api/graphql`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: ` + query SearchDocs($query: String!) { + searchDocs(query: $query, limit: 5) { + nodes { + title + href + } + } + } + `, + variables: { query }, + }), + }) + + if (!response.ok) return [] + + const data = await response.json() + return (data?.data?.searchDocs?.nodes ?? []).filter( + (n: Partial): n is SearchSuggestion => !!n.title && !!n.href + ) + } catch { + return [] + } +} + +function buildNotFoundMarkdown(slug: string[], suggestions: SearchSuggestion[]): string { + const pagePath = slug.join('/') + + const suggestionsMd = + suggestions.length > 0 + ? `## You might be looking for...\n\n${suggestions + .map(({ title, href }) => `- [${title}](${href}.md)`) + .join('\n')}\n` + : '' + + return `# 404 - Page Not Found + +The page \`/docs/guides/${pagePath}.md\` does not exist. + +${suggestionsMd} +See also: [Changelog](https://supabase.com/changelog.md) +` +} diff --git a/apps/docs/app/guides/ai/[[...slug]]/page.tsx b/apps/docs/app/guides/ai/[[...slug]]/page.tsx index f517756dd71..c17bd51fb12 100644 --- a/apps/docs/app/guides/ai/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/ai/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const AiGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('ai') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('ai') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['ai', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/api/[[...slug]]/page.tsx b/apps/docs/app/guides/api/[[...slug]]/page.tsx index f68aadb62e4..1ec826244b8 100644 --- a/apps/docs/app/guides/api/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/api/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const ApiGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('api') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('api') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['api', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/auth/[[...slug]]/page.tsx b/apps/docs/app/guides/auth/[[...slug]]/page.tsx index 49c670824a4..f4c062505be 100644 --- a/apps/docs/app/guides/auth/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/auth/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const AuthGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('auth') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('auth') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['auth', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/cron/[[...slug]]/page.tsx b/apps/docs/app/guides/cron/[[...slug]]/page.tsx index e77e622d824..3c40eef45c9 100644 --- a/apps/docs/app/guides/cron/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/cron/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const CronGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('cron') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('cron') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['cron', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/database/[[...slug]]/page.tsx b/apps/docs/app/guides/database/[[...slug]]/page.tsx index 8b6622d22fc..124f0b315a9 100644 --- a/apps/docs/app/guides/database/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const DatabaseGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('database') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('database') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['database', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/database/database-advisors/page.tsx b/apps/docs/app/guides/database/database-advisors/page.tsx index 889dff9b749..e35e9da410c 100644 --- a/apps/docs/app/guides/database/database-advisors/page.tsx +++ b/apps/docs/app/guides/database/database-advisors/page.tsx @@ -1,19 +1,18 @@ +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' +import { MDXRemoteBase } from '~/features/docs/MdxBase' +import { TabPanel, Tabs } from '~/features/ui/Tabs' +import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { getGitHubFileContents, octokit, OCTOKIT_RETRY_OPTIONS } from '~/lib/octokit' +import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { capitalize } from 'lodash-es' import rehypeSlug from 'rehype-slug' import { Heading } from 'ui' import { Admonition } from 'ui-patterns' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' -import { MDXRemoteBase } from '~/features/docs/MdxBase' -import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit' -import { TabPanel, Tabs } from '~/features/ui/Tabs' -import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' -import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' -import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' -import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { SerializeOptions } from '~/types/next-mdx-remote-serialize' - // We fetch these docs at build time from an external repo const org = 'supabase' const repo = 'splinter' @@ -64,7 +63,7 @@ const DatabaseAdvisorDocs = async () => { } as SerializeOptions return ( - + Available checks diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index ddce4af8b82..aa0e0fd0c15 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -1,15 +1,5 @@ -import matter from 'gray-matter' -import Link from 'next/link' -import { notFound } from 'next/navigation' import { readFile } from 'node:fs/promises' import { join, relative } from 'node:path' -import rehypeSlug from 'rehype-slug' -import emoji from 'remark-emoji' -// End of third-party imports - -import { IS_PROD, isFeatureEnabled } from 'common' -import { Button } from 'ui' -import { Admonition } from 'ui-patterns' import { genGuideMeta, genGuidesStaticParams, @@ -17,6 +7,9 @@ import { } from '~/features/docs/GuidesMdx.utils' import { newEditLink } from '~/features/helpers.edit-link' import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide' +// End of third-party imports + +import { IS_DEV } from '~/lib/constants' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' import { linkTransform, type UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' @@ -24,6 +17,14 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents, octokit } from '~/lib/octokit' import type { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import { isFeatureEnabled } from 'common' +import matter from 'gray-matter' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import rehypeSlug from 'rehype-slug' +import emoji from 'remark-emoji' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns' // We fetch these docs at build time from an external repo const org = 'supabase' @@ -424,7 +425,7 @@ const urlTransform: UrlTransformFunction = (url) => { } const generateStaticParams = async () => { - if (IS_PROD) { + if (!IS_DEV) { return [] } diff --git a/apps/docs/app/guides/deployment/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/[[...slug]]/page.tsx index 7b8be32adcc..0b15eb80b1f 100644 --- a/apps/docs/app/guides/deployment/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const DeploymentGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('deployment') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('deployment') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['deployment', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index c97957170c2..07c76c8da40 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -1,24 +1,24 @@ +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' +import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' +import { isValidGuideFrontmatter } from '~/lib/docs' +import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { getGitHubFileContents } from '~/lib/octokit' +import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import matter from 'gray-matter' import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { getGitHubFileContents } from '~/lib/octokit' -import { isValidGuideFrontmatter } from '~/lib/docs' -import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' -import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' -import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' -import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { terraformDocsBranch, terraformDocsDocsDir, terraformDocsOrg, terraformDocsRepo, } from '../terraformConstants' -import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import { IS_PROD } from 'common' -import { getEmptyArray } from '~/features/helpers.fn' // Each external docs page is mapped to a local page const pageMap = [ @@ -143,7 +143,7 @@ const getContent = async ({ slug }: Params) => { } } -const generateStaticParams = IS_PROD +const generateStaticParams = !IS_DEV ? async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] })) : getEmptyArray const generateMetadata = genGuideMeta(getContent) diff --git a/apps/docs/app/guides/deployment/terraform/reference/page.tsx b/apps/docs/app/guides/deployment/terraform/reference/page.tsx index 93029a2c0d1..220d551479a 100644 --- a/apps/docs/app/guides/deployment/terraform/reference/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/reference/page.tsx @@ -352,7 +352,11 @@ const TerraformReferencePage = async () => { const editLink = newEditLink('supabase/terraform-provider-supabase') return ( - + The Terraform Provider provides access to{' '} }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('functions') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('functions') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['functions', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx b/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx index 52e9425c92f..a3c33a9a546 100644 --- a/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/getting-started/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const GettingStartedGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('getting-started') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('getting-started') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['getting-started', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx index 82f3837575d..1213cb07a74 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/page.tsx @@ -1,6 +1,7 @@ +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { source } from 'common-tags' import { notFound } from 'next/navigation' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' + import { generateAiPromptMetadata, generateAiPromptsStaticParams, @@ -50,6 +51,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str meta={{ title: `AI Prompt: ${heading}` }} content={content} editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)} + pathname={`/guides/getting-started/ai-prompts/${slug}`} /> ) } diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index 5368816194c..eb0c01b48f5 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -1,17 +1,16 @@ -import { notFound } from 'next/navigation' import { isAbsolute, relative } from 'path' -import rehypeSlug from 'rehype-slug' - import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' -import { getGitHubFileContents } from '~/lib/octokit' -import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' +import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import { IS_PROD } from 'common' -import { getEmptyArray } from '~/features/helpers.fn' +import { notFound } from 'next/navigation' +import rehypeSlug from 'rehype-slug' // We fetch these docs at build time from an external repo const org = 'supabase' @@ -190,7 +189,7 @@ const urlTransform: UrlTransformFunction = (url) => { } } -const generateStaticParams = IS_PROD +const generateStaticParams = !IS_DEV ? async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] })) : getEmptyArray const generateMetadata = genGuideMeta(getContent) diff --git a/apps/docs/app/guides/integrations/[[...slug]]/page.tsx b/apps/docs/app/guides/integrations/[[...slug]]/page.tsx index 819c1e0bfd7..cc02780dc73 100644 --- a/apps/docs/app/guides/integrations/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/integrations/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const IntegrationsGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('integrations') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('integrations') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['integrations', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/local-development/[[...slug]]/page.tsx b/apps/docs/app/guides/local-development/[[...slug]]/page.tsx index 4166055f268..81dcba599a6 100644 --- a/apps/docs/app/guides/local-development/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/local-development/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const LocalDevelopmentGuidePage = async (props: { params: Promise }) => return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('local-development') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('local-development') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['local-development', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/local-development/cli/config/page.tsx b/apps/docs/app/guides/local-development/cli/config/page.tsx index 75a28a947e0..124ade3a74c 100644 --- a/apps/docs/app/guides/local-development/cli/config/page.tsx +++ b/apps/docs/app/guides/local-development/cli/config/page.tsx @@ -36,7 +36,7 @@ const Config = () => { const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml') return ( - + {specFile.info.description}
{content}
diff --git a/apps/docs/app/guides/platform/[[...slug]]/page.tsx b/apps/docs/app/guides/platform/[[...slug]]/page.tsx index 7ca75717816..dce62f3cf79 100644 --- a/apps/docs/app/guides/platform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/platform/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const PlatformGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('platform') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('platform') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['platform', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/queues/[[...slug]]/page.tsx b/apps/docs/app/guides/queues/[[...slug]]/page.tsx index 991c81c189b..fa3852979b4 100644 --- a/apps/docs/app/guides/queues/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/queues/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const QueuesGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('queues') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('queues') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['queues', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx index c65eb2ae994..8b9f7bbbce4 100644 --- a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx @@ -1,4 +1,3 @@ -import { IS_PROD } from 'common' import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, @@ -6,6 +5,7 @@ import { getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const RealtimeGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('realtime') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('realtime') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['realtime', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/resources/[[...slug]]/page.tsx b/apps/docs/app/guides/resources/[[...slug]]/page.tsx index c4ef4796b42..d32333d1b97 100644 --- a/apps/docs/app/guides/resources/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/resources/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const ResourcesGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('resources') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('resources') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['resources', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/security/[[...slug]]/page.tsx b/apps/docs/app/guides/security/[[...slug]]/page.tsx index 4a62d58281d..ce65389dc74 100644 --- a/apps/docs/app/guides/security/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/security/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const SecurityGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('security') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('security') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['security', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx b/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx index 39f7d03b452..9d17d547ad6 100644 --- a/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/self-hosting/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const SelfHostingGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('self-hosting') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('self-hosting') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['self-hosting', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/self-hosting/analytics/config/page.tsx b/apps/docs/app/guides/self-hosting/analytics/config/page.tsx index 5874b2b40dd..2c28fa2bf44 100644 --- a/apps/docs/app/guides/self-hosting/analytics/config/page.tsx +++ b/apps/docs/app/guides/self-hosting/analytics/config/page.tsx @@ -1,6 +1,6 @@ import Param from '~/components/Params' -import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { MDXRemoteBase } from '~/features/docs/MdxBase' import specAnalyticsV0 from '~/spec/analytics_v0_config.yaml' with { type: 'yml' } @@ -23,6 +23,7 @@ const AnalyticsConfigPage = async () => { editLink={newEditLink( 'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/analytics/config/page.tsx' )} + pathname="/guides/self-hosting/analytics/config" > diff --git a/apps/docs/app/guides/self-hosting/auth/config/page.tsx b/apps/docs/app/guides/self-hosting/auth/config/page.tsx index 4ba779c8304..4221f2f7f42 100644 --- a/apps/docs/app/guides/self-hosting/auth/config/page.tsx +++ b/apps/docs/app/guides/self-hosting/auth/config/page.tsx @@ -1,6 +1,6 @@ import Param from '~/components/Params' -import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { MDXRemoteBase } from '~/features/docs/MdxBase' import specAuthV1 from '~/spec/gotrue_v1_config.yaml' with { type: 'yml' } @@ -23,6 +23,7 @@ const AuthConfigPage = async () => { editLink={newEditLink( 'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/auth/config/page.tsx' )} + pathname="/guides/self-hosting/auth/config" > diff --git a/apps/docs/app/guides/self-hosting/realtime/config/page.tsx b/apps/docs/app/guides/self-hosting/realtime/config/page.tsx index 6d3ce48ccaf..80076742be8 100644 --- a/apps/docs/app/guides/self-hosting/realtime/config/page.tsx +++ b/apps/docs/app/guides/self-hosting/realtime/config/page.tsx @@ -1,6 +1,6 @@ import Param from '~/components/Params' -import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { MDXRemoteBase } from '~/features/docs/MdxBase' import specRealtimeV0 from '~/spec/realtime_v0_config.yaml' with { type: 'yml' } @@ -23,6 +23,7 @@ const RealtimeConfigPage = async () => { editLink={newEditLink( 'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/realtime/config/page.tsx' )} + pathname="/guides/self-hosting/realtime/config" > diff --git a/apps/docs/app/guides/self-hosting/storage/config/page.tsx b/apps/docs/app/guides/self-hosting/storage/config/page.tsx index 88c71034cd9..c6682ce7add 100644 --- a/apps/docs/app/guides/self-hosting/storage/config/page.tsx +++ b/apps/docs/app/guides/self-hosting/storage/config/page.tsx @@ -1,6 +1,6 @@ import Param from '~/components/Params' -import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { MDXRemoteBase } from '~/features/docs/MdxBase' import specStorageV0 from '~/spec/storage_v0_config.yaml' with { type: 'yml' } @@ -23,6 +23,7 @@ const StorageConfigPage = async () => { editLink={newEditLink( 'supabase/supabase/blob/master/apps/docs/app/guides/self-hosting/storage/config/page.tsx' )} + pathname="/guides/self-hosting/storage/config" > diff --git a/apps/docs/app/guides/storage/[[...slug]]/page.tsx b/apps/docs/app/guides/storage/[[...slug]]/page.tsx index 67ad5d42301..6f01587f27f 100644 --- a/apps/docs/app/guides/storage/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/storage/[[...slug]]/page.tsx @@ -1,4 +1,3 @@ -import { IS_PROD } from 'common' import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, @@ -6,6 +5,7 @@ import { getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const StorageGuidePage = async (props: { params: Promise }) => { return } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('storage') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('storage') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['storage', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx b/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx index 26be534bde8..0aa9feceb0d 100644 --- a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx @@ -1,11 +1,11 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' -import { IS_PROD } from 'common' import { getEmptyArray } from '~/features/helpers.fn' +import { IS_DEV } from '~/lib/constants' type Params = { slug?: string[] } @@ -17,7 +17,7 @@ const MonitoringTroubleshootingGuidePage = async (props: { params: Promise } -const generateStaticParams = IS_PROD ? genGuidesStaticParams('telemetry') : getEmptyArray +const generateStaticParams = !IS_DEV ? genGuidesStaticParams('telemetry') : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['telemetry', ...(params.slug ?? [])]) ) diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index d5bad10e70d..aea0eb429ee 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,7 +1,7 @@ import '@code-hike/mdx/styles.css' import 'config/code-hike.css' import 'ui-patterns/ShimmeringLoader/index.css' -import '../styles/main.css' +import '../styles/globals.css' import '../styles/prism-okaidia.css' import { GlobalProviders } from '~/features/app.providers' diff --git a/apps/docs/components/AppleSecretGenerator/AppleSecretGenerator.tsx b/apps/docs/components/AppleSecretGenerator/AppleSecretGenerator.tsx index 04936737eba..aeb45ed4bb8 100644 --- a/apps/docs/components/AppleSecretGenerator/AppleSecretGenerator.tsx +++ b/apps/docs/components/AppleSecretGenerator/AppleSecretGenerator.tsx @@ -1,6 +1,8 @@ import { useState } from 'react' -import { Button, Input } from 'ui' +import { Button, Input_Shadcn_ as Input } from 'ui' import { Admonition } from 'ui-patterns/admonition' +import { Input as DataInput } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' function base64URL(value: string) { return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_') @@ -105,30 +107,45 @@ const AppleSecretGenerator = () => { return (
- setTeamID(e.target.value.trim())} - /> - + setTeamID(e.target.value.trim())} + /> + + setServiceID(e.target.value.trim())} - /> - + setServiceID(e.target.value.trim())} + /> + + setKeyID(e.target.value.trim())} - /> + > + setKeyID(e.target.value.trim())} + /> +
{ {secretKey && ( <>
- + description={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`} + > + + )}
diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx index b9bde64e769..3e524100491 100644 --- a/apps/docs/components/Breadcrumbs.tsx +++ b/apps/docs/components/Breadcrumbs.tsx @@ -1,18 +1,18 @@ 'use client' +import { resolveBreadcrumbs } from '~/lib/breadcrumbs' +import { useBreakpoint } from 'common' import Link from 'next/link' import { usePathname, useSearchParams } from 'next/navigation' import React, { Fragment, Suspense } from 'react' - -import { useBreakpoint } from 'common' import { Breadcrumb_Shadcn_ as Breadcrumb, - BreadcrumbList_Shadcn_ as BreadcrumbList, + BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis, BreadcrumbItem_Shadcn_ as BreadcrumbItem, BreadcrumbLink_Shadcn_ as BreadcrumbLink, - BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator, + BreadcrumbList_Shadcn_ as BreadcrumbList, BreadcrumbPage_Shadcn_ as BreadcrumbPage, - BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis, + BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator, Button, cn, Drawer, @@ -26,9 +26,6 @@ import { DropdownMenuTrigger, } from 'ui' -import * as NavItems from '~/components/Navigation/NavigationMenu/NavigationMenu.constants' -import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils' - interface BreadcrumbsProps extends React.HTMLAttributes { minLength?: number forceDisplayOnMobile?: boolean @@ -159,56 +156,5 @@ const BreadcrumbsInternal = ({ function useBreadcrumbs() { const pathname = usePathname() - - const isTroubleshootingPage = pathname.startsWith('/guides/troubleshooting') - if (isTroubleshootingPage) { - const breadcrumbs = [{ name: 'Troubleshooting', url: '/guides/troubleshooting' }] - return breadcrumbs - } - - const isAiPromptsPage = pathname.startsWith('/guides/getting-started/ai-prompts') - if (isAiPromptsPage) { - const breadcrumbs = [ - { name: 'Getting started', url: '/guides/getting-started' }, - { name: 'AI Tools' }, - { name: 'Prompts', url: '/guides/getting-started/ai-prompts' }, - ] - return breadcrumbs - } - - // TODO: Breadcrumbs currently can't infer the "AI Tools" parent for /guides/getting-started/ai-* routes, - // so we special-case these paths here. Remove when Breadcrumbs can derive this hierarchy from NavigationMenu. - const isAiSkillsPage = pathname.startsWith('/guides/getting-started/ai-skills') - if (isAiSkillsPage) { - const breadcrumbs = [ - { name: 'Getting started', url: '/guides/getting-started' }, - { name: 'AI Tools' }, - { name: 'Agent Skills', url: '/guides/getting-started/ai-skills' }, - ] - return breadcrumbs - } - - const menuId = getMenuId(pathname) - const menu = NavItems[menuId] - return findMenuItemByUrl(menu, pathname, []) -} - -function findMenuItemByUrl(menu: any, targetUrl: string, parents: any[] = []) { - // If the menu has items, recursively search through them - if (menu.items) { - for (let item of menu.items) { - const result = findMenuItemByUrl(item, targetUrl, [...parents, menu]) - if (result) { - return result - } - } - } - - // Check if the current menu object itself has the target URL - if (menu.url === targetUrl) { - return [...parents, menu] - } - - // If the URL is not found, return null - return null + return resolveBreadcrumbs(pathname) } diff --git a/apps/docs/components/Extensions/Extensions.tsx b/apps/docs/components/Extensions/Extensions.tsx index c9a3c201d5e..2d09841d758 100644 --- a/apps/docs/components/Extensions/Extensions.tsx +++ b/apps/docs/components/Extensions/Extensions.tsx @@ -2,7 +2,7 @@ import { X } from 'lucide-react' import Link from 'next/link' import React, { useState } from 'react' import { extensions } from 'shared-data' -import { Badge, Input } from 'ui' +import { Badge, Input_Shadcn_ as Input } from 'ui' import { GlassPanel } from 'ui-patterns/GlassPanel' type Extension = { diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 93f642501bc..92ba17cb22d 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -1,13 +1,13 @@ 'use client' -import { ChevronRight, Play, Sparkles } from 'lucide-react' -import Link from 'next/link' -import { useTheme } from 'next-themes' // End of third-party imports - import { isFeatureEnabled, useBreakpoint } from 'common' +import { ChevronRight, Play, Sparkles } from 'lucide-react' +import { useTheme } from 'next-themes' +import Link from 'next/link' import { cn, IconBackground } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' + import { getCustomContent } from '../lib/custom-content/getCustomContent' import DocsCoverLogo from './DocsCoverLogo' @@ -41,7 +41,7 @@ const HomePageCover = (props) => { const iconSize = isXs ? 'sm' : 'lg' const { homepageHeading } = getCustomContent(['homepage:heading']) const { resolvedTheme } = useTheme() - const isLightMode = resolvedTheme !== 'dark' + const isLightMode = !resolvedTheme?.includes('dark') const frameworks = [ { diff --git a/apps/docs/components/Navigation/NavigationMenu/MenuIconPicker.tsx b/apps/docs/components/Navigation/NavigationMenu/MenuIconPicker.tsx index d9ed3451f40..8f3adf1ef41 100644 --- a/apps/docs/components/Navigation/NavigationMenu/MenuIconPicker.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/MenuIconPicker.tsx @@ -1,33 +1,33 @@ -import { Clock, Heart, Server, SquareStack, Telescope } from 'lucide-react' +import { Clock, Heart, ScrollText, Server, SquareStack, Telescope } from 'lucide-react' import { IconBranching, - IconGitHub, IconGitBranch, + IconGitHub, + IconMenuAI, IconMenuApi, IconMenuAuth, IconMenuCli, IconMenuCsharp, IconMenuDatabase, - IconMenuGraphQL, + IconMenuDevCli, IconMenuEdgeFunctions, IconMenuFlutter, IconMenuGettingStarted, + IconMenuGraphQL, IconMenuHome, IconMenuIntegrations, IconMenuJavascript, + IconMenuKotlin, IconMenuPlatform, IconMenuPython, IconMenuRealtime, IconMenuResources, - IconMenuSelfHosting, IconMenuRestApis, + IconMenuSelfHosting, + IconMenuStatus, IconMenuStorage, IconMenuSwift, - IconMenuStatus, - IconMenuKotlin, - IconMenuAI, - IconMenuDevCli, IconSecurity, IconSupport, IconTroubleshooting, @@ -98,6 +98,8 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, c return case 'troubleshooting': return + case 'changelog': + return case 'contributing': return case 'deployment': diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 8a3e380adda..bde417699ef 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -500,6 +500,10 @@ export const gettingstarted: NavMenuConstant = { name: 'AI Tools', url: undefined, items: [ + { + name: 'Supabase Agent Plugin', + url: '/guides/getting-started/plugins' as `/${string}`, + }, { name: 'Prompts', url: '/guides/getting-started/ai-prompts' as `/${string}`, diff --git a/apps/docs/content/_partials/api_settings.mdx b/apps/docs/content/_partials/api_settings.mdx index dca5688a908..1ff65a3332f 100644 --- a/apps/docs/content/_partials/api_settings.mdx +++ b/apps/docs/content/_partials/api_settings.mdx @@ -8,12 +8,13 @@ To do this, you need to get the Project URL and key from [the project **Connect* -Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys. +Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260). -**The legacy keys will be deprecated shortly, so we strongly encourage switching to and using the new publishable and secret API keys**. +The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now. -In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/): +In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard. -**For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. +- **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab. +- **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. diff --git a/apps/docs/content/_partials/api_settings_steps.mdx b/apps/docs/content/_partials/api_settings_steps.mdx index 84228d1cbd9..50d45feb465 100644 --- a/apps/docs/content/_partials/api_settings_steps.mdx +++ b/apps/docs/content/_partials/api_settings_steps.mdx @@ -1,7 +1,5 @@ {/* TODO: How to completely consolidate partials? */} -You can also get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}). - ### Get API details Now that you've created some database tables, you are ready to insert data using the auto-generated API. @@ -12,12 +10,13 @@ To do this, you need to get the Project URL and key from [the project **Connect* -Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys. +Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement on GitHub](https://github.com/orgs/supabase/discussions/29260). -**The legacy keys will be deprecated shortly, so we strongly encourage switching to and using the new publishable and secret API keys**. +The older `anon` and `service_role` keys will work until the end of 2026 but **we strongly encourage switching to and using** the new publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys now. -In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/): +In most cases, you can get keys from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard. -**For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. +- **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab. +- **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. diff --git a/apps/docs/content/_partials/auth_methods.mdx b/apps/docs/content/_partials/auth_methods.mdx new file mode 100644 index 00000000000..f8ab5ddbad6 --- /dev/null +++ b/apps/docs/content/_partials/auth_methods.mdx @@ -0,0 +1,9 @@ +The Supabase Auth SDK contains three different functions for authenticating user access to applications: + +### Summary of the methods + +- Use [`getClaims`](/docs/reference/javascript/auth-getclaims) to protect pages and user data. It reads the access token from storage and verifies it. Locally via the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) and a cached JWKS endpoint when the project uses asymmetric signing keys (the default for new projects), or by calling `getUser` solely to validate when symmetric keys are in use. The returned claims always come from decoding the JWT, not from a user lookup. +- [`getUser`](/docs/reference/javascript/auth-getuser) makes a network call to the project's Auth instance to get the user record, which includes the most up-to-date information about the user at the cost of a network call. +- [`getSession`](/docs/reference/javascript/auth-getsession) when you need the raw session (the access token, refresh token, and expiry). For example to forward the access token to another service. The session is loaded directly from local storage and isn't re-validated against the Auth server, so the embedded user object shouldn't be trusted on its own when storage is shared with the client (cookies, request headers). To verify identity, validate the access token with `getClaims`, or call `getUser` for a fresh, server-confirmed user record. + +**In summary**: use `getClaims` to verify identity (typically for protecting pages and data), `getUser` when you need an up-to-date user record from the Auth server, and `getSession` when you need the access or refresh token directly, but don't rely on the user object it returns for authorization decisions. diff --git a/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx b/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx index 6ded63808f5..11f0672f9d6 100644 --- a/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx +++ b/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx @@ -60,11 +60,12 @@ interface WebhookPayload { serve(async (req) => { const payload: WebhookPayload = await req.json() const soRecord = payload.record + const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabaseAdminClient = createClient( // Supabase API URL - env var exported by default when deployed. Deno.env.get('SUPABASE_URL') ?? '', - // Supabase API SERVICE ROLE KEY - env var exported by default when deployed. - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + // Supabase API SECRET KEY - env var exported by default when deployed. + Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? '' ) // Construct image url from storage diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index 0aa64754120..2c9e1e606b9 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -313,9 +313,14 @@ Depending on your application requirements, data conflicts can arise when an ano In some cases, you may need to link an anonymous user to an existing account rather than creating a new permanent account. This process requires manual handling of potential conflicts. Here's a general approach: ```javascript -// 1. Sign in anonymously (assuming the user is already signed in anonymously) +// 1. Get the current session and verify the user is anonymous const { data: anonData, error: anonError } = await supabase.auth.getSession() +if (!anonData.session?.user?.is_anonymous) { + console.log('User is not anonymous. This flow only applies to anonymous users.') + return +} + // 2. Attempt to update the user with the existing email const { data: updateData, error: updateError } = await supabase.auth.updateUser({ email: 'valid.email@supabase.io', diff --git a/apps/docs/content/guides/auth/auth-captcha.mdx b/apps/docs/content/guides/auth/auth-captcha.mdx index e735db59644..6b5e2cc358e 100644 --- a/apps/docs/content/guides/auth/auth-captcha.mdx +++ b/apps/docs/content/guides/auth/auth-captcha.mdx @@ -34,8 +34,11 @@ In the Settings page, look for the **Sitekey** section and copy the key. - Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the Welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below - ![cloudflare_settings.png](/docs/img/guides/auth-captcha/cloudflare_settings.png) + +Sign in to the [Cloudflare dashboard](https://dash.cloudflare.com/login) and create a Turnstile widget by following Cloudflare's [Create a widget](https://developers.cloudflare.com/turnstile/get-started/widget-management/dashboard/) guide. + +Once the widget is created, copy the **Sitekey** and **Secret Key** — you need them in the next steps. + diff --git a/apps/docs/content/guides/auth/custom-oauth-providers.mdx b/apps/docs/content/guides/auth/custom-oauth-providers.mdx index 4ecb6379cab..17d12760733 100644 --- a/apps/docs/content/guides/auth/custom-oauth-providers.mdx +++ b/apps/docs/content/guides/auth/custom-oauth-providers.mdx @@ -15,7 +15,7 @@ There are two provider types: -You can add up to 3 custom providers per project. If you need more, [contact support](/dashboard/support/new). +Free plan projects can add up to 3 custom providers. Pro plan and above have unlimited custom providers. diff --git a/apps/docs/content/guides/auth/social-login/auth-figma.mdx b/apps/docs/content/guides/auth/social-login/auth-figma.mdx index cc6561fb5a3..1204263579c 100644 --- a/apps/docs/content/guides/auth/social-login/auth-figma.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-figma.mdx @@ -10,13 +10,13 @@ To enable Figma Auth for your project, you need to set up a Figma OAuth applicat Setting up Figma logins for your application consists of 3 parts: -- Create and configure a Figma App on the [Figma Developers page](https://www.figma.com/developers/app). +- Create and configure a Figma App on the [Figma Developers page](https://www.figma.com/developers/apps). - Add your Figma `client_id` and `client_secret` to your [Supabase Project](https://app.supabase.com). - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js). ## Access the Figma Developers page -- Go to the [Figma Developers page](https://www.figma.com/developers/app) +- Go to the [Figma Developers page](https://www.figma.com/developers/apps) - Log in (if necessary) ## Find your callback URL diff --git a/apps/docs/content/guides/auth/third-party/auth0.mdx b/apps/docs/content/guides/auth/third-party/auth0.mdx index f392a8eff63..d19b5d30abb 100644 --- a/apps/docs/content/guides/auth/third-party/auth0.mdx +++ b/apps/docs/content/guides/auth/third-party/auth0.mdx @@ -88,7 +88,7 @@ Future main() async { final auth0 = Auth0('AUTH0_DOMAIN', 'AUTH0_CLIENT_ID'); await Supabase.initialize( url: 'https://.supabase.co', - anonKey: 'SUPABASE_PUBLISHABLE_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', accessToken: () async { final credentials = await auth0.credentialsManager.credentials(); return credentials.idToken; 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 208f838d2b5..b7afa944b32 100644 --- a/apps/docs/content/guides/auth/third-party/aws-cognito.mdx +++ b/apps/docs/content/guides/auth/third-party/aws-cognito.mdx @@ -89,7 +89,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { await Supabase.initialize( url: 'https://.supabase.co', - anonKey: 'SUPABASE_PUBLISHABLE_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', accessToken: () async { final session = await Amplify.Auth.fetchAuthSession(); final cognitoSession = session as CognitoAuthSession; diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index 152de808181..1358f8421db 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -56,7 +56,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_PUBLISHABLE_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', accessToken: () async { final token = await ClerkAuth.of(context).sessionToken(); return token.jwt; 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 870b790c0cb..b1e9f4380cf 100644 --- a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx +++ b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx @@ -49,7 +49,7 @@ Creating a client for the Web is as easy as passing the `accessToken` async func ```dart await Supabase.initialize( url: supabaseUrl, - anonKey: supabaseKey, + publishableKey: publishableKey, debug: false, accessToken: () async { final token = await FirebaseAuth.instance.currentUser?.getIdToken(); diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 9ad4e83383b..f6897e83ea6 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -157,13 +157,14 @@ Deno.serve(async (req) => { ## Simplifying with `@supabase/server` -The [`@supabase/server`](https://github.com/supabase/server) package wraps your handler, checks the caller's credentials against a declared `allow` mode, and hands you a pre-configured Supabase client on `ctx`. The same patterns above, written against the SDK, look like this. +The [`@supabase/server`](https://github.com/supabase/server) package wraps your handler, checks the caller's credentials against a declared `auth` mode, and hands you a pre-configured Supabase client on `ctx`. The same patterns above, written against the SDK, look like this. -| Mode | Accepts | -| ----------------- | ------------------------------------------ | -| `'user'` | A valid user JWT on `Authorization` | -| `'secret:'` | A named secret key on `apikey` | -| `'always'` | Any caller, no check (for signed webhooks) | +| Mode | Accepts | +| ---------------------- | ------------------------------------------ | +| `'user'` | A valid user JWT on `Authorization` | +| `'secret:'` | A named secret key on `apikey` | +| `'publishable:'` | A named publishable key on `apikey` | +| `'none'` | Any caller, no check (for signed webhooks) | @@ -171,30 +172,30 @@ See the [`@supabase/server` docs](https://github.com/supabase/server) for the fu -### Authenticated user calls +### Authenticated user calls [#authenticated-user-calls-with-server-sdk] -`allow: 'user'` pairs with `verify_jwt = true`. The platform validates the JWT, and the SDK hands you `ctx.supabase` already scoped to the caller. +`auth: 'user'` pairs with `verify_jwt = true`. The platform validates the JWT, and the SDK hands you `ctx.supabase` already scoped to the caller. ```ts import { withSupabase } from 'npm:@supabase/server' export default { - fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => { + fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { // your business logic. ctx.supabase is scoped to the caller return Response.json({ email: ctx.userClaims?.email }) }), } ``` -### Service-to-service calls +### Service-to-service calls [#service-to-service-calls-with-server-sdk] -`allow: 'secret:'` validates the `apikey` header against the named secret key from your [dashboard](/dashboard/project/_/settings/api-keys) and gives you `ctx.supabaseAdmin` for privileged work. The `` matches the name you gave the key. Keep `verify_jwt = false`. +`auth: 'secret:'` validates the `apikey` header against the named secret key from your [dashboard](/dashboard/project/_/settings/api-keys) and gives you `ctx.supabaseAdmin` for privileged work. The `` matches the name you gave the key. Keep `verify_jwt = false`. ```ts import { withSupabase } from 'npm:@supabase/server' export default { - fetch: withSupabase({ allow: 'secret:automations' }, async (_req, ctx) => { + fetch: withSupabase({ auth: 'secret:automations' }, async (_req, ctx) => { // your business logic. ctx.supabaseAdmin bypasses RLS return Response.json({ ok: true }) }), @@ -209,13 +210,13 @@ Create a named secret key for each caller in the [**Settings > API keys**](/dash -### Public functions +### Public functions [#public-functions-with-server-sdk] -The SDK adds nothing to a truly public function. Use the raw pattern from the previous section. If you need a Supabase client anyway, `allow: 'always'` with `verify_jwt = false` skips every check and treats every caller as anonymous. +The SDK adds nothing to a truly public function. Use the raw pattern from the previous section. If you need a Supabase client anyway, `auth: 'none'` with `verify_jwt = false` skips every check and treats every caller as anonymous. -### External webhooks +### External webhooks [#external-webhooks-with-server-sdk] -Use `allow: 'always'` to skip the SDK's credential check, then verify the provider's signature inside the handler. Keep `verify_jwt = false`. +Use `auth: 'none'` to skip the SDK's credential check, then verify the provider's signature inside the handler. Keep `verify_jwt = false`. ```ts import { withSupabase } from 'npm:@supabase/server' @@ -224,7 +225,7 @@ import Stripe from 'npm:stripe' const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!) export default { - fetch: withSupabase({ allow: 'always' }, async (req, ctx) => { + fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { const signature = req.headers.get('stripe-signature') ?? '' const body = await req.text() @@ -242,20 +243,20 @@ export default { -`allow: 'always'` disables every credential check. Your handler is fully responsible for authenticating the caller. Never use it on an endpoint that reads or writes sensitive data without verifying the caller some other way. +`auth: 'none'` disables every credential check. Your handler is fully responsible for authenticating the caller. Never use it on an endpoint that reads or writes sensitive data without verifying the caller some other way. ### Combining modes -Functions that answer both users and internal callers take an array on `allow`. Modes are tried in order. The first match wins, and `ctx.authType` tells you which matched. +Functions that answer both users and internal callers take an array on `auth`. Modes are tried in order. The first match wins, and `ctx.authMode` tells you which matched. ```ts import { withSupabase } from 'npm:@supabase/server' export default { - fetch: withSupabase({ allow: ['user', 'secret:automations'] }, async (req, ctx) => { - if (ctx.authType === 'user') { + fetch: withSupabase({ auth: ['user', 'secret:automations'] }, async (req, ctx) => { + if (ctx.authMode === 'user') { // your business logic for user calls. ctx.supabase is scoped to them return Response.json({ ok: true }) } @@ -275,7 +276,7 @@ import { createSupabaseContext } from 'npm:@supabase/server' export default { fetch: async (req: Request) => { - const { data: ctx, error } = await createSupabaseContext(req, { allow: 'user' }) + const { data: ctx, error } = await createSupabaseContext(req, { auth: 'user' }) if (error) { return Response.json({ message: error.message, code: error.code }, { status: error.status }) } diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx index 3fb84811193..db8248876c3 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx @@ -117,10 +117,10 @@ console.log(`Function "elevenlabs-scribe-bot" up and running!`) const elevenLabsClient = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '', }) - +const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabase = createClient( Deno.env.get('SUPABASE_URL') || '', - Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || '' + Deno.env.get(SUPABASE_SECRET_KEYS['default']) || '' ) async function scribe({ diff --git a/apps/docs/content/guides/functions/quickstart.mdx b/apps/docs/content/guides/functions/quickstart.mdx index 7b9fd8575fd..ef9fb77823f 100644 --- a/apps/docs/content/guides/functions/quickstart.mdx +++ b/apps/docs/content/guides/functions/quickstart.mdx @@ -63,7 +63,7 @@ This creates a new function at `supabase/functions/hello-world/index.ts` with th ```tsx export default { - fetch: withSupabase({ allow: ['public', 'secret'] }, async (req, ctx) => { + fetch: withSupabase({ auth: ['publishable', 'secret'] }, async (req, ctx) => { const { name } = await req.json() return Response.json({ diff --git a/apps/docs/content/guides/getting-started/ai-skills.mdx b/apps/docs/content/guides/getting-started/ai-skills.mdx index 5a933d53259..5728a5665d9 100644 --- a/apps/docs/content/guides/getting-started/ai-skills.mdx +++ b/apps/docs/content/guides/getting-started/ai-skills.mdx @@ -18,16 +18,11 @@ To install a specific skill from the repository: npx skills add supabase/agent-skills --skill SKILL_NAME ``` -### Claude Code plugin +Skills are installed at project scope by default, placing them in your repository so contributors and cloud agents all share the same setup. Pass `--global` to install across all your projects instead. -You can also install the skills as Claude Code plugins: +Add skills for all detected agents at the same time by passing `--all`. See the [skills package](https://github.com/vercel-labs/skills) for more options. -```bash -/plugin marketplace add supabase/agent-skills -/plugin install supabase@supabase-agent-skills -``` - -Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others. +You can also install the agent skills together with the Supabase MCP server using the [Supabase agent plugin](/docs/guides/getting-started/plugins) for an agent-first workflow. ## Available skills @@ -46,3 +41,4 @@ npx skills find QUERY - [Agent Skills Repository](https://github.com/supabase/agent-skills) - [Agent Skills Documentation](https://agentskills.io/home) - [Agent Skills Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) +- [skills npm package](https://github.com/vercel-labs/skills) diff --git a/apps/docs/content/guides/getting-started/mcp.mdx b/apps/docs/content/guides/getting-started/mcp.mdx index c0d90df1052..76cbd9c2055 100644 --- a/apps/docs/content/guides/getting-started/mcp.mdx +++ b/apps/docs/content/guides/getting-started/mcp.mdx @@ -30,6 +30,8 @@ To verify the client has access to the MCP server tools, try asking it to query For curated, ready-to-use prompts that work well with IDEs and AI agents, see our [AI Prompts](/docs/guides/getting-started/ai-prompts) collection. +Additionally, you can install Supabase agent skills alongside the MCP server, use the [Supabase agent plugin](/docs/guides/getting-started/plugins) for a combined one-step setup. + ## Available tools The Supabase MCP server provides tools organized into feature groups. All groups except Storage are enabled by default. You can enable or disable specific groups using the [configuration panel above](#step-2-configure-your-ai-tool). diff --git a/apps/docs/content/guides/getting-started/plugins.mdx b/apps/docs/content/guides/getting-started/plugins.mdx new file mode 100644 index 00000000000..6a2e624edcb --- /dev/null +++ b/apps/docs/content/guides/getting-started/plugins.mdx @@ -0,0 +1,42 @@ +--- +id: 'ai-tools-plugins' +title: 'Supabase Agent Plugin' +subtitle: 'One-click setup for Supabase in your AI coding agent' +description: 'The Supabase agent plugin bundles the MCP server and agent skills into a single install for your AI coding agent.' +sidebar_label: 'Supabase Agent Plugin' +--- + +The Supabase agent plugin is a single install that gives your AI coding agent everything it needs to work with Supabase. It bundles the [Supabase MCP server](/docs/guides/getting-started/mcp) and [Supabase agent skills](/docs/guides/getting-started/ai-skills) so your agent can query your database, manage migrations, deploy Edge Functions, and follow Supabase and Postgres best practices — without manual configuration. + +## Why use the plugin? + +Agent plugins are packages of AI agent extensions. A single plugin can bundle any combination of: + +- **MCP servers** — external tool integrations that let your agent interact with services like Supabase +- **Skills** — procedural knowledge and context your agent loads on demand to work more accurately +- **Hooks** — event handlers that run at agent lifecycle points (e.g. before or after a tool call) +- **Agents** — specialized sub-agents with specific personas and tool configurations +- **Slash commands** — custom commands you can invoke directly in chat + +Bundling the [MCP server](/docs/guides/getting-started/mcp) and [agent skills](/docs/guides/getting-started/ai-skills) into a single plugin means you can set up both in one step. You can also install them separately if you prefer. You can install the plugin globally to use it across all your projects, or per project to keep it isolated. + +## Installation + +Choose your AI coding agent and follow the installation steps: + + + +## What's included + +### Supabase MCP server + +The [Supabase MCP server](/docs/guides/getting-started/mcp) connects your AI coding agent directly to your Supabase projects. Once authenticated, your agent can query your database, manage migrations, deploy Edge Functions, and more — see the [full list of available tools](/docs/guides/getting-started/mcp#available-tools). + +### Supabase agent skills + +Skills provide your agent with Supabase-specific procedural knowledge: + +- **`supabase`** — Core guidance for working with Supabase products (Database, Auth, Edge Functions, Storage, Realtime) +- **`supabase-postgres-best-practices`** — Postgres query optimization, schema design, connection management, and RLS patterns + +For a full list of available skills and supported agents, see [Agent Skills](/docs/guides/getting-started/ai-skills). diff --git a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx index a4dc3776d47..66d9743c162 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx @@ -102,7 +102,13 @@ hideToc: true }, []); async function getInstruments() { - const { data } = await supabase.from("instruments").select(); + const { data, error } = await supabase.from("instruments").select(); + + if (error) { + console.error(error); + return; + } + setInstruments(data); } diff --git a/apps/docs/content/guides/getting-started/tutorials/with-angular.mdx b/apps/docs/content/guides/getting-started/tutorials/with-angular.mdx index 5bfa9c9faf3..9ad548dfa13 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-angular.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-angular.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Angular App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management). @@ -21,24 +21,22 @@ Start with building the Angular app from scratch. ### Initialize an Angular app -You can use the [Angular CLI](https://angular.io/cli) to initialize an app called `supabase-angular`. -The command sets some defaults, that you change to suit your needs: +Use the [Angular CLI](https://angular.io/cli) to initialize an app called `supabase-angular` setting some defaults that you can change to suit your needs: ```bash npx ng new supabase-angular --routing false --style css --standalone false --ssr false cd supabase-angular ``` -Then, install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js) +Install [supabase-js](https://github.com/supabase/supabase-js): ```bash npm install @supabase/supabase-js ``` -Finally, save the environment variables in a new `src/environments/environment.ts` file. -You need to create the `src/environments` directory first. -All you need are the API URL and the key that you copied [earlier](#get-api-details). -The application exposes these variables in the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database. +Create a `src/environments` directory and save API URL and key that you copied [earlier](#get-api-details) as environment variables in a new `src/environments/environment.ts` file. + +The application exposes these variables in the browser, and that's fine as Supabase enables [Row Level Security](/docs/guides/database/postgres/row-level-security) by default on all tables. <$CodeSample path="/user-management/angular-user-management/src/environments/environment.ts" @@ -48,8 +46,6 @@ meta="name=src/environments/environment.ts" With the API credentials in place, create a `SupabaseService` with `ng g s supabase` and add the following code to initialize the Supabase client and implement functions to communicate with the Supabase API. -This uses the [`getUser`](/docs/reference/javascript/auth-getuser) method to get the current user details if there is an existing session. This method performs a network request to the Supabase Auth server. - <$CodeSample path="/user-management/angular-user-management/src/app/supabase.service.ts" lines={[[1, -1]]} @@ -60,7 +56,13 @@ Optionally, update `src/styles.css` to style the app. You can find the full cont ### Set up a login component -Next, set up an Angular component to manage logins and sign ups. The component uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords. +You need an Angular component to manage logins and sign ups. The component uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords. + + + +You can customize other emails sent out to new users, including the email's looks, content, and query parameters from [the **Authentication > Email**](/dashboard/project/_/auth/templates) section of the Dashboard. + + Create an `AuthComponent` with the `ng g c auth` Angular CLI command and add the following code. @@ -82,20 +84,19 @@ meta="name=src/app/auth/auth.component.html" ### Account page -Users also need a way to edit their profile details and manage their accounts after signing in. -Create an `AccountComponent` with the `ng g c account` Angular CLI command and add the following code. +Users also need a way to edit their profile details and manage their accounts after signing in. Create an `AccountComponent` with the `ng g c account` Angular CLI command and add the following code. <$CodeTabs> <$CodeSample path="/user-management/angular-user-management/src/app/account/account.component.ts" -lines={[[1, -1]]} +lines={[[1, 16], [27, -1]]} meta="name=src/app/account/account.component.ts" /> <$CodeSample path="/user-management/angular-user-management/src/app/account/account.component.html" -lines={[[1, -1]]} +lines={[[1, 1], [3, -1]]} meta="name=src/app/account/account.component.html" /> @@ -103,12 +104,11 @@ meta="name=src/app/account/account.component.html" ## Profile photos -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. +Add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget -Create an avatar for the user so that they can upload a profile photo. -Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code. +Create an `AvatarComponent` with the `ng g c avatar` Angular CLI command and add the following code. <$CodeTabs> @@ -126,9 +126,39 @@ meta="name=src/app/avatar/avatar.component.html" +### Update the Account component + +With the Avatar component created, update `AccountComponent` to include it: + +<$CodeTabs> + +<$CodeSample +path="/user-management/angular-user-management/src/app/account/account.component.ts" +lines={[[1, -1]]} +meta="name=src/app/account/account.component.ts" +/> + +<$CodeSample +path="/user-management/angular-user-management/src/app/account/account.component.html" +lines={[[1, -1]]} +meta="name=src/app/account/account.component.html" +/> + + + +You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package. + +<$CodeSample +path="/user-management/angular-user-management/src/app/app.module.ts" +lines={[[1, -1]]} +meta="name=src/app/app.module.ts" +/> + ### Launch! -Now you have all the components in place, update `AppComponent`: +With all the components in place, change the contents of `AppComponent` to include the new components and Auth logic: + +<$Partial path="auth_methods.mdx" /> <$CodeTabs> @@ -146,15 +176,7 @@ meta="name=src/app/app.component.html" -You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package. - -<$CodeSample -path="/user-management/angular-user-management/src/app/app.module.ts" -lines={[[1, -1]]} -meta="name=src/app/app.module.ts" -/> - -Once that's done, run the application in a terminal: +Now run the application in a terminal: ```bash npm run start diff --git a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx index 0e7a738170c..1aa5d7e3fca 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx @@ -193,7 +193,7 @@ Create a new component for that called `Account.tsx`. <$CodeSample path="/user-management/expo-user-management/components/Account.tsx" -lines={[[1, -1]]} +lines={[[1, 4], [6, 79], [90, -1]]} meta="name=components/Account.tsx" /> @@ -241,7 +241,13 @@ meta="name=components/Avatar.tsx" ### Add the new widget -And then add the widget to the Account page. The `Account.tsx` component [shown earlier](#account-page) already includes the `Avatar` component when using the full example code. +And then add the widget to the Account page: + +<$CodeSample +path="/user-management/expo-user-management/components/Account.tsx" +lines={[[1, -1]]} +meta="name=components/Account.tsx" +/> Now run the prebuild command to get the application working on your chosen platform. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx b/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx index ccc1ffdf8e6..0148facc6db 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx @@ -87,7 +87,7 @@ meta="name=src/app/account/account.page.ts" <$CodeSample path="/user-management/ionic-angular-user-management/src/app/account/account.page.html" -lines={[[1, -1]]} +lines={[[1, 7], [13, -1]]} meta="name=src/app/account/account.page.html" /> @@ -169,6 +169,16 @@ lines={[[1, -1]]} meta="name=src/app/avatar/avatar.component.scss" /> +### Update the account page + +With the Avatar component created, update the account page template to include it: + +<$CodeSample +path="/user-management/ionic-angular-user-management/src/app/account/account.page.html" +lines={[[1, -1]]} +meta="name=src/app/account/account.page.html" +/> + At this stage, you have a fully functional application! ## See also diff --git a/apps/docs/content/guides/getting-started/tutorials/with-ionic-react.mdx b/apps/docs/content/guides/getting-started/tutorials/with-ionic-react.mdx index 0f5b6ecaee8..fac23b6ec1e 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-ionic-react.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-ionic-react.mdx @@ -74,7 +74,7 @@ Create a new component for that called `Account.tsx`. <$CodeSample path="/user-management/ionic-react-user-management/src/pages/Account.tsx" -lines={[[1, -1]]} +lines={[[1, 15], [17, 124], [129, -1]]} meta="name=src/pages/Account.tsx" /> @@ -136,7 +136,7 @@ And then add the widget to the Account page: <$CodeSample path="/user-management/ionic-react-user-management/src/pages/Account.tsx" -lines={[[16, 16], [101, 110]]} +lines={[[1, -1]]} meta="name=src/pages/Account.tsx" /> diff --git a/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx b/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx index 70c84bdae8d..baff725b244 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx @@ -70,7 +70,7 @@ After a user has signed in, let them edit their profile details and manage their <$CodeSample path="/user-management/ionic-vue-user-management/src/views/Account.vue" -lines={[[1, -1]]} +lines={[[1, 9], [11, 67], [69, -1]]} meta="name=src/views/Account.vue" /> @@ -134,6 +134,12 @@ meta="name=src/components/Avatar.vue" ### Add the new widget -Add the widget to the Account page (already included in the Account.vue code above since the example includes the Avatar component by default). +And then add the widget to the Account page: + +<$CodeSample +path="/user-management/ionic-vue-user-management/src/views/Account.vue" +lines={[[1, -1]]} +meta="name=src/views/Account.vue" +/> At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx index 5144851b5cf..6487f311807 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx @@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Next.js App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management). @@ -29,7 +29,7 @@ npx create-next-app@latest --ts --use-npm supabase-nextjs cd supabase-nextjs ``` -Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js) +Install [supabase-js](https://github.com/supabase/supabase-js): ```bash npm install @supabase/supabase-js @@ -37,6 +37,8 @@ npm install @supabase/supabase-js Save the environment variables in a `.env.local` file at the root of the project, and paste the API URL and the key that you copied [earlier](#get-api-details). +The application exposes these variables in the browser, and that's fine as Supabase enables [Row Level Security](/docs/guides/database/postgres/row-level-security) by default on all tables. + ```bash .env.local NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY @@ -47,11 +49,11 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY An optional step is to update the CSS file `app/globals.css` to make the app look better. You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css). -### Supabase Server-Side Auth +### Supabase Server-Side Auth package -Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and proxy edge-functions. +Next.js is a versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and proxy edge-functions. -To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs) for more information. +To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs) for more information. Install the package for Next.js. @@ -66,9 +68,9 @@ There are two different types of clients in Supabase: 1. **Client Component client** - To access Supabase from Client Components, which run in the browser. 2. **Server Component client** - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server. -We recommend creating the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project. +We recommend creating the following utilities files for creating clients, and organize them within `lib/supabase` at the root of the project. -Create a `client.ts` and a `server.ts` with the following functionalities for client-side Supabase and server-side Supabase, respectively. +Create a `client.ts` and a `server.ts` with the following code for client-side Supabase and server-side Supabase, respectively. <$CodeTabs> @@ -90,7 +92,7 @@ meta="name=lib/supabase/server.ts" Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them. -You can accomplish this by: +You accomplish this by: - Refreshing the Auth token with the call to `supabase.auth.getClaims`. - Passing the refreshed Auth token to Server Components through `request.cookies.set`, so they don't attempt to refresh the same token themselves. @@ -102,14 +104,10 @@ You could also add a matcher, so that the Proxy only runs on routes that access Be careful when protecting pages. The server gets the user session from the cookies, which anyone can spoof. -Most of the time, use `supabase.auth.getClaims()` to protect pages and user data. - -_Never_ trust `supabase.auth.getSession()` inside server code such as proxy. It isn't guaranteed to revalidate the Auth token. - -It's safe to trust `getClaims()` because it validates the token in storage, either directly or by calling `getUser()` solely to check the result. It doesn't use the response from `getUser()` itself, only whether the validation succeeded. - +<$Partial path="auth_methods.mdx" /> + Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. The `proxy.ts` file uses this, which is a Next.js convention. <$CodeTabs> @@ -128,13 +126,11 @@ meta="name=lib/supabase/proxy.ts" -## Set up a login page +### Set up a login page -### Login and signup form +#### Login and signup form -In order to add login/signup page for your application: - -Create a new folder named `login`, containing a `page.tsx` file with a login/signup form. +To add login/signup page for your application, create a new folder named `login`, containing a `page.tsx` file with the following code for a login/signup form: <$CodeTabs> @@ -146,20 +142,12 @@ meta="name=app/login/page.tsx" -Next, you need to create the login/signup actions to hook up the form to the function. Which does the following: +Create the login/signup actions to hook up the form to the function which does the following: - Retrieve the user's information. -- Send that information to Supabase as a signup request, which in turns sends a confirmation email. +- Send that information to Supabase as a signup request, which in turns sends a confirmation email. It uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords. - Handle any error that arises. - - -The `cookies` method is called before any calls to Supabase, which takes fetch calls out of Next.js's caching. This is important for authenticated data fetches, to ensure that users get access only to their own data. - -Read the Next.js docs to learn more about [opting out of data caching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching). - - - Create the `action.ts` file in the `app/login` folder, which contains the login and signup functions and the `error/page.tsx` file, which displays an error message if the login or signup fails. <$CodeTabs> @@ -178,7 +166,15 @@ meta="name=app/error/page.tsx" -### Email template + + +The `cookies` method is called before any calls to Supabase, which takes fetch calls out of Next.js's caching. This is important for authenticated data fetches, to ensure that users get access only to their own data. + +Read the Next.js docs to learn more about [opting out of data caching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching). + + + +#### Email template Before proceeding, change the email template to support a server-side authentication flow that sends a token hash: @@ -188,11 +184,11 @@ Before proceeding, change the email template to support a server-side authentica -You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates). +You can customize other emails sent out to new users, including the email's looks, content, and query parameters from [the **Authentication > Email**](/dashboard/project/_/auth/templates) section of the Dashboard. -### Confirmation endpoint +#### Confirmation endpoint As you are working in a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session. @@ -210,13 +206,13 @@ meta="name=app/auth/confirm/route.ts" ### Account page -After a user signs in, allow them to edit their profile details and manage their account. +After a user signs in, they need a way to edit their profile details and manage their accounts. Create a new component for that called `AccountForm` within the `app/account` folder. <$CodeSample path="/user-management/nextjs-user-management/app/account/account-form.tsx" -lines={[[1, 4], [7, 78], [88, -1]]} +lines={[[1, 4], [7, 78], [88, 89], [99, -1]]} meta="name=app/account/account-form.tsx" /> @@ -238,9 +234,33 @@ lines={[[1, -1]]} meta="name=app/auth/signout/route.ts" /> +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Start by creating a new component: + +<$CodeSample +path="/user-management/nextjs-user-management/app/account/avatar.tsx" +lines={[[1, -1]]} +meta="name=app/account/avatar.tsx" +/> + +### Update the account form + +With the Avatar component created, update `app/account/account-form.tsx` to include it: + +<$CodeSample +path="/user-management/nextjs-user-management/app/account/account-form.tsx" +lines={[[1, -1]]} +meta="name=app/account/account-form.tsx" +/> + ### Launch -Now you have all the pages, route handlers, and components in place, run the following in a terminal window: +With all the pages, route handlers, and components in place, run the following in a terminal window: ```bash npm run dev @@ -250,31 +270,6 @@ And then open the browser to [localhost:3000/login](http://localhost:3000/login) When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!! -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like -photos and videos. - -### Create an upload widget - -Create an avatar widget for the user so that they can upload a profile photo. Start by creating a new component: - -<$CodeSample -path="/user-management/nextjs-user-management/app/account/avatar.tsx" -lines={[[1, -1]]} -meta="name=app/account/avatar.tsx" -/> - -### Add the new widget - -Then add the widget to the `AccountForm` component: - -<$CodeSample -path="/user-management/nextjs-user-management/app/account/account-form.tsx" -lines={[[5, 5], [77, 87], [137, -1]]} -meta="name=app/account/account-form.tsx" -/> - At this stage you have a fully functional application! ## See also diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx index 88868969d72..fc1bdb68c77 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx @@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Nuxt 3 App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nuxt3-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nuxt3-user-management). @@ -54,8 +54,8 @@ No need to initialize Supabase. The library will take care of it automatically. ### App styling (optional) -An optional step is to update the CSS file `assets/main.css` to make the app look nice. -You can find the full contents of this file [here](https://github.com/supabase-community/nuxt3-quickstarter/blob/main/assets/main.css). +An optional step is to update the CSS file `assets/main.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://github.com/supabase-community/nuxt3-quickstarter/blob/main/assets/main.css). <$CodeTabs> @@ -229,44 +229,13 @@ async function signOut() { -### Launch! +## Profile photos -Now that we have all the components in place, let's update `app.vue`: - -<$CodeTabs> - -```vue name=app.vue - - - -``` - - - -Once that's done, run this in a terminal window: - -```bash -npm run dev -``` - -And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. - -![Supabase Nuxt 3](/docs/img/supabase-vue-3-demo.png) - -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: +Start by creating a new component: <$CodeTabs> @@ -359,108 +328,35 @@ watch(path, () => { -### Add the new widget +### Launch! -And then we can add the widget to the Account page: +With all the components in place, update `app.vue`: <$CodeTabs> -```vue name=components/Account.vue +```vue name=app.vue ``` -That is it! You should now be able to upload a profile photo to Supabase Storage and you have a fully functional application. +Once that's done, run this in a terminal window: + +```bash +npm run dev +``` + +And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. + +![Supabase Nuxt 3](/docs/img/supabase-vue-3-demo.png) + +At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-react.mdx b/apps/docs/content/guides/getting-started/tutorials/with-react.mdx index 8f656779f11..f740c3a7c8e 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-react.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-react.mdx @@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your React App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/react-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/react-user-management). @@ -29,14 +29,13 @@ npm create vite@latest supabase-react -- --template react cd supabase-react ``` -Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js). +Install [supabase-js](https://github.com/supabase/supabase-js): ```bash npm install @supabase/supabase-js ``` -And finally, save the environment variables in a `.env.local` file. -Use the Project URL and the key that you copied [earlier](#get-api-details). +Save the environment variables in a `.env.local` file, using the Project URL and the key that you copied [earlier](#get-api-details). <$CodeSample path="/user-management/react-user-management/.env.example" @@ -44,8 +43,7 @@ lines={[[1, -1]]} meta="name=.env" /> -With the API credentials in place, create a helper file to initialize the Supabase client. These variables will be exposed -on the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database. +With the API credentials in place, create a helper file to initialize the Supabase client. The application exposes these variables in the browser, and that's fine as Supabase enables [Row Level Security](/docs/guides/database/postgres/row-level-security) by default on all tables. Create and edit `src/supabaseClient.js`: @@ -57,12 +55,18 @@ meta="name=src/supabaseClient.js" ### App styling (optional) -An optional step is to update the CSS file `src/index.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css). +An optional step is to update the CSS file `src/index.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css). ### Set up a login component -Create a React component to manage logins and sign-ups. It uses Magic Links, so users can sign in with their email without using passwords. +You need a React component to manage logins and sign-ups. It uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords. + + + +You can customize other emails sent out to new users, including the email's looks, content, and query parameters from [the **Authentication > Email**](/dashboard/project/_/auth/templates) section of the Dashboard. + + Create and edit `src/Auth.jsx`: @@ -74,15 +78,23 @@ meta="name=src/Auth.jsx" ### Account page -Users also need a way to edit their profile details and manage their accounts after signing in. +After a user signs in, they need a way to edit their profile details and manage their accounts. + +Create a new component called `src/Account.jsx` and add the following code: + +<$CodeSample +path="/user-management/react-user-management/src/Account.jsx" +lines={[[1, 2], [4, 66], [74, -1]]} +meta="name=src/Account.jsx" +/> + +## Profile photos + +Add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -Create an avatar for the user so that they can upload a profile photo. Start by creating a new component: - -Create and edit `src/Avatar.jsx`: +Create `src/Avatar.jsx` and add the following code: <$CodeSample path="/user-management/react-user-management/src/Avatar.jsx" @@ -90,9 +102,9 @@ lines={[[1, -1]]} meta="name=src/Avatar.jsx" /> -After a user is signed in, allow them to edit their profile details and manage their account. +### Update the Account component -Create a new component for that called `src/Account.jsx` and also add the `Avatar` component created earlier. +With the Avatar component created, update `src/Account.jsx` to include it: <$CodeSample path="/user-management/react-user-management/src/Account.jsx" @@ -102,7 +114,9 @@ meta="name=src/Account.jsx" ### Launch! -Now that you have all the components in place, update `src/App.jsx`, which fetches the current user via the [`getUser`](/docs/reference/javascript/auth-getuser) method if there is an existing session. This method performs a network request to the Supabase Auth server. +With all the components in place, change the contents of `src/App.jsx` to include the new components and Auth logic. + +<$Partial path="auth_methods.mdx" /> <$CodeSample path="/user-management/react-user-management/src/App.jsx" @@ -118,6 +132,6 @@ npm run dev And then open the browser to [localhost:5173](http://localhost:5173) and you should see the completed app. -![Supabase React](/docs/img/supabase-react-demo.png) +![Screenshot of the Supabase React application running in a browser](/docs/img/supabase-react-demo.png) At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx index 09ccbd9d1bb..18f4af41521 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your RedwoodJS App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart). @@ -169,8 +169,8 @@ export default App ### App styling (optional) -An optional step is to update the CSS file `web/src/index.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css). +An optional step is to update the CSS file `web/src/index.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css). ### Start RedwoodJS and your first page @@ -457,7 +457,7 @@ of the Supabase client to interact with your API. ### Update home page -Now that we have all the components in place, let's update your `HomePage` page to use them: +With all the components in place, update your `HomePage` page to use them: <$CodeTabs> @@ -490,21 +490,9 @@ What we're doing here is showing the sign in form if you aren't logged in and yo -### Launch! +## Profile photos -Once that's done, run this in a terminal window to launch the `dev` server: - -```bash -yarn rw dev -``` - -And then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app. - -![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png) - -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget @@ -613,35 +601,17 @@ export default Avatar -### Add the new widget +### Launch! -And then we can add the widget to the Account component: +Once that's done, run this in a terminal window to launch the `dev` server: -<$CodeTabs> - -```jsx name=web/src/components/Account/Account.js -// Import the new component -import Avatar from 'src/components/Avatar' - -// ... - -return ( -
- {/* Add to the body */} - { - setAvatarUrl(url) - updateProfile({ username, website, avatar_url: url }) - }} - /> - {/* ... */} -
-) +```bash +yarn rw dev ``` - +And then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app. + +![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png) At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-refine.mdx b/apps/docs/content/guides/getting-started/tutorials/with-refine.mdx index 553d0f02105..62b3582985c 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-refine.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-refine.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Refine App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/refine-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/refine-user-management). @@ -65,13 +65,15 @@ VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY The `supabaseClient` fetches calls to Supabase endpoints from the app. The client is instrumental in implementing authentication using Refine's auth provider methods and CRUD actions with appropriate data provider methods. -One optional step is to update the CSS file `src/App.css` to make the app look nice. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/refine-user-management/src/App.css). +### App styling (optional) -In order to add login and user profile pages in this App, tweak the `` component inside `App.tsx`. +An optional step is to update the CSS file `src/App.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/refine-user-management/src/App.css). ### The `` component +In order to add login and user profile pages in this App, tweak the `` component inside `App.tsx`. + The `App.tsx` file initially looks like this: <$CodeTabs> @@ -122,7 +124,7 @@ export default App Focus on the [``](https://refine.dev/docs/api-reference/core/components/refine-config/) component, which comes with props passed to it. Notice the `dataProvider` prop. It uses a `dataProvider()` function with `supabaseClient` passed as argument to generate the data provider object. The `authProvider` object also uses `supabaseClient` in implementing its methods. You can look it up in `src/authProvider.ts` file. -## Customize `authProvider` +### Customize `authProvider` If you examine the `authProvider` object you can notice that it has a `login` method that implements an OAuth and Email / Password strategy for authentication. This tutorial instead removes them and use Magic Links to allow users sign in with their email without using passwords. @@ -195,9 +197,35 @@ This uses three Refine hooks, namely the [`useGetIdentity()`](https://refine.dev The `useForm()` hook is a higher-level hook built on top of Refine's `useForm()` core hook. It fully supports form state management, field validation and submission using React Hook Form. Behind the scenes, it invokes the `dataProvider.getOne` method to get the user profile data from the Supabase `/profiles` endpoint and also invokes `dataProvider.update` method when `onFinish()` is called. +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Add a new component: + +Create and edit `src/components/avatar.tsx`: + +<$CodeSample +path="/user-management/refine-user-management/src/components/avatar.tsx" +lines={[[1, -1]]} +meta="name=src/components/avatar.tsx" +/> + +### Update the Account component + +With the Avatar component created, update `src/components/account.tsx` to include it: + +<$CodeSample +path="/user-management/refine-user-management/src/components/account.tsx" +lines={[[1, -1]]} +meta="name=src/components/account.tsx" +/> + ### Launch! -Now that you have all the components in place, define the routes for the pages in which they should be rendered. +With all the components in place, define the routes for the pages in which they should be rendered. Add the routes for `/login` with the `` component and the routes for `index` path with the `` component. So, the final `App.tsx`: @@ -217,30 +245,4 @@ And then open the browser to [localhost:5173](http://localhost:5173) and you sho ![Supabase Refine](/docs/img/supabase-refine-demo.png) -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -Create an avatar for the user so that they can upload a profile photo. Add a new component: - -Create and edit `src/components/avatar.tsx`: - -<$CodeSample -path="/user-management/refine-user-management/src/components/avatar.tsx" -lines={[[1, -1]]} -meta="name=src/components/avatar.tsx" -/> - -### Add the new widget - -And then add the widget to the Account page at `src/components/account.tsx`: - -<$CodeSample -path="/user-management/refine-user-management/src/components/account.tsx" -lines={[[1, -1]]} -meta="name=src/components/account.tsx" -/> - At this stage, you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx index 9b8f37a9bb4..7cda94c9d61 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SolidJS App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/solid-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/solid-user-management). @@ -62,7 +62,7 @@ meta="name=src/supabaseClient.tsx" ### App styling (optional) An optional step is to update the CSS file `src/index.css` to make the app look better. -You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/solid-user-management/src/index.css). +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/solid-user-management/src/index.css). ### Set up a login component @@ -94,9 +94,41 @@ meta="name=src/Account.tsx" +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Start by creating a new component: + +<$CodeTabs> + +<$CodeSample +path="/user-management/solid-user-management/src/Avatar.tsx" +lines={[[1, -1]]} +meta="name=src/Avatar.tsx" +/> + + + +### Update the Account component + +With the Avatar component created, update `src/Account.tsx` to include it: + +<$CodeTabs> + +<$CodeSample +path="/user-management/solid-user-management/src/Account.tsx" +lines={[[1, -1]]} +meta="name=src/Account.tsx" +/> + + + ### Launch! -Now that you have all the components in place, update `App.tsx`: +With all the components in place, update `App.tsx`: <$CodeTabs> @@ -118,36 +150,4 @@ And then open the browser to [localhost:3000](http://localhost:3000) and you sho ![Supabase SolidJS](/docs/img/supabase-solidjs-demo.png) -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -Create an avatar for the user so that they can upload a profile photo. Start by creating a new component: - -<$CodeTabs> - -<$CodeSample -path="/user-management/solid-user-management/src/Avatar.tsx" -lines={[[1, -1]]} -meta="name=src/Avatar.tsx" -/> - - - -### Add the new widget - -And then add the widget to the Account page: - -<$CodeTabs> - -<$CodeSample -path="/user-management/solid-user-management/src/Account.tsx" -lines={[[1, 3], [76, 88]]} -meta="name=src/Account.tsx" -/> - - - At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-svelte.mdx b/apps/docs/content/guides/getting-started/tutorials/with-svelte.mdx index 15c69b1c2fe..0ec26c0f8d1 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-svelte.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-svelte.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Svelte App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/svelte-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/svelte-user-management). @@ -50,21 +50,21 @@ VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY Now you have the API credentials in place, create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's fine since you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database. <$CodeSample -path="user-management/svelte-user-management/src/supabaseClient.ts" +path="/user-management/svelte-user-management/src/supabaseClient.ts" meta="name=src/supabaseClient.ts" /> ### App styling (optional) -Optionally, update the CSS file `src/app.css` to make the app look nice. -You can find the full contents of this file [on GitHub](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css). +Optionally, update the CSS file `src/app.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css). ### Set up a login component Set up a Svelte component to manage logins and sign ups. It uses Magic Links, so users can sign in with their email without using passwords. <$CodeSample -path="user-management/svelte-user-management/src/lib/Auth.svelte" +path="/user-management/svelte-user-management/src/lib/Auth.svelte" meta="name=src/lib/Auth.svelte" /> @@ -75,16 +75,40 @@ Create a new component for that called `Account.svelte`. <$CodeSample path="/user-management/svelte-user-management/src/lib/Account.svelte" -lines={[[1, 4], [7, 11], [14, 33], [34,53], [55,73], [75,-1]]} +lines={[[1, 4], [6, 79], [81, -1]]} meta="src/lib/Account.svelte" /> +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Start by creating a new component: + +<$CodeSample +path="/user-management/svelte-user-management/src/lib/Avatar.svelte" +lines={[[1, -1]]} +meta="name=src/lib/Avatar.svelte" +/> + +### Update the account component + +With the Avatar component created, update `src/lib/Account.svelte` to include it: + +<$CodeSample +path="/user-management/svelte-user-management/src/lib/Account.svelte" +lines={[[1, -1]]} +meta="name=src/lib/Account.svelte" +/> + ### Launch! -Now that you have all the components in place, update `App.svelte`: +With all the components in place, update `App.svelte`: <$CodeSample -path="user-management/svelte-user-management/src/App.svelte" +path="/user-management/svelte-user-management/src/App.svelte" meta="name=src/App.svelte" /> @@ -104,27 +128,4 @@ Svelte uses Vite and the default port is `5173`, Supabase uses `port 3000`. To c ![Supabase Svelte](/docs/img/supabase-svelte-demo.png) -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -Create an avatar for the user so that they can upload a profile photo. Start by creating a new component: - -<$CodeSample -path="user-management/svelte-user-management/src/lib/Avatar.svelte" -meta="name=src/lib/Avatar.svelte" -/> - -### Add the new widget - -And then you can add the widget to the Account page: - -<$CodeSample -path="/user-management/svelte-user-management/src/lib/Account.svelte" -lines={[[1,1], [5,5], [71,73], [74,74], [92,-1]]} -meta="src/lib/Account.svelte" -/> - At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx index 08d723c8ce0..a6f8b9bd743 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SvelteKit App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/sveltekit-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/sveltekit-user-management). @@ -132,7 +132,7 @@ meta="name=src/routes/+layout.svelte" -## Set up a login page +### Set up a login page Create a magic link login/signup page for your application by updating the `routes/+page.svelte` file: @@ -158,7 +158,7 @@ meta="name=src/routes/+page.server.ts" -### Email template +#### Email template Change the email template to support a server-side authentication flow. @@ -175,7 +175,7 @@ You can also customize emails sent out to new users, including the email's looks -### Confirmation endpoint +#### Confirmation endpoint As this is a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session. @@ -195,7 +195,7 @@ meta="name=src/routes/auth/confirm/+server.ts" -### Authentication error page +#### Authentication error page If there is an error with confirming the token, redirect the user to an error page. @@ -208,7 +208,7 @@ meta="name=src/routes/auth/error/+page.svelte" -### Account page +#### Account page After a user signs in, they need to be able to edit their profile details page. Create a new `src/routes/account/+page.svelte` file with the content below. @@ -217,7 +217,7 @@ Create a new `src/routes/account/+page.svelte` file with the content below. <$CodeSample path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte" -lines={[[1, 3],[6,12],[15,38],[47,-1]]} +lines={[[1, 3],[6,12],[15,38],[49,-1]]} meta="name=src/routes/account/+page.svelte" /> @@ -232,6 +232,38 @@ lines={[[1, -1]]} meta="name=src/routes/account/+page.server.ts" /> +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory: + +<$CodeTabs> + +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/account/Avatar.svelte" +lines={[[1, -1]]} +meta="name=src/routes/account/Avatar.svelte" +/> + + + +### Update the account page + +With the Avatar component created, update `src/routes/account/+page.svelte` to include it: + +<$CodeTabs> + +<$CodeSample +path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte" +lines={[[1, -1]]} +meta="name=src/routes/account/+page.svelte" +/> + + + ### Launch! With all the pages in place, run this command in a terminal: @@ -244,36 +276,4 @@ And then open the browser to [localhost:5173](http://localhost:5173) and you sho ![Supabase Svelte](/docs/img/supabase-svelte-demo.png) -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -Create an avatar for the user so that they can upload a profile photo. Start by creating a new component called `Avatar.svelte` in the `src/routes/account` directory: - -<$CodeTabs> - -<$CodeSample -path="/user-management/sveltekit-user-management/src/routes/account/Avatar.svelte" -lines={[[1, -1]]} -meta="name=src/routes/account/Avatar.svelte" -/> - - - -### Add the new widget - -Add the widget to the Account page: - -<$CodeTabs> - -<$CodeSample -path="/user-management/sveltekit-user-management/src/routes/account/+page.svelte" -lines={[[1, 1], [4,4], [31,31],[39,46],[82,82]]} -meta="name=src/routes/account/+page.svelte" -/> - - - At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx index 89177e8ea8b..4e240e1d969 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx @@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SwiftUI App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/swift-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/swift-user-management). @@ -273,47 +273,9 @@ struct UpdateProfileParams: Encodable { -### Launch! +## Profile photos -Now that you've created all the views, add an entry point for the application. This will verify if the user has a valid session and route them to the authenticated or non-authenticated state. - -Add a new `AppView.swift` file. - -<$CodeTabs> - -```swift name=AppView.swift -import SwiftUI - -struct AppView: View { - @State var isAuthenticated = false - - var body: some View { - Group { - if isAuthenticated { - ProfileView() - } else { - AuthView() - } - } - .task { - for await state in supabase.auth.authStateChanges { - if [.initialSession, .signedIn, .signedOut].contains(state.event) { - isAuthenticated = state.session != nil - } - } - } - } -} -``` - - - -Update the entry point to the newly created `AppView`. Run in Xcode to launch your application in the simulator. - -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like -photos and videos. +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} @@ -562,4 +524,41 @@ struct Profile: Codable { You no longer need the `UpdateProfileParams` struct, as you can now reuse the `Profile` struct for both request and response calls. +### Launch! + +With all the views in place, add an entry point for the application. + +Add a new `AppView.swift` file. + +<$CodeTabs> + +```swift name=AppView.swift +import SwiftUI + +struct AppView: View { + @State var isAuthenticated = false + + var body: some View { + Group { + if isAuthenticated { + ProfileView() + } else { + AuthView() + } + } + .task { + for await state in supabase.auth.authStateChanges { + if [.initialSession, .signedIn, .signedOut].contains(state.event) { + isAuthenticated = state.session != nil + } + } + } + } +} +``` + + + +Update the entry point to the newly created `AppView`. Run in Xcode to launch your application in the simulator. + At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-vue-3.mdx b/apps/docs/content/guides/getting-started/tutorials/with-vue-3.mdx index a14fd0f2b91..3013377e7a1 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-vue-3.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-vue-3.mdx @@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Vue 3 App.' -If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/vue3-user-management). +If you get stuck while working through this guide, you can find the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/vue3-user-management). @@ -61,7 +61,10 @@ lines={[[1, -1]]} meta="name=src/supabase.js" /> -Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/vue3-user-management/src/style.css) to style the app. +### App styling (optional) + +An optional step is to update the CSS file `src/style.css` to make the app look better. +You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/vue3-user-management/src/style.css). ### Set up a login component @@ -78,6 +81,30 @@ meta="name=src/components/Auth.vue" After a user signs in, allow them to edit their profile details and manage their account. Create a new `src/components/Account.vue` component to handle this. +<$CodeSample +path="/user-management/vue3-user-management/src/components/Account.vue" +lines={[[1, 3], [5, 76], [78, -1]]} +meta="name=src/components/Account.vue" +/> + +## Profile photos + +Next, add a way for users to upload a profile photo. Supabase configures every project with [Storage](/docs/guides/storage) for managing large files like photos and videos. + +### Create an upload widget + +Create a new `src/components/Avatar.vue` component that allows users to upload profile photos: + +<$CodeSample +path="/user-management/vue3-user-management/src/components/Avatar.vue" +lines={[[1, -1]]} +meta="name=src/components/Avatar.vue" +/> + +### Update the Account component + +With the Avatar component created, update `src/components/Account.vue` to include it: + <$CodeSample path="/user-management/vue3-user-management/src/components/Account.vue" lines={[[1, -1]]} @@ -104,24 +131,4 @@ And then open the browser to [localhost:5173](http://localhost:5173) and you sho ![Supabase Vue 3](/docs/img/supabase-vue-3-demo.png) -## Bonus: Profile photos - -Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. - -### Create an upload widget - -Create a new `src/components/Avatar.vue` component that allows users to upload profile photos: - -<$CodeSample -path="/user-management/vue3-user-management/src/components/Avatar.vue" -lines={[[1, -1]]} -meta="name=src/components/Avatar.vue" -/> - -### Add the new widget - -Finally, add the widget to the Account page. - -The `Account.vue` component [shown earlier](#account-page) already includes the `Avatar` component. - At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/local-development.mdx b/apps/docs/content/guides/local-development.mdx index 872f486567e..6b9490c59cf 100644 --- a/apps/docs/content/guides/local-development.mdx +++ b/apps/docs/content/guides/local-development.mdx @@ -116,7 +116,7 @@ As a prerequisite, you must install a container runtime compatible with Docker A -If your local development machine is connected to an untrusted public network, you should create a separate docker network and bind to 127.0.0.1 before starting the local development stack. This restricts network access to only your localhost machine. +If your local development machine is connected to an untrusted public network, you should create a separate Docker network and bind to 127.0.0.1 before starting the local development stack. This restricts network access to only your localhost machine. ```sh docker network create -o 'com.docker.network.bridge.host_binding_ipv4=127.0.0.1' local-network diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx index 5a4a68ef205..2b65ba51e7f 100644 --- a/apps/docs/content/guides/realtime/authorization.mdx +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -381,9 +381,9 @@ with check ( ## Interaction with Postgres Changes -Realtime Postgres Changes are separate from Channel authorization. The `private` Channel option does not apply to Postgres Changes. +When using Postgres Changes on tables with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies. -When using Postgres Changes with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies. +Private and public channels can subscribe to Postgres Changes. ## Updating RLS policies diff --git a/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx index 794a233cecf..5d4df0ceda2 100644 --- a/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx +++ b/apps/docs/content/guides/self-hosting/self-hosted-proxy-https.mdx @@ -33,7 +33,7 @@ If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefi -Envoy is an optional [API gateway](/docs/guides/self-hosting/self-hosted-envoy), enabled via the `docker-compose.envoy.yml` overlay. If you already run Envoy instead of Kong, edit `docker-compose.caddy.yml` or `docker-compose.nginx.yml` to comment out the `kong:` block and uncomment the `api-gw:` block (and the matching `depends_on` entry) so the reverse proxy sits in front of Envoy. +Envoy is an optional [API gateway](/docs/guides/self-hosting/self-hosted-envoy), enabled via the `docker-compose.envoy.yml` override. If you already run Envoy instead of Kong, edit `docker-compose.caddy.yml` or `docker-compose.nginx.yml` to comment out the `kong:` block and uncomment the `api-gw:` block (and the matching `depends_on` entry) so the reverse proxy sits in front of Envoy. @@ -56,7 +56,7 @@ CERTBOT_EMAIL=admin@your-domain.example.com ### Step 2: Start the reverse proxy -Pick one of the options below and use the corresponding Docker Compose overlay. +Pick one of the options below and use the corresponding Docker Compose override. {/* supa-mdx-lint-disable-next-line Rule003Spelling */} -An overlay `docker-compose.s3.yml` configuration can be added to enable MinIO container and provide an S3-compatible API for Storage backend: +An override `docker-compose.s3.yml` can be added to enable MinIO container and provide an S3-compatible API for Storage backend: ```sh docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d diff --git a/apps/docs/content/guides/storage/s3/authentication.mdx b/apps/docs/content/guides/storage/s3/authentication.mdx index 07070181f3b..bdd21c52270 100644 --- a/apps/docs/content/guides/storage/s3/authentication.mdx +++ b/apps/docs/content/guides/storage/s3/authentication.mdx @@ -70,6 +70,15 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa + + +On [local development](/docs/guides/local-development), use these values: + +- `region`: `local` +- `endpoint`: IP and port e.g. `http://127.0.0.1:54321/storage/v1/s3` + + + ## Session token You can authenticate to Supabase S3 with a user JWT token to provide limited access via RLS to all S3 operations. This is useful when you want initialize the S3 client on the server scoped to a specific user, or use the S3 client directly from the client side. @@ -79,7 +88,7 @@ All S3 operations performed with the Session Token are scoped to the authenticat To authenticate with S3 using a Session Token, use the following credentials: - access_key_id: `project_ref` -- secret_access_key: `publishableKey` +- secret_access_key: `anonKey` (`publishableKey` is [not yet supported](https://github.com/supabase/storage/issues/750)) - session_token: `valid jwt token` For example, using the `aws-sdk` library: @@ -111,6 +120,11 @@ const client = new S3Client({ -On self-hosted Supabase, the `accessKeyId` is the `STORAGE_TENANT_ID` environment variable defined in the `.env` file. Refer to the [self-hosted S3 guide](/docs/guides/self-hosting/self-hosted-s3#session-token) for more details. +- On self-hosted Supabase, the `accessKeyId` is the `STORAGE_TENANT_ID` environment variable defined in the `.env` file. Refer to the [self-hosted S3 guide](/docs/guides/self-hosting/self-hosted-s3#session-token) for more details. +- On [local development](/docs/guides/local-development), use the following values: + - `region`: `local` + - `endpoint`: IP and port e.g. `http://127.0.0.1:54321/storage/v1/s3` + - `accessKeyId`: `stub` + - `secretAccessKey`: use `ANON_KEY` value from `supabase status -o env` diff --git a/apps/docs/content/guides/storage/schema/helper-functions.mdx b/apps/docs/content/guides/storage/schema/helper-functions.mdx index b8fef508159..afdc2ba8d06 100644 --- a/apps/docs/content/guides/storage/schema/helper-functions.mdx +++ b/apps/docs/content/guides/storage/schema/helper-functions.mdx @@ -61,3 +61,57 @@ with check ( bucket_id = 'cats' and storage.extension(name) = 'png' ); ``` + +### `storage.allow_only_operation()` + +Returns `true` when the current Storage API operation exactly matches the provided operation name. + +This is useful when a single SQL privilege such as `SELECT` is used by multiple Storage actions, but you want a policy to apply to only one of them, such as object listing versus object download. + +The current operation names are defined in [`src/http/routes/operations.ts`](https://github.com/supabase/storage/blob/master/src/http/routes/operations.ts). + +Storage normalizes operation names before comparing them, so both of the following forms are treated as equivalent: + +- `storage.object.list` +- `object.list` + +The comparison remains exact after normalization. Partial values such as `object` do not match `object.list`. If the current operation is not set, or the input is empty, the function returns `false`. + +**Usage** + +This example demonstrates how you would allow authenticated users to list only their own objects: + +```sql +create policy "Allow users to list their own objects" +on storage.objects +for select +to authenticated +using ( + storage.allow_only_operation('object.list') + and owner_id = (select auth.uid()::text) +); +``` + +### `storage.allow_any_operation()` + +Returns `true` when the current Storage API operation exactly matches any operation in the provided array. + +Use this when the same policy should apply to a small set of Storage actions. + +**Usage** + +This example demonstrates how you would allow authenticated users to list their own objects and read their own authenticated objects: + +```sql +create policy "Allow users to list and read their own authenticated objects" +on storage.objects +for select +to authenticated +using ( + storage.allow_any_operation(ARRAY[ + 'object.list', + 'storage.object.get_authenticated' + ]) + and owner_id = (select auth.uid()::text) +); +``` diff --git a/apps/docs/content/guides/storage/security/access-control.mdx b/apps/docs/content/guides/storage/security/access-control.mdx index 29b1ce4a1d3..909896d1a62 100644 --- a/apps/docs/content/guides/storage/security/access-control.mdx +++ b/apps/docs/content/guides/storage/security/access-control.mdx @@ -16,6 +16,8 @@ By default Storage does not allow any uploads to buckets without RLS policies. Y You can find the documentation for the storage schema [here](/docs/guides/storage/schema/design) , and to simplify the process of crafting your policies, you can utilize these [helper functions](/docs/guides/storage/schema/helper-functions) . +If you need different `SELECT` policies for different Storage actions, such as listing objects versus reading authenticated objects, use the operation-aware helpers `storage.allow_only_operation()` and `storage.allow_any_operation()` documented in [Storage Helper Functions](/docs/guides/storage/schema/helper-functions). + The RLS policies required for different operations are documented [here](/docs/reference/javascript/storage-createbucket) diff --git a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx index 01b88df6b9a..753e8b6bea9 100644 --- a/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx +++ b/apps/docs/content/guides/storage/uploads/resumable-uploads.mdx @@ -33,6 +33,12 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa Here's an example of how to upload a file using `tus-js-client`: + + + Typically we advise against using `getSession`, because the session is read from local storage and you can't trust its claims for auth decisions. In this case however, the code only needs the raw access token string to forward as a credential to Supabase storage, which validates the token server-side. Since no client-side auth decision is made based on the session data, `getSession` is appropriate here. + + + ```javascript const tus = require('tus-js-client') diff --git a/apps/docs/content/guides/telemetry/logs.mdx b/apps/docs/content/guides/telemetry/logs.mdx index e8f30e05d8c..c8957dd2465 100644 --- a/apps/docs/content/guides/telemetry/logs.mdx +++ b/apps/docs/content/guides/telemetry/logs.mdx @@ -213,6 +213,11 @@ show log_min_messages; Note that `LOG` is a higher level than `WARNING` and `ERROR`, so if your level is set to `LOG`, you will not see `WARNING` and `ERROR` messages. +### Limits and caveats + +- Postgres log events on the Supabase Platform are limited to 100,000 characters. If a log event exceeds this limit, it will be truncated. This does not apply to self-hosting. +- Internal connection logs to Postgres within the Supabase Platform by internal services are not logged. This does not apply to self-hosting. + ## Logging realtime connections Realtime doesn't log new WebSocket connections or Channel joins by default. Enable connection logging per client by including an `info` `log_level` parameter when instantiating the Supabase client. diff --git a/apps/docs/content/troubleshooting/auth-error-503-authretryablefetcherror-51b88c.mdx b/apps/docs/content/troubleshooting/auth-error-503-authretryablefetcherror-51b88c.mdx new file mode 100644 index 00000000000..5108fa277c4 --- /dev/null +++ b/apps/docs/content/troubleshooting/auth-error-503-authretryablefetcherror-51b88c.mdx @@ -0,0 +1,29 @@ +--- +title = "Auth error: 503 AuthRetryableFetchError" +date_created = "2026-05-07T09:19:37+00:00" +topics = [ "auth" ] +keywords = [] +[[errors]] +http_status_code = 503 +message = "AuthRetryableFetchError" + +--- + +A `503` status code accompanied by an `AuthRetryableFetchError` typically indicates that the Auth (GoTrue) service is failing to initialize because it cannot load its configuration. + +**Why Does This Happen?** +This issue is most commonly caused by an invalid duration string in the `GOTRUE_SESSIONS_TIMEBOX` setting. This occurs if the configured value: + +- Exceeds the maximum supported duration (approximately hundreds of years). +- Uses an incorrect or unsupported time unit format. + +When the service encounters an invalid configuration value during startup, it fails to initialize, resulting in fetch errors for all authentication requests. + +**How to Resolve:** + +1. Navigate to the [Sessions](/dashboard/project/_/auth/sessions) settings in the dashboard. +2. Locate the **Timebox** duration field within the **User Sessions** section. +3. Replace the current value with a smaller, valid duration or fallback to the default value (`4320` hours). +4. Save your changes. + +Updating this field triggers a configuration reload, which allows the Auth service to restart and resume normal operation. diff --git a/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx b/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx index 5e917995a23..e1900cefcfc 100644 --- a/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx +++ b/apps/docs/content/troubleshooting/edge-function-shutdown-reasons-explained.mdx @@ -51,7 +51,7 @@ These events are surfaced through logs and observability tools, allowing you to ### CPUTime -**What it means:** The worker consumed more CPU time than allowed. CPU time measures actual processing cycles used by your code, excluding time spent waiting for I/O or sleeping. Currently limited to 200 milliseconds. +**What it means:** The worker consumed more CPU time than allowed. CPU time measures actual processing cycles used by your code, excluding time spent waiting for I/O or sleeping. Currently limited to 2000 milliseconds. **When it happens:** Your function is performing too much computation. This includes complex calculations, data processing, encryption, or other CPU-intensive operations. diff --git a/apps/docs/content/troubleshooting/project-status-reports-unhealthy-services.mdx b/apps/docs/content/troubleshooting/project-status-reports-unhealthy-services.mdx index 9fb536f57ed..706ec05040f 100644 --- a/apps/docs/content/troubleshooting/project-status-reports-unhealthy-services.mdx +++ b/apps/docs/content/troubleshooting/project-status-reports-unhealthy-services.mdx @@ -18,3 +18,11 @@ Possible resolutions: - Restart the database in [Project Settings](/dashboard/project/_/settings/general) (this may be only a temporary fix if the project is undersized / unoptimized). - Increase project resources in [Compute and Disk](/dashboard/project/_/settings/compute-and-disk). - [Performance Tune](/docs/guides/platform/performance) the database. + +The "Edge Functions Unhealthy" indicator is based on a platform-level health check, not the project's own functions. Confirm the actual functions work by invoking them directly. If functions are working fine, this is likely a false positive. + +Check the following if only Edge Functions service reports unhealthy: + +- Verify actual function behavior by invoking the edge function directly (e.g., via cURL with a CORS request). If it responds correctly, the health indicator is likely a false positive. +- Check invocation logs. Filter logs using the keyword `~"health"` to see if health check calls are failing and if any errors are presented. +- Restart the database in [Project Settings](/dashboard/project/_/settings/general). This has resolved the unhealthy state in some cases, particularly when it's a transient issue. diff --git a/apps/docs/content/troubleshooting/supavisor-error-circuit-breaker-open-after-password-rotation-0fdb72.mdx b/apps/docs/content/troubleshooting/supavisor-error-circuit-breaker-open-after-password-rotation-0fdb72.mdx new file mode 100644 index 00000000000..8516e7917db --- /dev/null +++ b/apps/docs/content/troubleshooting/supavisor-error-circuit-breaker-open-after-password-rotation-0fdb72.mdx @@ -0,0 +1,31 @@ +--- +title = "Supavisor error: 'Circuit breaker open' after password rotation" +date_created = "2026-05-07T09:17:40+00:00" +topics = [ "database", "supavisor" ] +keywords = [] +--- + +If you are seeing `FATAL: Circuit breaker open` with one of the error messages: + +- `failed to retrieve database credentials after multiple attempts, new connections are temporarily blocked` +- `too many failed attempts to connect to the database, new connections are temporarily blocked` +- `too many authentication failures, new connections are temporarily blocked` + +it indicates that the connection pooler has blocked the origin IP to protect the database. + +This typically happens when clients attempt to connect to the pooler (ports 5432 or 6543) using an outdated password after a rotation. + +**Why Does This Happen?** + +- To prevent overloading the database with failed authentication attempts, Supavisor triggers a circuit breaker. +- Once triggered, the origin IP is blocked for up to 2 minutes. +- If clients continue attempting connections with invalid credentials during this window, the circuit breaker will reapply, effectively extending the lockout. + +**How to Resolve This Issue:** + +- **Stop clients:** Shut down application instances or services currently attempting to connect to the pooler with the wrong credentials. +- **Update credentials:** Ensure all connection strings and environment variables are updated with the new database password. +- **Restart services:** Resume application traffic and the clients will be able to reconnect after the lockout period has elapsed. + +**Prevention:** +To avoid this IP lockout issue in the future, stop application instances before rotating the database password. Only restart your services once the new credentials have been successfully applied to your configuration. diff --git a/apps/docs/content/troubleshooting/too-many-connections-for-database-postgres.mdx b/apps/docs/content/troubleshooting/too-many-connections-for-database-postgres.mdx new file mode 100644 index 00000000000..28dd945172e --- /dev/null +++ b/apps/docs/content/troubleshooting/too-many-connections-for-database-postgres.mdx @@ -0,0 +1,27 @@ +--- +title = "Database: \"Error: too many connections for database \"postgres\"\"" +topics = [ "database" ] +keywords = [] +--- + +When getting an error where your connections are overwhelmed `Error: too many connections for database "postgres"` + +## Why this occurs + +This issue occurs when `datconnlimit` gets modified. The default value for `datconnlimit` is -1. +https://www.postgresql.org/docs/current/catalog-pg-database.html + +## To check and resolve + +1. **Check the value for `datconnlimit` using the query below** + + ```bash + select datconnlimit from pg_database where datname='postgres'; + ``` + + - If the value you see is 0 or any other value other than -1, proceed with the next step. + +2. **Update `datconnlimit` to DEFAULT** + ```bash + ALTER DATABASE postgres CONNECTION LIMIT DEFAULT; + ``` diff --git a/apps/docs/docs/ref/dart/upgrade-guide.mdx b/apps/docs/docs/ref/dart/upgrade-guide.mdx index 2ecc588f7b9..8c777a47e97 100644 --- a/apps/docs/docs/ref/dart/upgrade-guide.mdx +++ b/apps/docs/docs/ref/dart/upgrade-guide.mdx @@ -44,7 +44,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart main.dart await Supabase.initialize( url: supabaseUrl, - anonKey: supabaseKey, + publishableKey: publishableKey, authFlowType: AuthFlowType.pkce, storageRetryAttempts: 10, realtimeClientOptions: const RealtimeClientOptions( @@ -59,7 +59,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart main.dart await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_ANON_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', authOptions: const FlutterAuthClientOptions( authFlowType: AuthFlowType.pkce, ), @@ -295,7 +295,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_ANON_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', authFlowType: AuthFlowType.implicit, // set to implicit by default ); ``` @@ -305,7 +305,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_ANON_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', authOptions: FlutterAuthClientOptions( authFlowType: AuthFlowType.pkce, // set to pkce by default ) @@ -339,7 +339,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_ANON_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', authCallbackUrlHostname: 'auth-callback', ); ``` @@ -349,7 +349,7 @@ We have brought numerous updates behind the scenes to make the SDK work more int ```dart await Supabase.initialize( url: 'SUPABASE_URL', - anonKey: 'SUPABASE_ANON_KEY', + publishableKey: 'SUPABASE_PUBLISHABLE_KEY', ); ``` diff --git a/apps/docs/docs/ref/javascript/typescript-support.mdx b/apps/docs/docs/ref/javascript/typescript-support.mdx index 6525afa9664..234d127ab7c 100644 --- a/apps/docs/docs/ref/javascript/typescript-support.mdx +++ b/apps/docs/docs/ref/javascript/typescript-support.mdx @@ -88,7 +88,7 @@ With TypeScript, `supabase-js` detects things like `not null` constraints and [g const supabase = createClient( process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY + process.env.SUPABASE_PUBLISHABLE_KEY ) ``` diff --git a/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx b/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx index 3c754c40a75..012df19d55e 100644 --- a/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx +++ b/apps/docs/docs/ref/javascript/v1/upgrade-guide.mdx @@ -43,7 +43,7 @@ _Optionally_ if you are using custom configuration with `createClient` then foll ```ts src/supabaseClient.ts -const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { +const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, { schema: 'custom', persistSession: false, }) @@ -53,7 +53,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { ```ts src/supabaseClient.ts -const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { +const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, { db: { schema: 'custom', }, diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 8469b004cbc..cc7460313a1 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -24,7 +24,7 @@ function GlobalProviders({ children }: PropsWithChildren) { - +
diff --git a/apps/docs/features/docs/GuidesMdx.template.tsx b/apps/docs/features/docs/GuidesMdx.template.tsx index 97758032965..d66ae606a5b 100644 --- a/apps/docs/features/docs/GuidesMdx.template.tsx +++ b/apps/docs/features/docs/GuidesMdx.template.tsx @@ -1,16 +1,16 @@ -import { ExternalLink } from 'lucide-react' -import { type ReactNode } from 'react' -import ReactMarkdown from 'react-markdown' - -import { cn } from 'ui' - import Breadcrumbs from '~/components/Breadcrumbs' import GuidesSidebar from '~/components/GuidesSidebar' import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client' import { MDXRemoteBase } from '~/features/docs/MdxBase' import type { WithRequired } from '~/features/helpers.types' +import { resolveBreadcrumbs } from '~/lib/breadcrumbs' import { type GuideFrontmatter } from '~/lib/docs' +import { breadcrumbListSchema, serializeJsonLd } from '~/lib/json-ld' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import { ExternalLink } from 'lucide-react' +import { type ReactNode } from 'react' +import ReactMarkdown from 'react-markdown' +import { cn } from 'ui' const EDIT_LINK_SYMBOL = Symbol('edit link') interface EditLink { @@ -54,14 +54,27 @@ interface BaseGuideTemplateProps { children?: ReactNode editLink: EditLink mdxOptions?: SerializeOptions + pathname: `/${string}` } type GuideTemplateProps = | WithRequired | WithRequired -const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideTemplateProps) => { +const GuideTemplate = ({ + meta, + content, + children, + editLink, + mdxOptions, + pathname, +}: GuideTemplateProps) => { const hideToc = meta?.hideToc || meta?.hide_table_of_contents + const breadcrumbChain = resolveBreadcrumbs(pathname) + const breadcrumbJsonLd = + breadcrumbChain.length > 0 + ? serializeJsonLd(breadcrumbListSchema({ pathname, chain: breadcrumbChain })) + : null return ( @@ -74,6 +87,12 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT 'col-span-12 md:col-span-9' )} > + {breadcrumbJsonLd && ( +