Merge branch 'master' into chore/integrations-ui-refine

This commit is contained in:
Raminder Singh committed 2026-05-08 16:13:45 +05:30
commit 5ec5232e4f
990 files changed
+101030 -74884

No files matched your search

+14 -2
View File
@@ -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'
+22
View File
@@ -126,6 +126,17 @@ export const Index: Record<string, any> = {
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<string, any> = {
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",
+4 -5
View File
@@ -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 (
<JotaiProvider>
<NextThemesProvider {...props}>
<ThemeProvider>
<TooltipProvider delayDuration={0}>
<MobileSidebarProvider>{children}</MobileSidebarProvider>
</TooltipProvider>
</NextThemesProvider>
</ThemeProvider>
</JotaiProvider>
)
}
+3 -7
View File
@@ -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) {
/>
</head>
<body>
<ThemeProvider
themes={['dark', 'light', 'classic-dark']}
defaultTheme="system"
enableSystem
>
<Providers>
<div vaul-drawer-wrapper="">
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
</div>
<Toaster />
</ThemeProvider>
</Providers>
</body>
</html>
)
@@ -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.
<ComponentPreview
name="admonition-description-only"
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
/>
### Success
Use `success` for positive, completed states where the user does not need to take corrective action.
<ComponentPreview
name="admonition-success"
className="[&_.preview>[data-orientation=vertical]]:sm:max-w-[70%]"
/>
### Destructive
<ComponentPreview
+2 -1
View File
@@ -20,6 +20,7 @@
"@hookform/resolvers": "^3.1.1",
"@tanstack/react-table": "^8.21.3",
"contentlayer2": "0.4.6",
"common": "workspace:*",
"date-fns": "^2.30.0",
"dayjs": "1.11.13",
"eslint-config-supabase": "workspace:*",
@@ -29,7 +30,7 @@
"markdown-wasm": "^1.2.0",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "^0.3.0",
"next-themes": "catalog:",
"react": "catalog:",
"react-data-grid": "7.0.0-beta.47",
"react-day-picker": "^9.11.1",
@@ -0,0 +1,10 @@
import { Admonition } from 'ui-patterns/admonition'
export default function AdmonitionDescriptionOnly() {
return (
<Admonition
type="default"
description="Changes to these settings can take a few minutes to appear across all projects."
/>
)
}
@@ -0,0 +1,11 @@
import { Admonition } from 'ui-patterns/admonition'
export default function AdmonitionSuccess() {
return (
<Admonition
type="success"
title="Connection confirmed"
description="You can now close this tab."
/>
)
}
@@ -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"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -1,4 +1,4 @@
import { Button, Checkbox, Input, Textarea } from 'ui'
import { Button, Checkbox, Input_Shadcn_ as Input, Textarea } from 'ui'
import {
Field,
FieldDescription,
@@ -1,4 +1,4 @@
import { Input } from 'ui'
import { Input_Shadcn_ as Input } from 'ui'
import {
Field,
FieldDescription,
@@ -1,4 +1,4 @@
import { Input } from 'ui'
import { Input_Shadcn_ as Input } from 'ui'
import {
Field,
FieldDescription,
@@ -1,4 +1,4 @@
import { Button, Input, Textarea } from 'ui'
import { Button, Input_Shadcn_ as Input, Textarea } from 'ui'
import {
Field,
FieldContent,
@@ -12,7 +12,7 @@ import {
FormItem,
FormLabel,
FormMessage,
Input,
Input_Shadcn_ as Input,
} from 'ui'
import { z } from 'zod'
@@ -8,7 +8,9 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
InputGroup,
InputGroupAddon,
InputGroupInput,
Table,
TableBody,
TableCell,
@@ -68,12 +70,16 @@ export default function PageLayoutList() {
<div className="w-full space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
<Input
placeholder="Search for a function"
size="tiny"
icon={<Search />}
className="w-full lg:w-52"
/>
<InputGroup>
<InputGroupInput
placeholder="Search for a function"
size="tiny"
className="w-full lg:w-52"
/>
<InputGroupAddon>
<Search />
</InputGroupAddon>
</InputGroup>
</div>
<Button type="primary">Create a new function</Button>
</div>
+12
View File
@@ -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',
+13 -8
View File
@@ -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%;
-21
View File
@@ -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',
},
},
},
})
+86 -5
View File
@@ -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<SearchSuggestion[]> {
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<SearchSuggestion>): 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)
`
}
+4 -4
View File
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('ai') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('ai') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['ai', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('api') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('api') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['api', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('auth') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('auth') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['auth', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('cron') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('cron') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['cron', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('database') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('database') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['database', ...(params.slug ?? [])])
)
@@ -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 (
<GuideTemplate meta={meta} editLink={editLink}>
<GuideTemplate meta={meta} editLink={editLink} pathname="/guides/database/database-advisors">
<MDXRemoteBase source={markdownIntro} />
<Heading tag="h2">Available checks</Heading>
@@ -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 []
}
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('deployment') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('deployment') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['deployment', ...(params.slug ?? [])])
)
@@ -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)
@@ -352,7 +352,11 @@ const TerraformReferencePage = async () => {
const editLink = newEditLink('supabase/terraform-provider-supabase')
return (
<GuideTemplate meta={meta} editLink={editLink}>
<GuideTemplate
meta={meta}
editLink={editLink}
pathname="/guides/deployment/terraform/reference"
>
The Terraform Provider provides access to{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/resources"
@@ -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 FunctionsGuidePage = async (props: { params: Promise<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('functions') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('functions') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['functions', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
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 ?? [])])
)
@@ -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}`}
/>
)
}
@@ -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)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('integrations') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('integrations') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['integrations', ...(params.slug ?? [])])
)
@@ -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<Params> }) =>
return <GuideTemplate {...data!} />
}
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 ?? [])])
)
@@ -36,7 +36,7 @@ const Config = () => {
const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml')
return (
<GuideTemplate meta={meta} editLink={editLink}>
<GuideTemplate meta={meta} editLink={editLink} pathname="/guides/local-development/cli/config">
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>{content}</div>
</GuideTemplate>
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('platform') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('platform') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['platform', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('queues') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('queues') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['queues', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('realtime') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('realtime') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['realtime', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('resources') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('resources') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['resources', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('security') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('security') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['security', ...(params.slug ?? [])])
)
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
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 ?? [])])
)
@@ -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"
>
<MDXRemoteBase source={descriptionMdx} />
@@ -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"
>
<MDXRemoteBase source={descriptionMdx} />
@@ -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"
>
<MDXRemoteBase source={descriptionMdx} />
@@ -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"
>
<MDXRemoteBase source={descriptionMdx} />
@@ -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<Params> }) => {
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('storage') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('storage') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['storage', ...(params.slug ?? [])])
)
@@ -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<Param
return <GuideTemplate {...data!} />
}
const generateStaticParams = IS_PROD ? genGuidesStaticParams('telemetry') : getEmptyArray
const generateStaticParams = !IS_DEV ? genGuidesStaticParams('telemetry') : getEmptyArray
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['telemetry', ...(params.slug ?? [])])
)
+1 -1
View File
@@ -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'
@@ -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 (
<div className="flex flex-col gap-3">
<Input
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Account ID"
description="Found in the upper-right corner of Apple Developer Center."
labelOptional="required"
placeholder="Apple Developer account ID, 10 alphanumeric digits"
descriptionText="Found in the upper-right corner of Apple Developer Center."
value={teamID}
onChange={(e) => setTeamID(e.target.value.trim())}
/>
<Input
>
<Input
placeholder="Apple Developer account ID, 10 alphanumeric digits"
value={teamID}
onChange={(e) => setTeamID(e.target.value.trim())}
/>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Service ID"
description="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
labelOptional="required"
placeholder="ID of the service, example: com.example.app.service"
descriptionText="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
value={serviceID}
onChange={(e) => setServiceID(e.target.value.trim())}
/>
<Input
>
<Input
placeholder="ID of the service, example: com.example.app.service"
value={serviceID}
onChange={(e) => setServiceID(e.target.value.trim())}
/>
</FormItemLayout>
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Key ID"
description="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
labelOptional="(optional)"
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
descriptionText="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
value={keyID}
onChange={(e) => setKeyID(e.target.value.trim())}
/>
>
<Input
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
value={keyID}
onChange={(e) => setKeyID(e.target.value.trim())}
/>
</FormItemLayout>
<div>
<input
type="file"
@@ -176,14 +193,14 @@ const AppleSecretGenerator = () => {
{secretKey && (
<>
<div style={{ height: '1rem' }} />
<Input
<FormItemLayout
isReactForm={false}
layout="vertical"
label="Secret Key"
value={secretKey}
descriptionText={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
reveal
copy
size="medium"
/>
description={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
>
<DataInput value={secretKey} copy reveal />
</FormItemLayout>
</>
)}
</div>
+6 -60
View File
@@ -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<HTMLDivElement> {
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)
}
@@ -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 = {
+5 -5
View File
@@ -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 = [
{
@@ -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 <Telescope width={width} height={height} className={className} />
case 'troubleshooting':
return <IconTroubleshooting width={width} height={height} className={className} />
case 'changelog':
return <ScrollText width={width} height={height} className={className} />
case 'contributing':
return <Heart width={width} height={height} className={className} />
case 'deployment':
@@ -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}`,
+5 -4
View File
@@ -8,12 +8,13 @@ To do this, you need to get the Project URL and key from [the project **Connect*
<Admonition type="note" title="Changes to API keys">
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.
</Admonition>
@@ -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*
<Admonition type="note" title="Changes to API keys">
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.
</Admonition>
@@ -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.
@@ -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<Database>(
// 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
@@ -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',
@@ -34,8 +34,11 @@ In the Settings page, look for the **Sitekey** section and copy the key.
</TabPanel>
<TabPanel id="turnstile-1" label="Turnstile">
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.
</TabPanel>
</Tabs>
@@ -15,7 +15,7 @@ There are two provider types:
<Admonition type="note">
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.
</Admonition>
@@ -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
+1 -1
View File
@@ -88,7 +88,7 @@ Future<void> main() async {
final auth0 = Auth0('AUTH0_DOMAIN', 'AUTH0_CLIENT_ID');
await Supabase.initialize(
url: 'https://<supabase-project>.supabase.co',
anonKey: 'SUPABASE_PUBLISHABLE_KEY',
publishableKey: 'SUPABASE_PUBLISHABLE_KEY',
accessToken: () async {
final credentials = await auth0.credentialsManager.credentials();
return credentials.idToken;
+1 -1
View File
@@ -89,7 +89,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
await Supabase.initialize(
url: 'https://<supabase-project>.supabase.co',
anonKey: 'SUPABASE_PUBLISHABLE_KEY',
publishableKey: 'SUPABASE_PUBLISHABLE_KEY',
accessToken: () async {
final session = await Amplify.Auth.fetchAuthSession();
final cognitoSession = session as CognitoAuthSession;
+1 -1
View File
@@ -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;
@@ -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();
+23 -22
View File
@@ -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:<name>'` | A named secret key on `apikey` |
| `'always'` | Any caller, no check (for signed webhooks) |
| Mode | Accepts |
| ---------------------- | ------------------------------------------ |
| `'user'` | A valid user JWT on `Authorization` |
| `'secret:<name>'` | A named secret key on `apikey` |
| `'publishable:<name>'` | A named publishable key on `apikey` |
| `'none'` | Any caller, no check (for signed webhooks) |
<Admonition type="tip">
@@ -171,30 +172,30 @@ See the [`@supabase/server` docs](https://github.com/supabase/server) for the fu
</Admonition>
### 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:<name>'` 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 `<name>` matches the name you gave the key. Keep `verify_jwt = false`.
`auth: 'secret:<name>'` 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 `<name>` 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
</Admonition>
### 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 {
<Admonition type="caution">
`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.
</Admonition>
### 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 })
}
@@ -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({
@@ -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({
@@ -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)
@@ -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).
@@ -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:
<AgentPluginsPanel />
## 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).
@@ -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);
}
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Angular App.'
<Admonition type="note">
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).
</Admonition>
@@ -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.
<Admonition type="tip" label="Did you know?">
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.
</Admonition>
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"
</$CodeTabs>
### 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"
/>
</$CodeTabs>
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"
</$CodeTabs>
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
@@ -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.
@@ -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
@@ -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"
/>
@@ -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!
@@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Next.js App.'
<Admonition type="note">
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).
</Admonition>
@@ -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.
</Admonition>
<$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"
</$CodeTabs>
## 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"
</$CodeTabs>
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.
<Admonition type="caution">
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).
</Admonition>
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"
</$CodeTabs>
### Email template
<Admonition type="caution">
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).
</Admonition>
#### 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
<Admonition type="tip" label="Did you know?">
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.
</Admonition>
### 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
@@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Nuxt 3 App.'
<Admonition type="note">
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).
</Admonition>
@@ -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() {
</$CodeTabs>
### Launch!
## Profile photos
Now that we have all the components in place, let's update `app.vue`:
<$CodeTabs>
```vue name=app.vue
<script setup>
const user = useSupabaseUser()
</script>
<template>
<div class="container" style="padding: 50px 0 100px 0">
<Account v-if="user" />
<Auth v-else />
</div>
</template>
```
</$CodeTabs>
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, () => {
</$CodeTabs>
### 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
<script setup>
const supabase = useSupabaseClient()
const loading = ref(true)
const username = ref('')
const website = ref('')
const avatar_path = ref('')
loading.value = true
const user = useSupabaseUser()
const { data } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.value.id)
.single()
if (data) {
username.value = data.username
website.value = data.website
avatar_path.value = data.avatar_url
}
loading.value = false
async function updateProfile() {
try {
loading.value = true
const user = useSupabaseUser()
const updates = {
id: user.value.id,
username: username.value,
website: website.value,
avatar_url: avatar_path.value,
updated_at: new Date(),
}
const { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
async function signOut() {
try {
loading.value = true
const { error } = await supabase.auth.signOut()
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
loading.value = false
}
}
</script>
<template>
<form class="form-widget" @submit.prevent="updateProfile">
<Avatar v-model:path="avatar_path" @upload="updateProfile" />
<div>
<label for="email">Email</label>
<input id="email" type="text" :value="user.email" disabled />
</div>
<div>
<label for="username">Name</label>
<input id="username" type="text" v-model="username" />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="url" v-model="website" />
</div>
<div>
<input
type="submit"
class="button primary block"
:value="loading ? 'Loading ...' : 'Update'"
:disabled="loading"
/>
</div>
<div>
<button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
</div>
</form>
<div class="container" style="padding: 50px 0 100px 0">
<Account v-if="user" />
<Auth v-else />
</div>
</template>
```
</$CodeTabs>
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!
@@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your React App.'
<Admonition type="note">
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).
</Admonition>
@@ -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.
<Admonition type="tip" label="Did you know?">
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.
</Admonition>
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!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your RedwoodJS App.'
<Admonition type="note">
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).
</Admonition>
@@ -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
</Admonition>
### 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
</$CodeTabs>
### 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 (
<div className="form-widget">
{/* Add to the body */}
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}
/>
{/* ... */}
</div>
)
```bash
yarn rw dev
```
</$CodeTabs>
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!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Refine App.'
<Admonition type="note">
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).
</Admonition>
@@ -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 `<Refine />` 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 `<Refine />` component
In order to add login and user profile pages in this App, tweak the `<Refine />` component inside `App.tsx`.
The `App.tsx` file initially looks like this:
<$CodeTabs>
@@ -122,7 +124,7 @@ export default App
Focus on the [`<Refine />`](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 `<Auth />` component and the routes for `index` path with the `<Account />` 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!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SolidJS App.'
<Admonition type="note">
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).
</Admonition>
@@ -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"
</$CodeTabs>
## 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
At this stage you have a fully functional application!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Svelte App.'
<Admonition type="note">
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).
</Admonition>
@@ -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!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SvelteKit App.'
<Admonition type="note">
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).
</Admonition>
@@ -132,7 +132,7 @@ meta="name=src/routes/+layout.svelte"
</$CodeTabs>
## 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"
</$CodeTabs>
### 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
</Admonition>
### 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"
</$CodeTabs>
### 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"
</$CodeTabs>
### 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
### 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"
/>
</$CodeTabs>
At this stage you have a fully functional application!
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your SwiftUI App.'
<Admonition type="note">
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).
</Admonition>
@@ -273,47 +273,9 @@ struct UpdateProfileParams: Encodable {
</$CodeTabs>
### 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
}
}
}
}
}
```
</$CodeTabs>
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
}
}
}
}
}
```
</$CodeTabs>
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!
@@ -10,7 +10,7 @@ description: 'Learn how to use Supabase in your Vue 3 App.'
<Admonition type="note">
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).
</Admonition>
@@ -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!
@@ -116,7 +116,7 @@ As a prerequisite, you must install a container runtime compatible with Docker A
<Admonition type="caution">
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
@@ -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
@@ -33,7 +33,7 @@ If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefi
<Admonition type="note" label="Using Envoy instead of Kong?">
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.
</Admonition>
@@ -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.
<Tabs
scrollable
@@ -68,7 +68,7 @@ defaultActiveId="caddy"
[Caddy](https://caddyserver.com/) automatically provisions and renews Let's Encrypt TLS certificates with zero configuration. It also handles HTTP-to-HTTPS redirects, WebSocket upgrades, and HTTP/2 and HTTP/3 out of the box.
Start Caddy by using the pre-configured `docker-compose.caddy.yml` overlay:
Start Caddy by using the pre-configured `docker-compose.caddy.yml` override:
```sh
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
@@ -81,7 +81,7 @@ Caddy configuration is in `volumes/proxy/caddy/Caddyfile`.
This option uses a third-party Nginx Docker image ([`jonasal/nginx-certbot`](https://github.com/JonasAlfredsson/docker-nginx-certbot)), which includes Certbot for automatic Let's Encrypt certificate issuance and renewal in a single container.
Start Nginx by using the pre-configured `docker-compose.nginx.yml` overlay:
Start Nginx by using the pre-configured `docker-compose.nginx.yml` override:
```sh
docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
@@ -88,7 +88,7 @@ Depending on your setup, you may need to adjust these values - for example, to u
### Using RustFS
{/* supa-mdx-lint-disable-next-line Rule003Spelling */}
An overlay `docker-compose.rustfs.yml` configuration can be added to enable RustFS container and provide an S3-compatible API for Storage backend:
An override `docker-compose.rustfs.yml` can be added to enable RustFS container and provide an S3-compatible API for Storage backend:
```sh
docker compose -f docker-compose.yml -f docker-compose.rustfs.yml up -d
@@ -109,7 +109,7 @@ MinIO no longer publishes open source Docker images or maintains their open sour
</Admonition>
{/* 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
@@ -70,6 +70,15 @@ Instead of `https://project-id.supabase.co` use `https://project-id.storage.supa
</Tabs>
<Admonition type="note">
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`
</Admonition>
## 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({
<Admonition type="note">
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`
</Admonition>
@@ -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)
);
```
@@ -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).
<Admonition type="note">
The RLS policies required for different operations are documented [here](/docs/reference/javascript/storage-createbucket)
@@ -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`:
<Admonition type="note">
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.
</Admonition>
```javascript
const tus = require('tus-js-client')
@@ -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.
@@ -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.
@@ -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.
Loaded 100 of 990 files, more files were not shown because too many files have changed in this diff. Show more