Merge branch 'master' into alan/market-716-one-more-additional-component-for-go-pages

This commit is contained in:
Alan Daniel committed 2026-03-26 17:14:34 -04:00
commit a96b60b73a
543 files changed
+115179 -59011

No files matched your search

+2 -3
View File
@@ -1,5 +1,5 @@
---
description: "Studio: form patterns (page layouts + side panels) and react-hook-form conventions"
description: 'Studio: form patterns (page layouts + side panels) and react-hook-form conventions'
globs:
- apps/studio/**/*.{ts,tsx}
alwaysApply: false
@@ -29,7 +29,6 @@ Use the Design System UI pattern docs as the source of truth:
## Actions and state
- Handle dirty state (`form.formState.isDirty`) to show Cancel and to disable Save.
- Handle dirty state by destructuring `isDirty` from `formState` (`const { isDirty } = form.formState`) then use it to show Cancel and to disable Save.
- Show loading on submit buttons via `loading`.
- When submit button is outside the `<form>`, set a stable `formId` and use the button’s `form` prop.
+16 -4
View File
@@ -117,10 +117,6 @@ await waitForApiResponse(page, 'pg-meta', ref, 'tables')
// ✅ Acceptable - waiting for client-side debounce
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // Allow debounce to complete
// ✅ Acceptable - waiting for clipboard API
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
```
## Test Structure
@@ -274,6 +270,22 @@ import {
} from '../utils/wait-for-response.js'
```
### Use the existing assertions utilities
#### Clipboard assertions
```ts
// ❌ Avoid - brittle hard coded timeout
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
// ✅ Good - this utility function uses Playwright auto-retries mechanisms
await expectClipboardValue({
page,
value: 'expectedValue'
})
```
## API Mocking
### Mock APIs for isolated testing
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- name: Generate token
id: app-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
run: cd apps/studio && pnpm evals:setup
- name: Run Evals
uses: braintrustdata/eval-action@v1
uses: braintrustdata/eval-action@c0dd75b29984a0cc63a827d6e8da2f23f2752be4 # v1.0.16
with:
api_key: ${{ secrets.BRAINTRUST_API_KEY }}
runtime: node
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- name: cache cargo
id: cache-cargo
if: steps.filter.outputs.docs == 'true'
uses: actions/cache@v4
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with:
path: |
~/.cargo/bin/
@@ -21,6 +21,7 @@ jobs:
sparse-checkout: |
apps/docs
patches
packages/generator
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
name: Install pnpm
+33
View File
@@ -1963,6 +1963,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"key-value-field-array-demo": {
name: "key-value-field-array-demo",
type: "components:example",
registryDependencies: ["button","form","input"],
component: React.lazy(() => import("@/registry/default/example/key-value-field-array-demo")),
source: "",
files: ["registry/default/example/key-value-field-array-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"field-choice-card": {
name: "field-choice-card",
type: "components:example",
@@ -2678,6 +2689,28 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"error-display-demo": {
name: "error-display-demo",
type: "components:example",
registryDependencies: ["error-display"],
component: React.lazy(() => import("@/registry/default/example/error-display-demo")),
source: "",
files: ["registry/default/example/error-display-demo.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"error-display-with-children": {
name: "error-display-with-children",
type: "components:example",
registryDependencies: ["error-display"],
component: React.lazy(() => import("@/registry/default/example/error-display-with-children")),
source: "",
files: ["registry/default/example/error-display-with-children.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"logs-bar-chart": {
name: "logs-bar-chart",
type: "components:example",
+5
View File
@@ -212,6 +212,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/status-codes',
items: [],
},
{
title: 'Key/Value Field Array',
href: '/docs/fragments/key-value-field-array',
items: [],
},
],
},
{
@@ -0,0 +1,43 @@
---
title: Key/Value Field Array
description: A shared form fragment for repeated text key/value pairs.
component: true
fragment: true
---
<ComponentPreview
name="key-value-field-array-demo"
description="A shared form fragment for repeated text key/value pairs."
peekCode
showDottedGrid
wide
/>
## Usage
Use `KeyValueFieldArray` when each row is two text inputs backed by `react-hook-form`, such as HTTP headers, query parameters, or configuration parameters.
```tsx
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
```
```tsx
<KeyValueFieldArray
control={form.control}
name="headers"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
/>
```
`KeyValueFieldArray` owns the row add/remove behavior and renders the per-input form messages for you. Compose it inside `FormItemLayout` when you want the standard label, description, and message treatment around the entire section.
## When to use it
- Use a plain field array for repeated single values such as redirect URIs.
- Use `KeyValueFieldArray` for repeated text/text pairs such as headers, parameters, and config entries.
- Build a custom row UI instead when each row mixes different controls, such as a text input paired with a `Select`.
@@ -27,6 +27,15 @@ Forms in side panels (Sheets) use `FormItemLayout` with `layout="horizontal"` on
wide
/>
## Field Arrays
The form previews above include both repeated-field patterns used across Studio:
- **Field Array** for repeated single-value rows such as redirect URIs.
- **Key/Value Field Array** for repeated text pairs such as headers, parameters, and config entries.
Use the shared [Key/Value Field Array](../fragments/key-value-field-array) fragment when each row is two text inputs managed by `react-hook-form`. Keep using a plain field array when each row is just one input, and build a custom row when the cells are mixed controls.
## Best Practices
1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
@@ -40,7 +49,7 @@ Forms in side panels (Sheets) use `FormItemLayout` with `layout="horizontal"` on
4. **Use Cards for grouping**: Wrap form sections in `Card` components with `CardContent` and `CardFooter` for actions.
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`.
5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate)
6. **Error handling**: Always use mutations with `onSuccess` and `onError` callbacks that show toast notifications.
@@ -104,8 +104,13 @@ Studio implementation (preferred in Studio code):
import { DiscardChangesConfirmationDialog } from 'components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { useConfirmOnClose } from 'hooks/ui/useConfirmOnClose'
const form = useForm(...)
// Always destructure formState values otherwise they won't be updated
// See https://react-hook-form.com/docs/useform/formstate
const { isDirty } = form.formState
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
checkIsDirty: () => form.formState.isDirty,
checkIsDirty: () => isDirty,
onClose,
})
+1 -1
View File
@@ -8,7 +8,7 @@
"dev": "next dev --turbopack --port 3003",
"dev:full": "concurrently \"pnpm dev\" \"pnpm content:dev\"",
"build": "pnpm run content:build && pnpm run build:registry && next build --turbopack",
"build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"build:registry": "tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache",
"start": "next start",
"lint": "eslint .",
"content:dev": "contentlayer2 dev",
@@ -1,4 +1,4 @@
import { ErrorDisplay } from 'ui-patterns/ErrorDisplay'
import { ErrorDisplay } from 'ui-patterns/ErrorDisplay/ErrorDisplay'
export default function ErrorDisplayDemo() {
return (
@@ -1,4 +1,4 @@
import { ErrorDisplay } from 'ui-patterns/ErrorDisplay'
import { ErrorDisplay } from 'ui-patterns/ErrorDisplay/ErrorDisplay'
export default function ErrorDisplayWithChildren() {
return (
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import {
@@ -30,6 +30,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import {
MultiSelector,
MultiSelectorContent,
@@ -61,6 +62,7 @@ const formSchema = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
duration: z.number().min(5).max(30),
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
apiKey: z.string().optional(),
})
@@ -91,6 +93,7 @@ export default function FormPatternsPageLayout() {
password: '',
duration: 10,
redirectUris: [{ value: '' }],
httpHeaders: [{ key: '', value: '' }],
apiKey: fakeApiKey,
},
})
@@ -285,7 +288,7 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setLogoFile(undefined)
setLogoUrl(undefined)
@@ -389,7 +392,7 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setUploadedFiles((prev) =>
prev.filter((_, i) => i !== idx)
@@ -669,7 +672,8 @@ export default function FormPatternsPageLayout() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
htmlType="button"
icon={<Trash size={12} />}
onClick={() => remove(index)}
/>
)}
@@ -679,6 +683,7 @@ export default function FormPatternsPageLayout() {
))}
<Button
type="default"
htmlType="button"
icon={<Plus />}
onClick={() => append({ value: '' })}
>
@@ -690,6 +695,33 @@ export default function FormPatternsPageLayout() {
/>
</CardContent>
{/* Key/Value Field Array */}
<CardContent>
<FormField_Shadcn_
control={form.control}
name="httpHeaders"
render={() => (
<FormItemLayout
layout="flex-row-reverse"
label="Key/Value Field Array"
description="Repeated text pairs for headers, parameters, and config entries"
>
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</FormItemLayout>
)}
/>
</CardContent>
{/* Action Field */}
<CardContent>
<FormItemLayout
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { format } from 'date-fns'
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
import { useRef, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import {
@@ -34,6 +34,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import {
MultiSelector,
MultiSelectorContent,
@@ -58,6 +59,7 @@ const formSchema = z.object({
password: z.string().min(8, 'Password must be at least 8 characters'),
duration: z.number().min(5).max(30),
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
apiKey: z.string().optional(),
})
@@ -89,6 +91,7 @@ export default function FormPatternsSidePanel() {
password: '',
duration: 10,
redirectUris: [{ value: '' }],
httpHeaders: [{ key: '', value: '' }],
apiKey: fakeApiKey,
},
})
@@ -303,7 +306,7 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setLogoFile(undefined)
setLogoUrl(undefined)
@@ -409,7 +412,7 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
icon={<Trash size={12} />}
onClick={() => {
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
}}
@@ -702,7 +705,8 @@ export default function FormPatternsSidePanel() {
<Button
type="default"
size="tiny"
icon={<Trash2 size={12} />}
htmlType="button"
icon={<Trash size={12} />}
onClick={() => remove(index)}
/>
)}
@@ -712,6 +716,7 @@ export default function FormPatternsSidePanel() {
))}
<Button
type="default"
htmlType="button"
icon={<Plus />}
onClick={() => append({ value: '' })}
>
@@ -725,6 +730,37 @@ export default function FormPatternsSidePanel() {
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
{/* Key/Value Field Array */}
<SheetSection>
<FormField_Shadcn_
control={form.control}
name="httpHeaders"
render={() => (
<FormItemLayout
layout="horizontal"
label="Key/Value Field Array"
description="Repeated text pairs for headers, parameters, and config entries"
>
<div className="col-span-6">
<KeyValueFieldArray
control={form.control}
name="httpHeaders"
keyFieldName="key"
valueFieldName="value"
createEmptyRow={() => ({ key: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</div>
</FormItemLayout>
)}
/>
</SheetSection>
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
{/* Action Field */}
<SheetSection>
<FormItemLayout
@@ -0,0 +1,57 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { Button, Form_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
import { z } from 'zod'
const formSchema = z.object({
headers: z.array(
z.object({
name: z.string(),
value: z.string(),
})
),
})
export default function KeyValueFieldArrayDemo() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
headers: [{ name: 'x-client-info', value: 'studio-docs' }],
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form_Shadcn_ {...form}>
<form className="w-full max-w-2xl" onSubmit={form.handleSubmit(onSubmit)}>
<FormItemLayout
label="HTTP headers"
description="Use KeyValueFieldArray for repeated text key/value pairs."
>
<KeyValueFieldArray
control={form.control}
name="headers"
keyFieldName="name"
valueFieldName="value"
createEmptyRow={() => ({ name: '', value: '' })}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
addLabel="Add header"
removeLabel="Remove header"
/>
</FormItemLayout>
<div className="mt-4">
<Button size="tiny" type="primary" htmlType="submit">
Submit
</Button>
</div>
</form>
</Form_Shadcn_>
)
}
+6
View File
@@ -1107,6 +1107,12 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/form-item-layout-demo.tsx'],
},
{
name: 'key-value-field-array-demo',
type: 'components:example',
registryDependencies: ['button', 'form', 'input'],
files: ['example/key-value-field-array-demo.tsx'],
},
{
name: 'field-choice-card',
type: 'components:example',
-20
View File
@@ -1,20 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules"]
}
+4 -21
View File
@@ -1,40 +1,23 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "tsconfig/base.json",
"extends": "tsconfig/nextjs.json",
"compilerOptions": {
"target": "es5",
"allowJs": false,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"paths": {
"@/*": ["./*"],
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
"contentlayer/generated": ["./.contentlayer/generated"],
"icons/*": ["./../../packages/icons/*"]
},
"plugins": [
{
"name": "next"
}
]
"plugins": [{ "name": "next" }]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated",
"./../../packages/ui/src/**/*.d.ts"
".contentlayer/generated"
],
"exclude": ["node_modules", "./scripts/build-registry.mts"]
}
-13
View File
@@ -1,13 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"isolatedModules": false
},
"include": [".contentlayer/generated", "scripts/**/*.ts"],
"exclude": ["node_modules"]
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.scss' {
export const styles: Record<string, string>
export default styles
}
+1
View File
@@ -32,6 +32,7 @@ public/llms.txt
public/llms/
# Generated guide markdown files
public/docs/
public/docs.tar.gz
# Copied examples folder
/examples/
@@ -14,7 +14,10 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
try {
const content = await fs.readFile(filePath, 'utf-8')
return new NextResponse(content, {
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=3600',
},
})
} catch {
return new NextResponse('Not found', { status: 404 })
+4 -18
View File
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { getGitHubFileContents } from '~/lib/octokit'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
@@ -83,23 +83,9 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
let response: Response
try {
response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
} catch (err) {
throw new Error(`Failed to fetch Python vecs docs from GitHub (network error): ${err}`)
}
if (!response.ok) {
throw new Error(
`Failed to fetch Python vecs docs from GitHub: ${response.status} ${response.statusText}`
)
}
let content = await response.text()
content = removeRedundantH1(content)
const content = removeRedundantH1(
await getGitHubFileContents({ org, repo, path: `${docsDir}/${remoteFile}`, branch })
)
return {
pathname: `/guides/ai/python/${slug}` satisfies `/${string}`,
@@ -1,4 +1,3 @@
import { Octokit } from '@octokit/core'
import { capitalize } from 'lodash-es'
import rehypeSlug from 'rehype-slug'
import { Heading } from 'ui'
@@ -7,7 +6,7 @@ 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 { fetchRevalidatePerDay } from '~/features/helpers.fetch'
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'
@@ -144,9 +143,7 @@ const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (
* Fetch lint remediation Markdown from external repo
*/
const getLints = async () => {
const octokit = new Octokit({ request: { fetch: fetchRevalidatePerDay } })
const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: org,
repo: repo,
path: docsDir,
@@ -154,10 +151,13 @@ const getLints = async () => {
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
request: OCTOKIT_RETRY_OPTIONS,
})
if (response.status >= 400) {
throw Error(`Could not get contents of repo ${org}/${repo}`)
throw new Error(
`Failed to fetch ${org}/${repo}/contents/${docsDir} docs from GitHub: ${response.status}`
)
}
if (!Array.isArray(response.data)) {
@@ -170,15 +170,7 @@ const getLints = async () => {
const lints = await Promise.all(
lintsList.map(async ({ path }) => {
const fileResponse = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
)
if (fileResponse.status >= 400) {
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
}
const content = await fileResponse.text()
const content = await getGitHubFileContents({ org, repo, path, branch })
return {
path: getBasename(path),
@@ -360,10 +360,22 @@ const getContent = async (params: Params) => {
const repoPath = `${org}/${repo}/${tag}/${docsDir}/${remoteFile}`
editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}`
const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
cache: 'force-cache',
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
})
let response: Response
try {
response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
cache: 'force-cache',
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
})
} catch (err) {
throw new Error(`Failed to fetch wrappers docs from GitHub (network error): ${err}`)
}
if (!response.ok) {
throw new Error(
`Failed to fetch wrappers docs from GitHub: ${response.status} ${response.statusText}`
)
}
const rawContent = await response.text()
assetsBaseUrl = `https://raw.githubusercontent.com/${org}/${repo}/${tag}/docs/assets/`
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { getGitHubFileContents } from '~/lib/octokit'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
@@ -82,22 +82,9 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
let response: Response
try {
response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
} catch (err) {
throw new Error(`Failed to fetch CI docs from GitHub (network error): ${err}`)
}
if (!response.ok) {
throw new Error(
`Failed to fetch CI docs from GitHub: ${response.status} ${response.statusText}`
)
}
const content = removeRedundantH1(await response.text())
const content = removeRedundantH1(
await getGitHubFileContents({ org, repo, path: `${docsDir}/${remoteFile}`, branch })
)
return {
pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`,
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
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'
@@ -115,22 +115,12 @@ const getContent = async ({ slug }: Params) => {
`${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
)
let response: Response
try {
response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
)
} catch (err) {
throw new Error(`Failed to fetch Terraform docs from GitHub (network error): ${err}`)
}
if (!response.ok) {
throw new Error(
`Failed to fetch Terraform docs from GitHub: ${response.status} ${response.statusText}`
)
}
let rawContent = await response.text()
let rawContent = await getGitHubFileContents({
org: terraformDocsOrg,
repo: terraformDocsRepo,
path: useRoot ? remoteFile : `${terraformDocsDocsDir}/${remoteFile}`,
branch: terraformDocsBranch,
})
// Strip out HTML comments
rawContent = rawContent.replace(/<!--.*?-->/, '')
let { content, data } = matter(rawContent)
@@ -13,7 +13,7 @@ import {
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { getGitHubFileContents } from '~/lib/octokit'
import { TabPanel, Tabs } from '~/features/ui/Tabs'
import {
terraformDocsBranch,
@@ -394,21 +394,14 @@ const TerraformReferencePage = async () => {
* Fetch JSON schema from external repo
*/
const getSchema = async () => {
let response: Response
try {
response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${terraformDocsDocsDir}/schema.json`
)
} catch (err) {
throw new Error(`Failed to fetch Terraform JSON schema from GitHub (network error): ${err}`)
}
if (!response.ok)
throw Error(
`Failed to fetch Terraform JSON schema from GitHub: ${response.status} ${response.statusText}`
)
const schema = await response.json()
const schema = JSON.parse(
await getGitHubFileContents({
org: terraformDocsOrg,
repo: terraformDocsRepo,
path: `${terraformDocsDocsDir}/schema.json`,
branch: terraformDocsBranch,
})
)
return {
schema,
@@ -0,0 +1,62 @@
import matter from 'gray-matter'
import { cache } from 'react'
import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit'
const SKILLS_REPO = {
org: 'supabase',
repo: 'agent-skills',
branch: 'main',
path: 'skills',
}
interface SkillMetadata {
name?: string
title?: string
description?: string
}
interface SkillSummary {
name: string
description: string
installCommand: string
}
async function getAiSkillsImpl(): Promise<SkillSummary[]> {
const { data: contents } = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: SKILLS_REPO.org,
repo: SKILLS_REPO.repo,
path: SKILLS_REPO.path,
ref: SKILLS_REPO.branch,
request: OCTOKIT_RETRY_OPTIONS,
})
if (!Array.isArray(contents)) {
throw new Error('Expected directory listing from GitHub agent skills repo')
}
const skillDirs = contents.filter((item) => item.type === 'dir')
const skills = await Promise.all(
skillDirs.map(async (item) => {
const skillPath = `${SKILLS_REPO.path}/${item.name}/SKILL.md`
const rawContent = await getGitHubFileContents({
org: SKILLS_REPO.org,
repo: SKILLS_REPO.repo,
branch: SKILLS_REPO.branch,
path: skillPath,
})
const { data } = matter(rawContent) as { data: SkillMetadata }
return {
name: item.name,
description: data.description || '',
installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`,
}
})
)
return skills.sort((a, b) => a.name.localeCompare(b.name))
}
export const getAiSkills = cache(getAiSkillsImpl)
@@ -0,0 +1,60 @@
import { getAiSkills } from './AiSkills.utils'
import { CopyButton } from './CopyButton'
export async function AiSkillsIndex() {
let skills: Awaited<ReturnType<typeof getAiSkills>> = []
try {
skills = await getAiSkills()
} catch {
// Swallow errors from getAiSkills to keep the page usable
}
if (!skills.length) {
return (
<div className="not-prose text-sm text-foreground-lighter">
Unable to load AI skills at the moment.
</div>
)
}
return (
<div className="not-prose overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-default">
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Skill</th>
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Description</th>
<th className="text-left py-2 text-foreground-lighter font-medium">Install command</th>
</tr>
</thead>
<tbody>
{skills.map((skill) => (
<tr key={skill.name} className="border-b border-default">
<td className="py-3 pr-4 font-mono text-xs whitespace-nowrap">
<a
href={`https://github.com/supabase/agent-skills/tree/main/skills/${skill.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-foreground hover:text-brand transition-colors"
>
{skill.name}
</a>
</td>
<td className="py-3 pr-4 text-foreground-lighter">{skill.description}</td>
<td className="w-px p-0">
<div className="h-full max-w-xs overflow-x-auto flex items-center py-3">
<div className="flex items-center gap-2 whitespace-nowrap">
<CopyButton text={skill.installCommand} />
<code className="font-mono text-xs text-foreground-lighter">
{skill.installCommand}
</code>
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
@@ -0,0 +1,29 @@
'use client'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
import { cn } from 'ui'
export function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
type="button"
onClick={handleCopy}
className={cn(
'inline-flex items-center rounded-md border p-1.5 transition-colors',
'border-default bg-surface-100 text-foreground-lighter hover:bg-surface-200 hover:text-foreground'
)}
title={copied ? 'Copied!' : 'Copy to clipboard'}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
)
}
@@ -3,7 +3,9 @@ import Layout from '~/layouts/guides'
import { getAiPrompts } from '../getting-started/ai-prompts/[slug]/AiPrompts.utils'
export default async function GettingStartedLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { prompts: await getPrompts() }
const additionalNavItems = {
prompts: await getPrompts(),
}
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
import { getGitHubFileContents } from '~/lib/octokit'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
@@ -136,23 +136,12 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
let response: Response
try {
response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`,
{ cache: 'force-cache', next: { tags: [REVALIDATION_TAGS.GRAPHQL] } }
)
} catch (err) {
throw new Error(`Failed to fetch GraphQL docs from GitHub (network error): ${err}`)
}
if (!response.ok) {
throw new Error(
`Failed to fetch GraphQL docs from GitHub: ${response.status} ${response.statusText}`
)
}
const content = await response.text()
const content = await getGitHubFileContents({
org,
repo,
path: `${docsDir}/${remoteFile}`,
branch,
})
return {
pathname: `/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
+4 -5
View File
@@ -1,18 +1,17 @@
import '@code-hike/mdx/styles'
import '@code-hike/mdx/styles.css'
import 'config/code-hike.scss'
import 'ui-patterns/ShimmeringLoader/index.css'
import '../styles/main.scss'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
import { TelemetryTagManager } from 'common'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import type { Metadata, Viewport } from 'next'
import { GlobalProviders } from '~/features/app.providers'
import { TopNavSkeleton } from '~/layouts/MainSkeleton'
import { BASE_PATH, IS_PRODUCTION } from '~/lib/constants'
import { getCustomContent } from '~/lib/custom-content/getCustomContent'
import { TelemetryTagManager } from 'common'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import type { Metadata, Viewport } from 'next'
const { metadataApplicationName, metadataTitle } = getCustomContent([
'metadata:application_name',
+12
View File
@@ -176,6 +176,18 @@ function useBreadcrumbs() {
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, [])
+9 -1
View File
@@ -30,7 +30,15 @@ function AiTools({ className }: { className?: string }) {
try {
const res = await fetch(mdUrl)
const text = await res.text()
let text: string
if (res.ok) {
text = await res.text()
} else {
// Default to HTML content within the article when no .md file is available.
text = document.getElementById('sb-docs-guide-main-article')?.innerHTML ?? ''
}
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
@@ -499,6 +499,10 @@ export const gettingstarted: NavMenuConstant = {
name: 'Prompts',
url: '/guides/getting-started/ai-prompts' as `/${string}`,
},
{
name: 'Agent Skills',
url: '/guides/getting-started/ai-skills' as `/${string}`,
},
{
name: 'Supabase MCP server',
url: '/guides/getting-started/mcp' as `/${string}`,
@@ -773,6 +777,12 @@ export const auth: NavMenuConstant = {
],
},
{
name: 'Custom OAuth/OIDC Providers',
url: '/guides/auth/custom-oauth-providers',
enabled: allAuthProvidersEnabled,
},
{
name: 'Anonymous Sign-Ins',
url: '/guides/auth/auth-anonymous',
@@ -2852,6 +2862,7 @@ export const self_hosting: NavMenuConstant = {
},
{ name: 'Configure S3 Storage', url: '/guides/self-hosting/self-hosted-s3' },
{ name: 'Copy Storage from Platform', url: '/guides/self-hosting/copy-from-platform-s3' },
{ name: 'Custom Email Templates', url: '/guides/self-hosting/custom-email-templates' },
{ name: 'Configure Social Login (OAuth)', url: '/guides/self-hosting/self-hosted-oauth' },
{ name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' },
{ name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' },
@@ -6,6 +6,12 @@ Supabase Auth works with many popular Auth methods, including Social and Phone A
<AuthProviders type="social" />
<Admonition type="tip">
You can also add any OAuth2 or OIDC-compatible identity provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
</Admonition>
### Phone Auth
<AuthProviders type="phone" />
@@ -372,7 +372,7 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
When `email_action_type` is `email_change`, the hook payload can include one or two OTPs and their hashes. This depends on your [Secure Email Change](/dashboard/project/_/auth/providers?provider=Email) setting.
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.email_new`). You must send two emails.
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.new_email`). You must send two emails.
- Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.
<Admonition type="caution" title="Counterintuitive field naming">
@@ -380,7 +380,7 @@ When `email_action_type` is `email_change`, the hook payload can include one or
The token hash field names are reversed due to backward compatibility. Pay careful attention to which token/hash pair goes with which email address:
- `token_hash_new` → use with the **current** email address (`user.email`) and `token`
- `token_hash` → use with the **new** email address (`user.email_new`) and `token_new`
- `token_hash` → use with the **new** email address (`user.new_email`) and `token_new`
Do not assume the `_new` suffix refers to the new email address.
@@ -391,7 +391,7 @@ Do not assume the `_new` suffix refers to the new email address.
When Secure Email Change is enabled (both token/hash pairs present):
- Send to **current** email address (`user.email`): use `token` with `token_hash_new`
- Send to **new** email address (`user.email_new`): use `token_new` with `token_hash`
- Send to **new** email address (`user.new_email`): use `token_new` with `token_hash`
When Secure Email Change is **disabled** (only one token/hash pair present):
@@ -0,0 +1,320 @@
---
id: 'custom-oauth-providers'
title: 'Custom OAuth/OIDC Providers'
description: 'Add any OAuth2 or OIDC-compatible identity provider to your Supabase project'
---
Custom OAuth/OIDC providers let you integrate any standards-compliant identity provider with Supabase Auth, beyond the ones Supabase supports out of the box.
Each custom provider uses a `custom:` prefix in its identifier (for example, `custom:my-idp` or `custom:github-enterprise`). This prefix distinguishes custom providers from built-in providers.
There are two provider types:
- **OAuth2**: for generic OAuth2 providers where you supply the authorization, token, and userinfo endpoints manually.
- **OIDC**: for providers that support [OpenID Connect](https://openid.net/connect/) discovery. You supply only the issuer URL and endpoints are resolved automatically.
<Admonition type="note">
You can add up to 3 custom providers per project. If you need more, [contact support](/dashboard/support/new).
</Admonition>
## Creating a provider
The create form displays a read-only **Callback URL**. Copy this URL and configure it as the redirect/callback URI in your external identity provider before completing setup.
### OAuth2 provider
Use an OAuth2 provider when your identity provider does not support OpenID Connect discovery. You must supply the authorization, token, and userinfo endpoint URLs explicitly.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click **New Provider**. Select **Manual configuration** as the configuration method.
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-oauth-provider`).
4. Enter the provider's **Client ID** and **Client Secret**.
5. Enter the **Authorization URL**, **Token URL**, and **UserInfo URL**.
6. Click **Create and enable provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oauth2',
identifier: 'custom:my-oauth-provider',
name: 'My OAuth Provider',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
authorization_url: 'https://provider.example.com/oauth/authorize',
token_url: 'https://provider.example.com/oauth/token',
userinfo_url: 'https://provider.example.com/oauth/userinfo',
scopes: ['profile', 'email'],
})
```
</TabPanel>
</Tabs>
### OIDC provider
Use an OIDC provider when your identity provider supports OpenID Connect. Supply the `issuer` URL and the discovery document, JWKS, and endpoints are resolved automatically.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click **New Provider**. Select **Auto-discovery (OIDC)** as the configuration method.
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-regional-provider`).
4. Enter the provider's **Client ID** and **Client Secret**.
5. Enter the **Issuer URL**. The discovery document and endpoints are resolved automatically.
6. Click **Create and enable provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oidc',
identifier: 'custom:my-regional-provider',
name: 'Regional Provider',
client_id: 'your-client-id',
client_secret: 'your-client-secret',
issuer: 'https://auth.example.com',
scopes: ['openid', 'profile', 'email'],
})
```
</TabPanel>
</Tabs>
OIDC providers have the following automatic behavior:
- The discovery document is fetched from `{issuer}/.well-known/openid-configuration` (or from `discovery_url` if set).
- The `openid` scope is always included. It is automatically added if missing from the `scopes` array.
- ID tokens are verified against the provider's JWKS (fetched from the discovery document's `jwks_uri`).
## Provider identifiers
Every custom provider identifier must start with the `custom:` prefix. Identifiers are 2–50 characters, lowercase alphanumeric with hyphens and colons allowed. Examples:
- `custom:my-provider`
- `custom:github-enterprise`
## User sign-in
Once a custom provider is created and enabled, users sign in via the standard OAuth authorize endpoint:
```
GET https://your-project.supabase.co/auth/v1/authorize?provider=custom:my-provider
```
Or using the Supabase client libraries:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'custom:my-provider',
})
```
</TabPanel>
<TabPanel id="flutter" label="Flutter">
```dart
await supabase.auth.signInWithOAuth(
OAuthProvider('custom:my-provider'),
);
```
</TabPanel>
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOAuth(
provider: "custom:my-provider",
redirectTo: URL(string: "my-custom-scheme://my-app-host")
)
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
```kotlin
supabase.auth.signInWith(CustomProvider("custom:my-provider"))
```
</TabPanel>
</Tabs>
## Managing providers
### List providers
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard. All custom providers are listed under **Custom OAuth Providers**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
// List all custom providers
const { data, error } = await supabase.auth.admin.customProviders.listProviders()
// Filter by provider type
const { data, error } = await supabase.auth.admin.customProviders.listProviders({
type: 'oidc',
})
```
</TabPanel>
</Tabs>
### Update a provider
Update any provider fields except `provider_type` and `identifier`. Only provided fields are changed (partial update). To rotate a client secret, update only the `client_secret` field.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click the three-dot menu (⋮) next to the provider and select **Update**.
3. Modify the fields you want to change.
4. Click **Update provider**.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.auth.admin.customProviders.updateProvider(
'custom:my-provider',
{
name: 'Updated Provider Name',
scopes: ['profile', 'email', 'groups'],
enabled: false,
}
)
```
</TabPanel>
</Tabs>
### Delete a provider
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="setup-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
2. Click the three-dot menu (⋮) next to the provider and select **Delete**.
3. Confirm the deletion.
</TabPanel>
<TabPanel id="js" label="JavaScript">
```js
const { data, error } =
await supabase.auth.admin.customProviders.deleteProvider('custom:my-provider')
```
</TabPanel>
</Tabs>
## Advanced configuration
### PKCE
PKCE (Proof Key for Code Exchange) is enabled by default (`pkce_enabled: true`) for all custom providers. The auth server automatically generates a code challenge and verifier during the authorization flow, protecting against authorization code interception attacks. This is handled entirely server-side, no client-side PKCE logic is needed.
To disable PKCE for a specific provider, set `pkce_enabled: false` when creating or updating it. This is not recommended unless the identity provider does not support PKCE.
### Authorization params
Extra query parameters appended to the provider's authorization URL during the OAuth flow. All values must be strings.
```json
{
"prompt": "consent",
"access_type": "offline",
"login_hint": "user@example.com"
}
```
The following reserved parameters are managed by the auth server and cannot be overridden: `client_id`, `client_secret`, `redirect_uri`, `response_type`, `state`, `code_challenge`, `code_challenge_method`, `code_verifier`, `nonce`.
### Multi-platform apps
If your app uses different client IDs for different platforms (for example, web vs mobile), use `acceptable_client_ids` to list additional client IDs that should be accepted for audience validation in OIDC ID tokens:
```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
provider_type: 'oidc',
identifier: 'custom:multi-platform-app',
name: 'Multi-Platform App',
client_id: 'web-client-id',
client_secret: 'your-client-secret',
issuer: 'https://app.example.com',
scopes: ['openid', 'profile', 'email'],
acceptable_client_ids: ['ios-client-id', 'android-client-id'],
})
```
### Email-optional providers
By default, providers must return an email address. Set `email_optional` to `true` when creating or updating a provider to allow sign-in without an email. This applies to both OAuth2 and OIDC providers.
### OIDC-specific options
| Field | Type | Default | Description |
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------- |
| `discovery_url` | `string` | `null` | Override the discovery document URL if the provider uses a non-standard location. |
| `skip_nonce_check` | `bool` | `false` | Skip nonce validation on ID tokens. Use only for providers that do not support nonce. |
## Error reference
| Error code | HTTP status | Description |
| ---------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
| `validation_failed` | 400 | Invalid parameters: missing required fields, bad format, reserved params, or invalid URLs. |
| `conflict` | 400 | A provider with the same identifier already exists. |
| `over_custom_provider_quota` | 400 | Maximum number of custom providers reached. |
| `custom_provider_not_found` | 404 | No provider exists with the given identifier. |
@@ -97,7 +97,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
export default function Auth() {
// Handle linking into app from email app.
const url = Linking.useURL();
const url = Linking.useLinkingURL();
if (url) createSessionFromUrl(url);
return (
@@ -45,6 +45,12 @@ Supabase supports a suite of social providers. Follow these guides to configure
</NavData>
</div>
<Admonition type="tip">
Need to integrate with a provider not listed here? You can add any OAuth2 or OIDC-compatible provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
</Admonition>
## Provider tokens
You can use the provider token and provider refresh token returned to make API calls to the OAuth provider. For example, you can use the Google provider token to access Google APIs on behalf of your user.
@@ -416,7 +416,7 @@ language plpgsql
security definer set search_path = ''
as $$
begin
select 'hello world';
return 'hello world';
end;
$$;
```
@@ -8,7 +8,7 @@ sidebar_label: 'Monitoring'
Monitoring replication lag is important and there are 3 ways to do this:
1. Dashboard - Under the [Reports](/docs/guides/platform/reports) of the dashboard, you can view the replication lag of your project
1. Dashboard - Under the [Reports](/docs/guides/telemetry/reports) of the dashboard, you can view the replication lag of your project
2. Database -
- pg_stat_subscription (subscriber) - if PID is null, then the subscription is not active
- pg_stat_subscription_stats - look here for error_count to see if there were issues applying or syncing (if yes, check the logs for why)
@@ -27,4 +27,4 @@ When you merge any branch into your main project, Supabase automatically runs a
6. **Seed** - Runs seed files to populate your branch with initial data (must be [enabled in config.toml](/docs/guides/deployment/branching/configuration#branch-configuration-with-remotes) for persistent branches)
7. **Deploy** - Deploys any changed Edge Functions and updates function secrets
If a parent deployment step fails, all dependent children steps will be skipped. For e.g., if your database migrations failed at step 5, our runner will not seed your branch because step 6 is skipped. If you are using GitHub integration, the same deployment workflow will be run on every commit pushed to your git branch.
If a parent deployment step fails, all dependent child steps will be skipped. For instance, if your database migrations failed at step 5, our runner will not seed your branch because step 6 is skipped. If you are using GitHub integration, the same deployment workflow will be run on every commit pushed to your git branch.
@@ -6,71 +6,90 @@ description: 'Things to do before making your app publicly available'
After developing your project and deciding it's production ready, you should run through this checklist to ensure that your project:
- is secure
- won't falter under the expected load
- remains available whilst in production
- [Is secure](#security)
- [Won't falter under the expected load](#performance)
- [Remains available whilst in production](#availability)
## Security
- Ensure RLS is enabled
<Admonition type="tip">
Check and review issues in your database using [Security Advisor](/dashboard/project/_/database/security-advisor).
</Admonition>
- Ensure you have enabled row level security (RLS) on all tables from the [**Database > Tables**](/dashboard/project/_/database/tables) section of the Supabase Dashboard.
- Tables that do not have RLS enabled with reasonable policies allow any client to access and modify their data. This is usually not what you want.
- [Learn more about RLS](/docs/guides/database/postgres/row-level-security).
- Enable replication on tables containing sensitive data by enabling Row Level Security (RLS) and setting row security policies:
- Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies.
- Go to the Database > Publications page in the Supabase Dashboard to manage replication tables.
- Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement) (see: [dashboard](/dashboard/project/_/auth/policies))
- Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for your database (see: [dashboard](/dashboard/project/_/database/settings#network-restrictions)).
- Ensure that your Supabase Account is protected with multi-factor authentication (MFA).
- If using a GitHub sign-in, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
- Enable replication on tables containing sensitive data by enabling RLS and setting row security policies:
- Go to the [**Authentication > Policies**](/dashboard/project/_/auth/policies) section of the Supabase Dashboard to enable RLS and create security policies.
- Go to the [**Database > Publications**](/dashboard/project/_/database/publications) section of the Supabase Dashboard to manage replication tables.
- Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement) from the [**Database > Settings > SSL Configuration**](/dashboard/project/_/database/settings#ssl-configuration) section of the dashboard.
- Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for the database from the [**Database > Settings > Network Restrictions**](/dashboard/project/_/database/settings#network-restrictions) section of the dashboard.
- Ensure that you protect your Supabase Account with multi-factor authentication (MFA).
- If using a GitHub sign-in, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication).
- Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
- If using email+password sign-in, set up [MFA for your Supabase account](/docs/guides/platform/multi-factor-authentication#enable-mfa).
- Enable [MFA enforcement on your organization](/docs/guides/platform/network-restrictions). This ensures all users must have a valid MFA backed session to interact with organization and project resources.
- Consider [adding multiple owners on your Supabase org](/dashboard/org/_/team). This ensures that if one of the owners is unreachable or loses access to their account, you still have Owner access to your org.
- Ensure email confirmations are [enabled](/dashboard/project/_/auth/providers) in the `Settings > Auth` page.
- Ensure that you've [set the expiry](/dashboard/project/_/auth/providers) for one-time passwords (OTPs) to a reasonable value that you are comfortable with. We recommend setting this to 3600 seconds (1 hour) or lower.
- Increase the length of the OTP if you need a higher level of entropy.
- Consider enabling [MFA enforcement on your organization](/docs/guides/platform/mfa/org-mfa-enforcement). This ensures all users must have a valid MFA-backed session to interact with organization and project resources.
- Consider adding multiple owners on your Supabase org from [the **Organization > Team**](/dashboard/org/_/team) section of the Supabase Dashboard. This ensures that if one of the owners is unreachable or loses access to their account, you still have Owner access to your org.
- Enable email confirmations in the [**Authentication > Providers**](/dashboard/project/_/auth/providers) section of the dashboard.
- Set the expiry in the [**Authentication > Providers**](/dashboard/project/_/auth/providers) section of the dashboard for one-time passwords (OTPs) to a reasonable value that you are comfortable with.
- We recommend setting this to 3600 seconds (1 hour) or lower.
- Increase the length of the OTP if you need a higher level of entropy.
- If your application requires a higher level of security, consider setting up [multi-factor authentication](/docs/guides/auth/auth-mfa) (MFA) for your users.
- Use a custom SMTP server for auth emails so that your users can see that the mails are coming from a trusted domain (preferably the same domain that your app is hosted on). Grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc.
- Consider how _you_ might abuse your service as an attacker, and take steps to mitigate it.
- Review these [common cybersecurity threats](https://auth0.com/docs/security/prevent-threats).
- Check and review issues in your database using [Security Advisor](/dashboard/project/_/database/security-advisor).
## Performance
<Admonition type="tip">
Check and review issues in your database using [Performance Advisor](/dashboard/project/_/database/performance-advisor).
</Admonition>
- Ensure that you have suitable indices to cater to your common query patterns
- [Learn more about indexes in Postgres](https://www.enterprisedb.com/postgres-tutorials/overview-postgresql-indexes).
- `pg_stat_statements` can help you [identify hot or slow queries](https://www.virtual-dba.com/blog/postgresql-performance-identifying-hot-and-slow-queries/).
- Perform load testing (preferably on a staging env)
- Tools like [k6](https://k6.io/) can simulate traffic from many different users.
- Upgrade your database if you require more resources. If you need anything beyond what is listed, contact enterprise@supabase.io.
- Upgrade your database if you require more resources. If you need anything beyond what is listed in [the compute and disk table](/docs/guides/platform/compute-and-disk), contact [enterprise@supabase.io](mailto:enterprise@supabase.io).
- If you are expecting a surge in traffic (for a big launch) and are on a Team or Enterprise Plan, [contact support](/dashboard/support/new) with more details about your launch and we'll help keep an eye on your project.
- If you expect your database size to be > 4 GB, [enable](/dashboard/project/_/settings/addons?panel=pitr) the Point in Time Recovery (PITR) add-on. Daily backups can take up resources from your database when the backup is in progress. PITR is more resource efficient, since only the changes to the database are backed up.
- Check and review issues in your database using [Performance Advisor](/dashboard/project/_/database/performance-advisor).
- If you expect your database size to be > 4 GB, enable the Point in Time Recovery (PITR) add-on in the [**Settings > Add-ons**](/dashboard/project/_/settings/addons?panel=pitr) section of the dashboard.
## Availability
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Settings > Auth)
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. You can refer to our [SMTP guide](/docs/guides/auth/auth-smtp) for more details.
- The default rate limit for auth emails when using a custom SMTP provider is 30 new users per hour, if doing a major public announcement you will likely require more than this.
- Applications on the Free Plan that exhibit extremely low activity in a 7 day period may be paused by Supabase to save on server resources.
- You can restore paused projects from the Supabase dashboard.
- Upgrade to Pro to guarantee that your project will not be paused for inactivity.
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails in the [**Authentication > Emails > SMTP Settings**](/dashboard/project/_/auth/smtp) section of the dashboard.
- You can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. Read our [SMTP guide](/docs/guides/auth/auth-smtp) for more setup details.
- The default rate limit for auth emails when using a custom SMTP provider is _30 new users per hour_. If you are doing a major public announcement, you will likely require more than this.
- We may pause applications on the Free Plan that exhibit low activity in a 7-day period to save on server resources.
- You can restore paused projects from [the Supabase dashboard](/dashboard/project/_).
- Upgrade to Pro to guarantee that we won't pause your project for inactivity.
- Database backups are not available for download for Free Plan projects.
- You can set up your own backup systems using tools like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump) or [wal-g](https://github.com/wal-g/wal-g).
- Nightly backups for Pro Plan projects are available on the Supabase dashboard for up to 7 days.
- Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. If a lower RPO is required, enable PITR.
- Read [the Database Backups guide](/docs/guides/platform/backups) for more options and retention details.
- If you need a lower recovery point objective (RPO), enable Point-in-Time Recovery (PITR). PITR allows you to back up a project at shorter intervals. This provides users an option to restore to any chosen point in time with second-level granularity.
- Supabase Projects use disks that offer 99.8-99.9% durability by default.
- Use Read Replicas if you require availability resilience to a disk failure event
- Use [Read Replicas](/docs/guides/platform/read-replicas) if you require availability resilience to a disk failure event
- Use PITR if you require durability resilience to a disk failure event
- Upgrading to the Supabase Pro Plan will give you [access to our support team](/dashboard/support/new).
- Upgrading to the Supabase Pro Plan gives you [access to our support team](/dashboard/support/new).
## Rate limiting, resource allocation, & abuse prevention
<Admonition type="caution" label="Shared Responsibility Model">
Running databases is a shared responsibility between you and Supabase. There are some things that we can take care of for you, and some things that you are responsible for.
Read more details in our [Shared Responsibility Model guide](/docs/guides/platform/shared-responsibility-model).
</Admonition>
- Supabase employs a number of safeguards against bursts of incoming traffic to prevent abuse and help maximize stability across the platform
- If you're on a Team or Enterprise Plan and expect high load events, such as production launches, heavy load testing, or prolonged high resource usage, open a ticket via the [support form](https://supabase.help) for help. Provide at least 2 weeks notice.
### Auth rate limits
- The table below shows the rate limit quotas on the following authentication endpoints. You can configure the auth rate limits for your project [here](/dashboard/project/_/auth/rate-limits).
- The table below shows the rate limit quotas on the following authentication endpoints. You can configure the authentication rate limits for your project in the [**Authentication > Rate Limits**](/dashboard/project/_/auth/rate-limits) section of the dashboard.
| Endpoint | Path | Limited By | Rate Limit |
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -87,39 +106,13 @@ After developing your project and deciding it's production ready, you should run
### Realtime limits
- Review the [Realtime limits](/docs/guides/realtime/limits).
- If you need limits increased you can always [contact support](/dashboard/support/new).
- If you need limits increased, [contact support](/dashboard/support/new).
### Abuse prevention
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Refer to [our guide](/docs/guides/auth/auth-captcha) on how to protect against abuse using this method.
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Read [the Auth CAPTCHA guide](/docs/guides/auth/auth-captcha) for more details on how to protect against abuse using this method.
### Email link validity
- When working with enterprise systems, email scanners may scan and make a `GET` request to the reset password link or sign up link in your email. Since links in Supabase Auth are single use, a user who opens an email post-scan to click on a link will receive an error. To get around this problem,
consider altering the email template to replace the original magic link with a link to a domain you control. The domain can present the user with a "Sign-in" button which redirect the user to the original magic link URL when clicked.
- When using a custom SMTP service, some services might have link tracking enabled which may overwrite or disform the email confirmation links sent by Supabase Auth. To prevent this from happening, we recommend that you disable link tracking when using a custom SMTP service.
## Subscribe to Supabase status page
Stay informed about Supabase service status by subscribing to the [Status Page](https://status.supabase.com/). We recommend setting up Slack notifications through an RSS feed to ensure your team receives timely updates about service status changes.
### Setting up Slack notifications
1. Install the RSS app in Slack:
- Visit the [RSS app page](https://slack.com/marketplace/A0F81R7U7-rss) in the Slack marketplace
- Click `Add to Slack` if not already installed
- Otherwise you will get straight to next step, no need to reinstall the app
2. Configure the Supabase status feed:
- Create a channel (e.g., `#supabase-status-alerts`) for status updates
- On the [RSS app page](https://slack.com/marketplace/A0F81R7U7-rss) go to _Add a Feed_ section and set Feed URL to `https://status.supabase.com/history.rss`
- Select your designated channel and click "Subscribe to this feed"
Once configured, your team will receive automatic notifications in Slack whenever the Supabase Status Page is updated.
For detailed setup instructions, see the [Add RSS feeds to Slack](https://slack.com/intl/en-nz/help/articles/218688467-Add-RSS-feeds-to-Slack).
## Next steps
This checklist is always growing so be sure to check back frequently, and also feel free to suggest additions and amendments by making a PR on [GitHub](https://github.com/supabase/supabase).
- When working with enterprise systems, email scanners may scan and make a `GET` request to the reset password link or sign-up link in your email. Since links in Supabase Auth are single-use, a user who opens an email post-scan to click on a link will receive an error. To get around this problem, consider altering the email template to replace the original magic link with a link to a domain you control. The domain can present the user with a "Sign-in" button, which redirects the user to the original magic link URL when clicked.
- When using a custom SMTP service, some services might have link tracking enabled which may overwrite or deform the email confirmation links sent by Supabase Auth. To prevent this from happening, we recommend that you disable link tracking when using a custom SMTP service.
@@ -0,0 +1,48 @@
---
title: Agent Skills
---
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. Agents are increasingly capable, but often don't have the context they need to do real work reliably. Skills solve this by giving agents access to procedural knowledge and company-, team-, and user-specific context they can load on demand. Agents with access to a set of skills can extend their capabilities based on the task they're working on.
## Installing skills
Install all Supabase skills using the skills CLI:
```bash
npx skills add supabase/agent-skills
```
To install a specific skill from the repository:
```bash
npx skills add supabase/agent-skills --skill SKILL_NAME
```
### Claude Code plugin
You can also install the skills as Claude Code plugins:
```bash
/plugin marketplace add supabase/agent-skills
/plugin install postgres-best-practices@supabase-agent-skills
```
Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others.
## Available skills
<AiSkillsIndex />
## Finding more skills
Browse the [skills.sh directory](https://skills.sh) to discover skills from the community. You can also search for skills using the CLI:
```bash
npx skills find QUERY
```
## Learn more
- [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)
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Ionic React App.'
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/mhartington/supabase-ionic-react).
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/ionic-react-user-management).
</Admonition>
@@ -17,11 +17,11 @@ If you get stuck while working through this guide, refer to the [full example on
## Building the app
Let's start building the React app from scratch.
Start building the React app from scratch.
### Initialize an Ionic React app
We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
Use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize
an app called `supabase-ionic-react`:
```bash
@@ -30,352 +30,63 @@ ionic start supabase-ionic-react blank --type react
cd supabase-ionic-react
```
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
```
And finally we want to save the environment variables in a `.env`.
All we need are the API URL and the key that you copied [earlier](#get-api-details).
Save the environment variables in a `.env`. You need the API URL and the key that you copied [earlier](#get-api-details).
<$CodeTabs>
```bash name=.env
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
VITE_SUPABASE_KEY=YOUR_SUPABASE_KEY
```
</$CodeTabs>
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed
on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
With the API credentials in place, create a helper file to initialize the Supabase client. These variables will be exposed
in the browser, which is safe because they use a restricted publishable key and the SQL quickstart enables [Row Level Security](/docs/guides/auth#row-level-security) on the `profiles` table.
<$CodeTabs>
```js name=src/supabaseClient.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || ''
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY || ''
export const supabase = createClient(supabaseUrl, supabasePublishableKey)
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/supabaseClient.ts"
lines={[[1, -1]]}
meta="name=src/supabaseClient.ts"
/>
### Set up a login route
Let's set up a React component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
Set up a React component to manage logins and sign ups which uses Magic Links, so users can sign in with their email without using passwords.
<$CodeTabs>
```jsx name=/src/pages/Login.tsx
import { useState } from 'react';
import {
IonButton,
IonContent,
IonHeader,
IonInput,
IonItem,
IonLabel,
IonList,
IonPage,
IonTitle,
IonToolbar,
useIonToast,
useIonLoading,
} from '@ionic/react';
import {supabase} from '../supabaseClient'
export function LoginPage() {
const [email, setEmail] = useState('');
const [showLoading, hideLoading] = useIonLoading();
const [showToast ] = useIonToast();
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
console.log()
e.preventDefault();
await showLoading();
try {
await supabase.auth.signInWithOtp({
"email": email
});
await showToast({ message: 'Check your email for the login link!' });
} catch (e: any) {
await showToast({ message: e.error_description || e.message , duration: 5000});
} finally {
await hideLoading();
}
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Login</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="ion-padding">
<h1>Supabase + Ionic React</h1>
<p>Sign in via magic link with your email below</p>
</div>
<IonList inset={true}>
<form onSubmit={handleLogin}>
<IonItem>
<IonLabel position="stacked">Email</IonLabel>
<IonInput
value={email}
name="email"
onIonChange={(e) => setEmail(e.detail.value ?? '')}
type="email"
></IonInput>
</IonItem>
<div className="ion-text-center">
<IonButton type="submit" fill="clear">
Login
</IonButton>
</div>
</form>
</IonList>
</IonContent>
</IonPage>
);
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/pages/Login.tsx"
lines={[[1, -1]]}
meta="name=src/pages/Login.tsx"
/>
### Account page
After a user is signed in we can allow them to edit their profile details and manage their account.
After a user signs in, they should be able to edit their profile details and manage their account.
Let's create a new component for that called `Account.tsx`.
Create a new component for that called `Account.tsx`.
<$CodeTabs>
```jsx name=src/pages/Account.tsx
import {
IonButton,
IonContent,
IonHeader,
IonInput,
IonItem,
IonLabel,
IonPage,
IonTitle,
IonToolbar,
useIonLoading,
useIonToast,
useIonRouter
} from '@ionic/react';
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import { Session } from '@supabase/supabase-js';
export function AccountPage() {
const [showLoading, hideLoading] = useIonLoading();
const [showToast] = useIonToast();
const [session, setSession] = useState<Session | null>(null)
const router = useIonRouter();
const [profile, setProfile] = useState({
username: '',
website: '',
avatar_url: '',
});
useEffect(() => {
const getSession = async () => {
setSession(await supabase.auth.getSession().then((res) => res.data.session))
}
getSession()
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
useEffect(() => {
getProfile();
}, [session]);
const getProfile = async () => {
console.log('get');
await showLoading();
try {
const user = await supabase.auth.getUser();
const { data, error, status } = await supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user!.data.user?.id)
.single();
if (error && status !== 406) {
throw error;
}
if (data) {
setProfile({
username: data.username,
website: data.website,
avatar_url: data.avatar_url,
});
}
} catch (error: any) {
showToast({ message: error.message, duration: 5000 });
} finally {
await hideLoading();
}
};
const signOut = async () => {
await supabase.auth.signOut();
router.push('/', 'forward', 'replace');
}
const updateProfile = async (e?: any, avatar_url: string = '') => {
e?.preventDefault();
console.log('update ');
await showLoading();
try {
const user = await supabase.auth.getUser();
const updates = {
id: user!.data.user?.id,
...profile,
avatar_url: avatar_url,
updated_at: new Date(),
};
const { error } = await supabase.from('profiles').upsert(updates);
if (error) {
throw error;
}
} catch (error: any) {
showToast({ message: error.message, duration: 5000 });
} finally {
await hideLoading();
}
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Account</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<form onSubmit={updateProfile}>
<IonItem>
<IonLabel>
<p>Email</p>
<p>{session?.user?.email}</p>
</IonLabel>
</IonItem>
<IonItem>
<IonLabel position="stacked">Name</IonLabel>
<IonInput
type="text"
name="username"
value={profile.username}
onIonChange={(e) =>
setProfile({ ...profile, username: e.detail.value ?? '' })
}
></IonInput>
</IonItem>
<IonItem>
<IonLabel position="stacked">Website</IonLabel>
<IonInput
type="url"
name="website"
value={profile.website}
onIonChange={(e) =>
setProfile({ ...profile, website: e.detail.value ?? '' })
}
></IonInput>
</IonItem>
<div className="ion-text-center">
<IonButton fill="clear" type="submit">
Update Profile
</IonButton>
</div>
</form>
<div className="ion-text-center">
<IonButton fill="clear" onClick={signOut}>
Log Out
</IonButton>
</div>
</IonContent>
</IonPage>
);
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/pages/Account.tsx"
lines={[[1, -1]]}
meta="name=src/pages/Account.tsx"
/>
### Launch!
Now that we have all the components in place, let's update `App.tsx`:
Now that you have all the components in place, update `App.tsx`:
<$CodeTabs>
```jsx name=src/App.tsx
import { Redirect, Route } from 'react-router-dom'
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'
import { IonReactRouter } from '@ionic/react-router'
import { supabase } from './supabaseClient'
import '@ionic/react/css/ionic.bundle.css'
/* Theme variables */
import './theme/variables.css'
import { LoginPage } from './pages/Login'
import { AccountPage } from './pages/Account'
import { useEffect, useState } from 'react'
import { Session } from '@supabase/supabase-js'
setupIonicReact()
const App: React.FC = () => {
const [session, setSession] = useState<Session | null>(null)
useEffect(() => {
const getSession = async () => {
setSession(await supabase.auth.getSession().then((res) => res.data.session))
}
getSession()
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
})
}, [])
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route
exact
path="/"
render={() => {
return session ? <Redirect to="/account" /> : <LoginPage />
}}
/>
<Route exact path="/account">
<AccountPage />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
}
export default App
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/App.tsx"
lines={[[1, -1]]}
meta="name=src/App.tsx"
/>
Once that's done, run this in a terminal window:
@@ -383,7 +94,7 @@ Once that's done, run this in a terminal window:
ionic serve
```
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
Then open your browser to the URL printed by `ionic serve` (by default, [http://localhost:8100](http://localhost:8100)) and you should see the completed app.
![Supabase Ionic React](/docs/img/ionic-demos/ionic-react.png)
@@ -403,142 +114,30 @@ npm install @ionic/pwa-elements @capacitor/camera
Ionic PWA elements is a companion package that will polyfill certain browser APIs that provide no user interface with custom Ionic UI.
With those packages installed we can update our `index.tsx` to include an additional bootstrapping call for the Ionic PWA Elements.
With those packages installed update `index.tsx` to include an additional bootstrapping call for the Ionic PWA Elements.
<$CodeTabs>
```ts name=src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
import reportWebVitals from './reportWebVitals'
import { defineCustomElements } from '@ionic/pwa-elements/loader'
defineCustomElements(window)
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
serviceWorkerRegistration.unregister()
reportWebVitals()
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/index.tsx"
lines={[[1, -1]]}
meta="name=src/index.tsx"
/>
Then create an `AvatarComponent`.
<$CodeTabs>
```jsx name=src/components/Avatar.tsx
import { IonIcon } from '@ionic/react';
import { person } from 'ionicons/icons';
import { Camera, CameraResultType } from '@capacitor/camera';
import { useEffect, useState } from 'react';
import { supabase } from '../supabaseClient';
import './Avatar.css'
export function Avatar({
url,
onUpload,
}: {
url: string;
onUpload: (e: any, file: string) => Promise<void>;
}) {
const [avatarUrl, setAvatarUrl] = useState<string | undefined>();
useEffect(() => {
if (url) {
downloadImage(url);
}
}, [url]);
const uploadAvatar = async () => {
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
});
const file = await fetch(photo.dataUrl!)
.then((res) => res.blob())
.then(
(blob) =>
new File([blob], 'my-file', { type: `image/${photo.format}` })
);
const fileName = `${Math.random()}-${new Date().getTime()}.${
photo.format
}`;
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, file);
if (uploadError) {
throw uploadError;
}
onUpload(null, fileName);
} catch (error) {
console.log(error);
}
};
const downloadImage = async (path: string) => {
try {
const { data, error } = await supabase.storage
.from('avatars')
.download(path);
if (error) {
throw error;
}
const url = URL.createObjectURL(data!);
setAvatarUrl(url);
} catch (error: any) {
console.log('Error downloading image: ', error.message);
}
};
return (
<div className="avatar">
<div className="avatar_wrapper" onClick={uploadAvatar}>
{avatarUrl ? (
<img src={avatarUrl} />
) : (
<IonIcon icon={person} className="no-avatar" />
)}
</div>
</div>
);
}
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/components/Avatar.tsx"
lines={[[1, -1]]}
meta="name=src/components/Avatar.tsx"
/>
### Add the new widget
And then we can add the widget to the Account page:
And then add the widget to the Account page:
<$CodeTabs>
```jsx name=src/pages/Account.tsx
// Import the new component
import { Avatar } from '../components/Avatar';
// ...
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Account</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<Avatar url={profile.avatar_url} onUpload={updateProfile}></Avatar>
```
</$CodeTabs>
<$CodeSample
path="/user-management/ionic-react-user-management/src/pages/Account.tsx"
lines={[[16, 16], [101, 110]]}
meta="name=src/pages/Account.tsx"
/>
At this stage you have a fully functional application!
@@ -44,7 +44,7 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
### App styling (optional)
An optional step is to update the CSS file `app/globals.css` to make the app look nice.
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
@@ -66,7 +66,7 @@ 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.
It is recommended to create 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 essential 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.
@@ -88,9 +88,11 @@ meta="name=lib/supabase/server.ts"
### Next.js proxy
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. This is accomplished by:
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.
- Refreshing the Auth token with the call to `supabase.auth.getUser`.
You can 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.
- Passing the refreshed Auth token to the browser, so it replaces the old token. This is done with `response.cookies.set`.
@@ -100,15 +102,15 @@ 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.
Always use `supabase.auth.getUser()` to protect pages and user data.
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 `getUser()` because it sends a request to the Supabase Auth server every time 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>
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. This is used by the `proxy.ts` file, which is a Next.js convention.
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>
@@ -184,9 +186,9 @@ Before proceeding, change the email template to support support a server-side au
- Select the **Confirm signup** template.
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
<Admonition type="tip">
<Admonition type="tip" label="Did you know?">
**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 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).
</Admonition>
@@ -162,16 +162,16 @@ meta="name=src/routes/+page.server.ts"
Change the email template to support a server-side authentication flow.
Before we proceed, let's change the email template to support sending a token hash:
Before proceeding, change the email template to support sending a token hash:
- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard.
- Select the **Confirm signup** template.
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
- Repeat the previous step for **Magic link** template.
<Admonition type="tip">
<Admonition type="tip" label="Did you know?">
**Did you know?** You can also customize 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 also customize 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).
</Admonition>
@@ -72,12 +72,28 @@ The Supabase CLI requires **Node.js 20 or later** when run via `npx` or `npm`. O
</Admonition>
<Admonition type="note">
Installing the Supabase CLI globally using `npm install -g supabase` is **not supported**.
For global usage, install the CLI via Homebrew, Scoop, or the standalone binary.
Alternatively, you can run the CLI using `npx supabase` or install it locally as a dev dependency.
</Admonition>
You can also install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase):
```sh
npm install supabase --save-dev
```
<Admonition type="note">
Global installation using `npm install -g supabase` is not supported. For global CLI usage, install via [Homebrew](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=macos), [Scoop](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=windows), or the [standalone binary](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=linux).
</Admonition>
</TabPanel>
</Tabs>
@@ -140,7 +140,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ
To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted.
Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
Restrictions due to usage limits are lifted once your quota refills at the start of the next billing cycle. Note that there may be a short delay after your billing period resets before restrictions are fully lifted. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
## Reports and invoices
@@ -4,6 +4,8 @@ subtitle: 'Learn how to backup and restore projects using the Supabase CLI'
breadcrumb: 'Migrations'
---
# Migrating the database
## Backup database using the CLI
<StepHikeCompact>
@@ -21,11 +23,11 @@ breadcrumb: 'Migrations'
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Get the new database connection string" fullWidth>
On your project dashboard, click [Connect](/dashboard/project/_?showConnect=true).
On your project dashboard, click [Connect](/dashboard/project/_?showConnect=true&method=session).
<Admonition type="note">
Use the [Session pooler](/dashboard/project/_?showConnect=true&method=session) connection string by default. If your ISP supports IPv6 or you have the IPv4 add-on enabled, use the direct connection string.
Use the [Session pooler](/dashboard/project/_?showConnect=true&method=session) connection string by default. If your network supports [IPv6](https://test-ipv6.com/) or you have the [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled, use the direct connection string.
</Admonition>
@@ -65,7 +67,7 @@ breadcrumb: 'Migrations'
```
```bash
supabase db dump --db-url [CONNECTION_STRING] -f data.sql --use-copy --data-only
supabase db dump --db-url [CONNECTION_STRING] -f data.sql --use-copy --data-only -x "storage.buckets_vectors" -x "storage.vector_indexes"
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
@@ -104,18 +106,17 @@ breadcrumb: 'Migrations'
- If Webhooks were used in the old database, enable [Database Webhooks](/dashboard/project/_/database/hooks).
- If any non-default extensions were used in the old database, enable the [Extensions](/dashboard/project/_/database/extensions).
- If Replication for Realtime was used in the old database, enable [Publication](/dashboard/project/_/database/publications) on the tables necessary
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Get the new database connection string" fullWidth>
Go to the [project page](/dashboard/project/_/) and click the "**Connect**" button at the top of the page for the connection string.
Go to [the **Connect** panel](/dashboard/project/_?showConnect=true&method=session) for the connection string.
<Admonition type="note">
Use the Session pooler connection string by default. If your ISP supports IPv6, use the direct connection string.
Use the Session pooler connection string by default. If your ISP [supports IPv6](https://test-ipv6.com/), use the direct connection string.
</Admonition>
@@ -135,15 +136,14 @@ breadcrumb: 'Migrations'
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Get the database password" fullWidth>
Reset the password in the [project connect page](/dashboard/project/_?showConnect=true).
Replace ```[YOUR-PASSWORD]``` in the connection string with the database password.
Replace ```[YOUR-PASSWORD]``` in the connection string with the database password. If you do not remember your password, you can reset it on [the **Database > Settings**](/dashboard/project/_/database/settings) page of the Dashboard.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Restore your Project with the CLI" fullWidth>
<StepHikeCompact.Details title="Restore your Project with PSQL" fullWidth>
<Tabs
scrollable
size="small"
@@ -188,17 +188,18 @@ breadcrumb: 'Migrations'
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details title="Reactivate Database publications" fullWidth>
If replication for Supabase Realtime was used in the old database, enable publication on [the **Database > Publications**](/dashboard/project/_/database/publications) section of the Dashboard on the tables necessary.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
## Important project restoration notes
## Special considerations
### Troubleshooting notes
- Setting the `session_replication_role` to `replica` disables all triggers so that columns are not double encrypted.
- If you have created any [custom roles](/dashboard/project/_/database/roles) with `login` attribute, you have to manually set their passwords in the new project.
- If you run into any permission errors related to `supabase_admin` during restore, edit the `schema.sql` file and comment out any lines containing `ALTER ... OWNER TO "supabase_admin"`.
### Preserving migration history
#### Preserving migration history
If you were using Supabase CLI for managing migrations on your old database and would like to preserve the migration history in your newly restored project, you need to insert the migration records separately using the following commands.
@@ -213,7 +214,7 @@ psql \
--dbname "$NEW_DB_URL"
```
### Schema changes to `auth` and `storage`
#### Schema changes to `auth` and `storage`
If you have modified the `auth` and `storage` schemas in your old project, such as adding triggers or Row Level Security(RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the changes to these schemas using the following commands.
@@ -222,61 +223,591 @@ supabase link --project-ref "$OLD_PROJECT_REF"
supabase db diff --linked --schema auth,storage > changes.sql
```
### Migrate storage objects
## Troubleshooting notes
The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another.
#### Disabling triggers during restore:
```js
// npm install @supabase/supabase-js@2
const { createClient } = require('@supabase/supabase-js')
Setting `session_replication_role` to `replica` disables triggers during the migration, preventing columns from being double encrypted.
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
#### Custom roles require passwords
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
If you created any [custom roles](/dashboard/project/_/database/roles) with the `LOGIN` attribute, you must manually set their passwords in the new project. This can be done with the SQL command:
;(async () => {
const oldSupabaseRestClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY, {
db: {
schema: 'storage',
},
})
const oldSupabaseClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
const newSupabaseClient = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
// make sure you update max_rows in postgrest settings if you have a lot of objects
// or paginate here
const { data: oldObjects, error } = await oldSupabaseRestClient.from('objects').select()
if (error) {
console.log('error getting objects from old bucket')
throw error
}
for (const objectData of oldObjects) {
console.log(`moving ${objectData.id}`)
try {
const { data, error: downloadObjectError } = await oldSupabaseClient.storage
.from(objectData.bucket_id)
.download(objectData.name)
if (downloadObjectError) {
throw downloadObjectError
}
const { _, error: uploadObjectError } = await newSupabaseClient.storage
.from(objectData.bucket_id)
.upload(objectData.name, data, {
upsert: true,
contentType: objectData.metadata.mimetype,
cacheControl: objectData.metadata.cacheControl,
})
if (uploadObjectError) {
throw uploadObjectError
}
} catch (err) {
console.log('error moving ', objectData)
console.log(err)
}
}
})()
```sql
alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD';
```
#### `supabase_admin` permission errors
If you encounter permission errors related to `supabase_admin` during restore:
- Open `schema.sql`
- Comment out any lines containing:
```sql
ALTER ... OWNER TO "supabase_admin"
```
#### `cli_login_postgres` role grant error
If you encounter the error:
```sh
ERROR: permission denied to grant role "postgres"
DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role.
```
- Open `roles.sql`
- Comment out the line:
```sql
GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin";
```
#### `cli_login_postgres` role issues after cloning
The `cli_login_role` must be created by the `supabase_admin` role. If the migration process cloned over the role before the CLI could generate its own version, it may encounter the error:
```sh
"message":"Failed to create login role:
ERROR: 0LP01: role "postgres" is a member of role "cli_login_postgres"
```
To resolve the issue, drop the custom `cli_login_postgres` role. Then the CLI can recreate it with the right privileges:
```sql
DROP ROLE IF EXISTS cli_login_postgres;
```
# Migrating edge functions
## Steps (using the Supabase CLI):
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Login to your Supabase Account" fullWidth>
With the Supabase CLI [Supabase CLI](/docs/guides/local-development/cli/getting-started), run:
```bash
supabase login
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="List your edge functions" fullWidth>
```bash
supabase functions list --project-ref your_project_ref
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Download your functions" fullWidth>
You can download an individual function with the following command:
```bash
supabase functions download YOUR_FUNCTION_NAME --project-ref your_project_ref
```
<Admonition type="note">
The command will not download [import maps](/docs/guides/functions/dependencies#using-import-maps-legacy) nor [deno.json](/docs/guides/functions/dependencies#using-denojson-recommended) files. If your edge functions rely on them for dependency management, you will have to add them back manually.
</Admonition>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Deploy the functions" fullWidth>
```bash
supabase functions deploy --project-ref your_target_project_ref
```
This deploys all functions within the `supabase/functions` to the target project. You can confirm by checking your Edge Functions on [the project dashboard](/dashboard/project/_/functions)
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
## Steps (using the Supabase Dashboard):
<Admonition type="note">
Dependencies defined through [import maps](/docs/guides/functions/dependencies#using-import-maps-legacy) and [deno.json](/docs/guides/functions/dependencies#using-denojson-recommended) files will need to be rewritten to rely on their [direct import paths](/docs/guides/functions/dependencies#importing-dependencies) when using this approach.
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details fullWidth>
In the source project, navigate to **Edge Functions** from the side menu
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details fullWidth>
Using the `Download` button, download your desired function as zip: ![Download Edge
Function](/docs/img/troubleshooting/download-edge-function-via-dashboard.gif)
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details fullWidth>
In the target project, navigate to **Edge Functions** from the side menu
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details fullWidth>
Click on the `Deploy a new function` button, select **Via Editor** operation
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details fullWidth>
Drag and drop your downloaded function (the zip function from step 2) into the editor
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={6}>
<StepHikeCompact.Details fullWidth>
Add your function name and click on the `Deploy function` button to deploy the function:
![Upload Edge Function](/docs/img/troubleshooting/upload-edge-function-via-dashboard.gif)
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
# Migrating storage objects
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="On your machine, create a javascript repository">
Using your preferred JavaScript package manager, create a new project with the `supabase` client package
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm_initiate"
queryGroup="initiate"
>
<TabPanel id="npm_initiate" label="npm">
```bash
npm init -y
npm install @supabase/supabase-js
```
</TabPanel>
<TabPanel id="pnpm_initiate" label="pnpm">
```bash
pnpm init -y
pnpm install @supabase/supabase-js
```
</TabPanel>
<TabPanel id="yarn_initiate" label="yarn">
```bash
yarn init -y
yarn add @supabase/supabase-js
```
</TabPanel>
<TabPanel id="bun_initiate" label="bun">
```bash
bun init -y
bun install @supabase/supabase-js
```
</TabPanel>
</Tabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Create an index.js file in your Node.js project">
Add the example script to it.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```js name=index.js
// npm install @supabase/supabase-js@2
const { createClient } = require('@supabase/supabase-js')
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
const oldSupabase = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
const newSupabase = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
function createLoadingAnimation(message) {
const readline = require('readline')
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
let i = 0
let timer
let stopped = false
const animate = () => {
if (stopped) return
process.stdout.write(`\r${frames[i]} ${message}`)
i = (i + 1) % frames.length
timer = setTimeout(animate, 80)
}
animate()
return {
stop: (finalMessage = '') => {
stopped = true
clearTimeout(timer)
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
process.stdout.write(`✓ ${finalMessage || message}\n`)
},
}
}
/**
* Lists all files in a bucket, handling nested folders recursively.
*/
async function listAllFiles(bucket, path = '') {
const loader = createLoadingAnimation(`Listing files in '${bucket}${path ? '/' + path : ''}'...`)
try {
const { data, error } = await oldSupabase.storage.from(bucket).list(path, { limit: 1000 })
if (error) {
loader.stop(`Error listing files in '${bucket}${path ? '/' + path : ''}'`)
throw new Error(`❌ Error listing files in bucket '${bucket}': ${error.message}`)
}
if (!data || data.length === 0) {
loader.stop(`No files found in '${bucket}${path ? '/' + path : ''}'`)
return []
}
let files = []
for (const item of data) {
if (!item.metadata) {
loader.stop(`Found folder '${item.name}' in '${bucket}${path ? '/' + path : ''}'`)
const subFiles = await listAllFiles(bucket, `${path}${item.name}/`)
files = files.concat(subFiles)
} else {
files.push({ fullPath: `${path}${item.name}`, metadata: item.metadata })
}
}
loader.stop(`Found ${files.length} files in '${bucket}${path ? '/' + path : ''}'`)
return files
} catch (error) {
loader.stop()
throw error
}
}
/**
* Creates a bucket in the new Supabase project if it doesn't exist.
*/
async function ensureBucketExists(bucketName, options = {}) {
const { data: existingBucket, error: getBucketError } =
await newSupabase.storage.getBucket(bucketName)
if (getBucketError && !getBucketError.message.includes('not found')) {
throw new Error(`❌ Error checking if bucket '${bucketName}' exists: ${getBucketError.message}`)
}
if (!existingBucket) {
console.log(`🪣 Creating bucket '${bucketName}' in new project...`)
const { error } = await newSupabase.storage.createBucket(bucketName, options)
if (error) throw new Error(`❌ Failed to create bucket '${bucketName}': ${error.message}`)
console.log(`✅ Created bucket '${bucketName}'`)
} else {
console.log(`ℹ️ Bucket '${bucketName}' already exists in new project`)
}
}
/**
* Migrates a single file from the old project to the new one.
*/
async function migrateFile(sourceBucketName, targetBucketName, file) {
const loader = createLoadingAnimation(
`Migrating ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'...`
)
try {
const { data, error: downloadError } = await oldSupabase.storage
.from(sourceBucketName)
.download(file.fullPath)
if (downloadError) {
loader.stop(`Failed to migrate ${file.fullPath}: Download error`)
throw new Error(`Download failed: ${downloadError.message}`)
}
// Preserve all available metadata from the original file
const uploadOptions = {
upsert: true,
contentType: file.metadata?.mimetype,
cacheControl: file.metadata?.cacheControl,
}
const { error: uploadError } = await newSupabase.storage
.from(targetBucketName)
.upload(file.fullPath, data, uploadOptions)
if (uploadError) {
loader.stop(`Failed to migrate ${file.fullPath}: Upload error`)
throw new Error(`Upload failed: ${uploadError.message}`)
}
loader.stop(
`Migrated ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'`
)
return { success: true, path: file.fullPath }
} catch (err) {
console.error(
`❌ Error migrating ${file.fullPath} in bucket '${targetBucketName}':`,
err.message
)
return { success: false, path: file.fullPath, error: err.message }
}
}
function chunkArray(array, size) {
const chunks = []
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size))
}
return chunks
}
/**
* Migrates all buckets and files from the old Supabase project to the new one.
* Processes files in parallel within batches for efficiency.
*/
async function migrateBuckets() {
console.log('🔄 Starting Supabase Storage migration...')
console.log(`📦 Source project: ${OLD_PROJECT_URL}`)
console.log(`📦 Target project: ${NEW_PROJECT_URL}`)
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
})
console.log(
'\n⚠️ WARNING: This migration may overwrite files in the target project if they have the same paths.'
)
console.log('⚠️ It is recommended to back up your target project before proceeding.')
const answer = await new Promise((resolve) => {
readline.question('Do you want to proceed with the migration? (yes/no): ', resolve)
})
readline.close()
if (answer.toLowerCase() !== 'yes') {
console.log('Migration canceled by user.')
return { canceled: true }
}
console.log('\n📦 Fetching all buckets from old project...')
const { data: oldBuckets, error: bucketListError } = await oldSupabase.storage.listBuckets()
if (bucketListError) throw new Error(`❌ Error fetching buckets: ${bucketListError.message}`)
console.log(`✅ Found ${oldBuckets.length} buckets to migrate.`)
const { data: existingBuckets, error: existingBucketsError } =
await newSupabase.storage.listBuckets()
if (existingBucketsError)
throw new Error(`❌ Error fetching existing buckets: ${existingBucketsError.message}`)
const existingBucketNames = existingBuckets.map((b) => b.name)
const conflictingBuckets = oldBuckets.filter((b) => existingBucketNames.includes(b.name))
let conflictStrategy = 2
if (conflictingBuckets.length > 0) {
console.log('\n⚠️ The following buckets already exist in the target project:')
conflictingBuckets.forEach((b) => console.log(` - ${b.name}`))
const conflictAnswer = await new Promise((resolve) => {
const rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
})
rl.question(
'\nHow do you want to handle existing buckets?\n' +
'1. Skip existing buckets\n' +
'2. Merge files (may overwrite existing files)\n' +
'3. Rename buckets in target (add suffix "_migrated")\n' +
'4. Cancel migration\n' +
'Enter your choice (1-4): ',
(answer) => {
rl.close()
resolve(answer)
}
)
})
if (conflictAnswer === '4') {
console.log('Migration canceled by user.')
return { canceled: true }
}
conflictStrategy = parseInt(conflictAnswer)
if (isNaN(conflictStrategy) || conflictStrategy < 1 || conflictStrategy > 3) {
console.log('Invalid choice. Migration canceled.')
return { canceled: true }
}
}
const migrationStats = {
totalBuckets: oldBuckets.length,
processedBuckets: 0,
skippedBuckets: 0,
totalFiles: 0,
successfulFiles: 0,
failedFiles: 0,
failedFilesList: [],
}
for (const bucket of oldBuckets) {
const bucketName = bucket.name
console.log(`\n📁 Processing bucket: ${bucketName}`)
let targetBucketName = bucketName
if (existingBucketNames.includes(bucketName)) {
if (conflictStrategy === 1) {
console.log(`⏩ Skipping bucket '${bucketName}' as it already exists in target project`)
migrationStats.skippedBuckets++
continue
} else if (conflictStrategy === 3) {
targetBucketName = `${bucketName}_migrated`
console.log(`🔄 Renaming bucket to '${targetBucketName}' in target project`)
} else {
console.log(`🔄 Merging files into existing bucket '${bucketName}' in target project`)
}
}
// Preserve bucket configuration when creating in the new project
if (targetBucketName !== bucketName || !existingBucketNames.includes(bucketName)) {
await ensureBucketExists(targetBucketName, {
public: bucket.public,
fileSizeLimit: bucket.file_size_limit,
allowedMimeTypes: bucket.allowed_mime_types,
})
}
const files = await listAllFiles(bucketName)
console.log(`✅ Found ${files.length} files in bucket '${bucketName}'.`)
migrationStats.totalFiles += files.length
const batches = chunkArray(files, 10)
for (let i = 0; i < batches.length; i++) {
console.log(`\n🚀 Processing batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
const results = await Promise.all(
batches[i].map((file) => migrateFile(bucketName, targetBucketName, file))
)
const batchSuccesses = results.filter((r) => r.success).length
const batchFailures = results.filter((r) => !r.success)
migrationStats.successfulFiles += batchSuccesses
migrationStats.failedFiles += batchFailures.length
migrationStats.failedFilesList.push(...batchFailures.map((f) => f.path))
console.log(
`✅ Completed batch ${i + 1}/${batches.length}: ${batchSuccesses} succeeded, ${batchFailures.length} failed`
)
}
migrationStats.processedBuckets++
console.log(`✅ Completed bucket '${bucketName}' migration`)
}
console.log('\n📊 Migration Summary:')
console.log(
`Buckets: ${migrationStats.processedBuckets}/${migrationStats.totalBuckets} processed, ${migrationStats.skippedBuckets} skipped`
)
console.log(
`Files: ${migrationStats.successfulFiles} succeeded, ${migrationStats.failedFiles} failed (${migrationStats.totalFiles} total)`
)
if (migrationStats.failedFiles > 0) {
console.log('\n⚠️ Failed files:')
migrationStats.failedFilesList.forEach((path) => console.log(` - ${path}`))
return migrationStats
}
return migrationStats
}
migrateBuckets()
.then((stats) => {
if (stats.failedFiles > 0) {
console.log(`\n⚠️ Migration completed with ${stats.failedFiles} failed files.`)
process.exit(1)
} else {
console.log('\n🎉 Migration completed successfully!')
process.exit(0)
}
})
.catch((err) => {
console.error('❌ Fatal error during migration:', err.message)
process.exit(1)
})
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Add the relevant project variables to the script">
Get the [secret keys](/dashboard/project/_/settings/api-keys) or [service_role keys](/dashboard/project/_/settings/api-keys/legacy) for both your new and old projects, then substitute them into the script.
From the [Data API settings](/dashboard/project/_/integrations/data_api/overview), copy your project URL and add it to the script as well.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```js name='index.js'
//rest of code
...
// add relevant details for old project
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
// add relevant details for new project
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
...
//rest of code
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Run the script from your command line">
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm_run"
queryGroup="npm_run"
>
<TabPanel id="npm_run" label="node">
```bash
node index.js
```
</TabPanel>
<TabPanel id="bun_run" label="bun">
```bash
bun index.js
```
</TabPanel>
</Tabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
## Resources
- [Connecting with PSQL](/docs/guides/database/psql)
@@ -60,7 +60,7 @@ height={625}
| Action | Description | More information |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Configure connection limit | Adjust the "Max concurrent connections" setting to increase or decrease the connection limit for your project | [Realtime Settings Guide](/docs/guides/realtime/settings) |
| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/docs/guides/platform/pricing) |
| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/pricing) |
| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/quotas) |
| Understand connection quota | Learn how the concurrent connections quota works and how to configure it for your plan | [Concurrent Peak Connections Quota Troubleshooting](/docs/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp) |
| Fix silent disconnections | Fix connection issues in background applications using heartbeat callbacks and Web Workers | [Handling Silent Disconnections in Background Apps](/docs/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) |
@@ -0,0 +1,175 @@
---
title: 'Custom Email Templates'
description: 'Configure custom email templates with self-hosted Supabase instance'
subtitle: 'Configure custom email templates with self-hosted Supabase instance'
---
When running a self-hosted Supabase instance, you can fully customize emails sent by Supabase Auth.
## Overview
Supabase Auth does not read email templates from mounted Docker volumes. Instead, it expects each template to be available at a URL that returns a valid HTML template.
This URL:
- Does not need to be public
- Must be reachable from `auth` service
- Must return a valid Golang HTML template
To provide templates to Supabase Auth, you need a service that serves static HTML files. This can be any server of your choice. You can even use `kong` service which is included with the default Supabase docker configuration. The only requirement is that the `auth` service must be able to reach it via a HTTP GET request.
This guide uses [Caddy](https://github.com/caddyserver/caddy) for serving templates.
<Admonition type="tip">
If Supabase Auth cannot fetch the template or if the fetched template is invalid, it falls back to the default template.
</Admonition>
## Authentication email templates
Authentication email templates can be configured using the following environment variables:
- `GOTRUE_MAILER_TEMPLATES_<AUTH_FLOW>`: Provide a custom template URL. Falls back to the default template if not set.
- `GOTRUE_MAILER_SUBJECTS_<AUTH_FLOW>`: Customize the email subject. Falls back to the default subject if not set.
| Auth flow | Sent |
| ------------------ | -------------------------------------------------------------------- |
| `CONFIRMATION` | When a user signs up and needs to verify their email address |
| `RECOVERY` | When a user requests a password reset |
| `MAGIC_LINK` | When a user requests a magic link for password-less authentication |
| `INVITE` | When a user is invited to join your application via email invitation |
| `EMAIL_CHANGE` | When a user requests to change their email address |
| `REAUTHENTICATION` | When a user needs to re-authenticate for sensitive operations |
For example:
```sh
GOTRUE_MAILER_TEMPLATES_MAGIC_LINK='<template_url>'
GOTRUE_MAILER_SUBJECTS_MAGIC_LINK='<custom_subject>'
```
### Example
Below is an example configuration for setting up a custom invite template.
### Step 1: Create a templates directory
Create a `templates` directory inside the existing `volumes` directory and add your email templates to it.
Your directory structure should look like this:
```
volumes/
templates/
invite.html
```
### Step 2: Update `docker-compose.yml`
Update the `auth` service to depend on `templates-server`, and pass the email template environment variables. Then add a `templates-server` service to serve the templates from `./volumes/templates`.
```yml
services:
auth:
depends_on:
db:
condition: service_healthy
templates-server: # 👈 new dependency
condition: service_started
environment:
GOTRUE_MAILER_TEMPLATES_INVITE: 'http://templates-server/invite.html'
GOTRUE_MAILER_SUBJECTS_INVITE: 'You have been invited'
templates-server:
image: caddy:2-alpine
command: ['caddy', 'file-server', '-r', '/templates', '--listen', ':80']
volumes:
- ./volumes/templates:/templates
```
#### What this configuration does
- Adds a `templates-server`service that runs alongside the Supabase services in the same docker network.
- Serves your custom email template files from the `./volumes/templates` directory.
- Keeps the templates-server private to the Docker network (no published ports), so it is not accessible from outside.
- Allows the `auth` service to fetch templates via `http://templates-server/<template>.html`.
### Step 3: Restart containers
```sh
docker compose up -d --force-recreate --no-deps auth templates-server
```
## Notification email templates
Notification email templates can be configured using the following environment variables:
- `GOTRUE_MAILER_NOTIFICATIONS_<NOTIFICATION_TYPE>_ENABLED`: Enable the notification email
- `GOTRUE_MAILER_TEMPLATES_<NOTIFICATION_TYPE>_NOTIFICATION`: Provide a custom template URL. Falls back to the default template if not set.
- `GOTRUE_MAILER_SUBJECTS_<NOTIFICATION_TYPE>_NOTIFICATION`: Customize the email subject. Falls back to the default subject if not set.
| Notification Type | Sent |
| ----------------------- | ----------------------------------------------------- |
| `PASSWORD_CHANGED` | When a user's password is changed |
| `EMAIL_CHANGED` | When a user's email address is changed |
| `PHONE_CHANGED` | When a user's phone number is changed |
| `MFA_FACTOR_ENROLLED` | When a new MFA factor is added to the user's account |
| `MFA_FACTOR_UNENROLLED` | When an MFA factor is removed from the user's account |
| `IDENTITY_LINKED` | When a new identity is linked to the account |
| `IDENTITY_UNLINKED` | When an identity is unlinked from the account |
For example:
```sh
GOTRUE_MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED='true'
GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGED_NOTIFICATION='<template_url>'
GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGED_NOTIFICATION='<custom_subject>'
```
### Example
Below is an example configuration for setting up a custom password changed notification template.
### Step 1: Create the templates directory
This example reuses the directory created in **Auth Email Templates – Step 1** (`volumes/templates`). Add your notification email templates to this directory.
Your directory structure should look like this:
```
volumes/
templates/
invite.html
password_changed_notification.html
```
### Step 2: Update `docker-compose.yml`
Update the `auth` service in `docker-compose.yml` to enable password changed notification
```yml
services:
auth:
depends_on:
db:
condition: service_healthy
templates-server:
condition: service_started
environment:
GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: 'true' # 👈 enabling the notification is required
GOTRUE_MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION: 'http://templates-server/password_changed_notification.html'
GOTRUE_MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION: 'Your password has been changed'
templates-server:
image: caddy:2-alpine
command: ['caddy', 'file-server', '-r', '/templates', '--listen', ':80']
volumes:
- ./volumes/templates:/templates
```
### Step 3: Restart containers
```sh
docker compose up -d --force-recreate --no-deps auth templates-server
```
@@ -446,7 +446,7 @@ See the [Configure Phone Login & MFA](/docs/guides/self-hosting/self-hosted-phon
Configuring the Supabase AI Assistant is optional. By adding **your own** `OPENAI_API_KEY` to `.env` you can enable AI services, which help with writing SQL queries, statements, and policies.
#### Setting database's `log_min_messages`
#### Setting log_min_messages in Postgres
By default, the database's `log_min_messages` configuration is set to `fatal` in [docker-compose.yml](https://github.com/supabase/supabase/blob/df8729a82b1847e2989c14ede27965612761d503/docker/docker-compose.yml#L466) to prevent redundant logs generated by Realtime. You can configure `log_min_messages` using any of the Postgres [Severity Levels](https://www.postgresql.org/docs/current/runtime-config-logging.html#RUNTIME-CONFIG-SEVERITY-LEVELS).
@@ -131,7 +131,7 @@ When `JWT_KEYS` is set, Auth will start signing new user session JWTs with the n
</Admonition>
## Rotating `sb_` API keys
## Rotating the new API keys
If your new API keys are compromised or you want to rotate them periodically, you can regenerate `sb_publishable` and `sb_secret` without touching the asymmetric key pair:
@@ -175,9 +175,9 @@ Regenerating asymmetric keys invalidates all ES256 user sessions. Plan a mainten
Below are a few notes on the details of the new authentication architecture.
### What `supabase-js` sends
### What client SDK sends
Every request includes two headers:
Every request via `supabase-js` includes two headers:
- `apikey` - the API key (`sb_` or legacy JWT)
- `Authorization` - when unauthenticated, the client SDK copies the API key here (`Bearer sb_publishable_xxx` or `Bearer eyJ...`). When authenticated, this contains the user session JWT minted by Auth.
@@ -24,7 +24,7 @@ This returns `"Hello from Edge Functions!"`.
## Create a new function
### Step 1: Create a new directory with an `index.ts` file in `volumes/functions/`:
### Step 1: Add a new function directory and the function code
```
mkdir -p volumes/functions/my-function &&
@@ -44,13 +44,13 @@ Deno.serve(async (req: Request) => {
})
```
### Step 2: Restart the functions service to pick up the new function:
### Step 2: Restart the functions service to pick up the new function
```bash
docker compose restart functions --no-deps
```
### Step 3: Invoke your function:
### Step 3: Invoke your function
```bash
curl -X POST http://<your-domain>:8000/functions/v1/my-function \
@@ -56,7 +56,7 @@ The default `.env.example` and `docker-compose.yml` include commented-out placeh
2. Set the **authorized redirect URL**, e.g., `https://<your-domain>/auth/v1/callback`
3. Copy the **client ID** and **client secret** into your `.env` file.
### Step 2: Configure variables in the `.env` file
### Step 2: Configure environment variables
Uncomment the lines for your provider in `.env` and add your client ID and secret, e.g., for Google:
@@ -66,7 +66,9 @@ GOOGLE_CLIENT_ID=your-client-id
GOOGLE_SECRET=your-client-secret
```
### Step 3: Enable the matching lines in `docker-compose.yml`
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
### Step 3: Enable the matching lines in Docker Compose configuration
Uncomment the corresponding `GOTRUE_EXTERNAL_` lines in the `auth` service's `environment`:
@@ -400,12 +402,12 @@ For detailed client-side integration, see [Social Login](/docs/guides/auth/socia
## Troubleshooting
### "Provider not enabled" or provider shows `false` in `/auth/v1/settings`
### "Provider not enabled" or provider seen as false in settings
- Check that `GOTRUE_EXTERNAL_*_ENABLED` is set to `true` in `docker-compose.yml`
- Verify the `.env` variable is not empty, e.g., check with `docker compose exec auth env | grep GOOGLE`
### Variables added to `.env` but provider still not working
### Variables added to the environment but provider still not working
Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
@@ -415,7 +417,7 @@ GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
Run `docker compose exec auth env | grep GOTRUE_EXTERNAL` to verify the variables are reaching the container.
### `SITE_URL` or redirect URL errors after login
### Site URL or redirect URL errors after login
After a successful OAuth login, the Auth service redirects to `SITE_URL` or a URL from `ADDITIONAL_REDIRECT_URLS`. Ensure:
@@ -23,7 +23,7 @@ The default `.env.example` and `docker-compose.yml` include commented-out SMS pr
To enable SMS delivery:
### Step 1: Uncomment and configure the settings in `.env`
### Step 1: Uncomment and configure the environment variables
```
SMS_PROVIDER=twilio
@@ -38,7 +38,9 @@ SMS_TWILIO_AUTH_TOKEN=your-auth-token
SMS_TWILIO_MESSAGE_SERVICE_SID=your-message-service-sid
```
### Step 2: Uncomment the matching lines in `docker-compose.yml`
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
### Step 2: Uncomment the matching lines in Docker Compose configuration
Uncomment the `GOTRUE_SMS_*` lines in the `auth` service's `environment` block:
@@ -200,7 +202,7 @@ Common causes:
- Provider credentials are wrong
- Phone number format is wrong (use E.164 format: `+1234567890`)
### Variables are configured in `.env` but not working
### Variables added to the environment but not working
Configuration variables from `.env` are **not** automatically available inside the container unless there's a matching passthrough definition in `docker-compose.yml`. Check, e.g., for:
@@ -147,7 +147,7 @@ kong:
KONG_SSL_CERT_KEY: /home/kong/server.key
```
### Step 3: Update configuration variables in `.env`
### Step 3: Update configuration variables
Edit your `.env` file to use HTTPS with the Kong HTTPS port:
@@ -1,93 +1,139 @@
---
title = "Edge Function 401 error response"
topics = [ "functions" ]
keywords = [ "401", "error" ]
database_id = "12e09c3f-a48b-4aea-9df3-90a25ef0021e"
keywords = ["401", "error", "JWT", "authorization"]
[[errors]]
http_status_code = 401
message = "Invalid JWT"
---
The function rejected the request for lacking the appropriate authorization headers.
A 401 response from an Edge Function means either:
## Context for the error
- The function failed the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
- Your function's logic deliberately returned a 401 response
#### The JWT verification check
## Quick triage
By default, edge functions are configured to check requests for a valid [legacy key](/docs/guides/api/api-keys#overview).
Check the response body returned by the request
#### How the check causes 401 errors
The check fails and return a 401 permission error if:
- The request includes an invalid legacy key, or
- The request uses the newer asymmetric keys instead of a legacy key.
#### What this check actually does
This validation provides limited security value. It only confirms that the request includes a legacy token associated with your project, such as the anon key.
It does not validate the user's identity or permissions beyond that.
Because the security check is weak, it was deprecated when used with the new asymmetric keys.
## Solving the error
### Step 1: Identifying the error
<Admonition type="note">
If the tests return a 401 but don't match the criteria below, the error is coming from your app logic, not the JWT check.
</Admonition>
### Inspecting the return message
When an edge function fails due to a platform 401 error, it returns the error:
### Case 1: `"Invalid JWT"` or `"Missing authorization header"`
```json
{
"code": 401,
"message": "Invalid JWT"
}
{ "code": 401, "message": "Invalid JWT" }
```
### Inspecting the logs
```json
{ "code": 401, "message": "Missing authorization header" }
```
Your code may return a 401 error due to its own logic
Both of these messages come from the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
Go to: [Built-in JWT check failures](#built-in-jwt-check-failures)
### Case 2: Custom message or empty body
If the response body contains a message you coded, or nothing at all, then your function code _did_ execute and returned a 401 itself.
Go to: [Your function returned a 401](#your-function-returned-a-401)
### Case 3: Not sure
Run this query in [Log Explorer](/dashboard/project/_/logs/explorer?q=SELECT%0A++++cast%28timestamp+AS+datetime%29++AS+timestamp%2C%0A++++req.pathname+++++++++++++++++AS+function_name%2C%0A%0A++++CASE%0A++++++++WHEN+metadata.execution_id+IS+NOT+NULL%0A++++++++++++THEN+%27your_code_returned_401%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+%28new_auth.prefix+IS+NOT+NULL+OR+legacy_payload.algorithm+<>+%27HS256%27%29%0A++++++++++++THEN+%27incompatible_keys%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A++++++++AND+%0A++++++++++++%28%0A++++++++++++++++%28legacy_auth_data.invalid+IS+NOT+NULL+OR+new_auth.error+IS+NOT+NULL%29%0A++++++++++++++++++++OR%0A++++++++++++++++legacy_payload.algorithm+%3D+%27HS256%27%0A++++++++++++%29%0A++++++++++++THEN+%27invalid_key%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+legacy_auth_data+++++++IS+NULL%0A+++++++++AND+new_auth.prefix+IS+NULL%0A++++++++++++THEN+%27missing_auth_header%27%0A++++END+AS+cause%0A%0AFROM+function_edge_logs%0A%0A++++--+unnesting+metadata%0A++++CROSS+JOIN+UNNEST%28metadata%29++++++++++AS+metadata%0A++++CROSS+JOIN+UNNEST%28metadata.request%29++AS+req%0A++++CROSS+JOIN+UNNEST%28metadata.response%29+AS+res%0A++++--+unnesting+auth+details%0A++++LEFT+JOIN+UNNEST%28req.sb%29++++++++++++++++++++AS+sb%0A++++LEFT+JOIN+UNNEST%28sb.apikey%29+++++++++++++++++AS+apikey%0A++++LEFT+JOIN+UNNEST%28apikey.authorization%29++++++AS+new_auth%0A++++LEFT+JOIN+UNNEST%28sb.jwt%29++++++++++++++++++++AS+legacy_jwt%0A++++LEFT+JOIN+UNNEST%28legacy_jwt.authorization%29++AS+legacy_auth_data%0A++++LEFT+JOIN+UNNEST%28legacy_auth_data.payload%29++AS+legacy_payload%0A%0AWHERE+res.status_code+%3D+401%0AORDER+BY+timestamp+DESC%0ALIMIT+200) to classify recent 401s:
```sql
select
cast(timestamp as datetime) as timestamp,
req.pathname as function_name,
case
when metadata.execution_id is not null then 'your_code_returned_401'
when metadata.execution_id is null
and (
new_auth.prefix is not null
or legacy_payload.algorithm != 'HS256'
) then 'incompatible_keys'
when metadata.execution_id is null
and (
(legacy_auth_data.invalid is not null or new_auth.error is not null)
or legacy_payload.algorithm = 'HS256'
) then 'invalid_key'
when metadata.execution_id is null
and legacy_auth_data is null
and new_auth.prefix is null then 'missing_auth_header'
end as cause
from
function_edge_logs
-- unnesting metadata
cross join UNNEST(metadata) as metadata
cross join UNNEST(metadata.request) as req
cross join UNNEST(metadata.response) as res
-- unnesting auth details
left join UNNEST(req.sb) as sb
left join UNNEST(sb.apikey) as apikey
left join UNNEST(apikey.authorization) as new_auth
left join UNNEST(sb.jwt) as legacy_jwt
left join UNNEST(legacy_jwt.authorization) as legacy_auth_data
left join UNNEST(legacy_auth_data.payload) as legacy_payload
where res.status_code = 401
order by timestamp desc
limit 50;
```
Depending on the output, you can use this table to find the appropriate debugging section:
| Value | Go to |
| ------------------------ | ------------------------------------------------------------- |
| `your_code_returned_401` | [Your function returned a 401](#your-function-returned-a-401) |
| `incompatible_keys` | [Incompatible key format](#incompatible-key-format) |
| `invalid_key` | [Invalid key](#invalid-key) |
| `missing_auth_header` | [Missing Authorization header](#missing-authorization-header) |
---
## Your function returned a 401
Your function ran, and somewhere in your code, its logic returned a 401.
**Example:**
```js
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 401, // app logic returning a 401
status: 401, // <-- you set this
})
```
A 401 status code alone doesn't confirm a JWT check failure by itself. The log must also lack an `execution_id`.
**How to fix:**
![image](/docs/img/troubleshooting/401_in_the_logs.png)
1. Search your function code for `401`. Look for explicit status codes on `Response` objects.
2. Trace the condition that triggered it. If you're interacting with a third-party API in your code, that service may be returning 401 that you're forwarding in the response object.
3. Add logging before the return so future occurrences leave a trace:
Instead of manually reviewing the logs, you can run the below query in the [log explorer](</dashboard/project/_/logs/explorer?q=SELECT+DISTINCT%0A++++req.pathname+AS+function_name,%0A++++res.status_code%0A++FROM+function_edge_logs%0A++CROSS+JOIN+UNNEST(metadata)+AS+metadata+%0A++CROSS+JOIN+UNNEST(metadata.request)+AS+req+%0A++CROSS+JOIN+UNNEST(metadata.response)+AS+res+%0AWHERE+status_code+=+401+AND+metadata.execution_id+IS+NULL%0ALIMIT+10;&its=&ite=%3E>) to get a list of functions that were impacted by the check:
```sql
select distinct
req.pathname as function_name,
res.status_code
from
function_edge_logs
cross join UNNEST(metadata) as metadata
cross join UNNEST(metadata.request) as req
cross join UNNEST(metadata.response) as res
where status_code = 401 and metadata.execution_id is null
limit 10;
```js
console.error('Returning 401 - reason:', reason)
```
### Step 2: Disabling the JWT check
See: [Error handling in Edge Functions](/docs/guides/functions/error-handling)
The JWT check provides minimal security benefits, so we now recommend handling authentication through app logic instead. See the [Edge Function Auth Doc](/docs/guides/functions/auth) for details.
---
If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/_/settings/api-keys) or no longer need the JWT check, you can disable it using one of the three options below:
## Built-in JWT check failures
Supabase Edge Functions have a legacy auth verification check that runs before your code. When it fails, your function never executes, and you get a 401 with `"Invalid JWT"` or `"Missing authorization header"` directly from the platform.
<Admonition type="deprecation">
Supabase now recommends turning off this built-in check and managing authentication directly in your function code, giving you more control over access. See [Securing Edge Functions](/docs/guides/functions/auth).
</Admonition>
The subsections below cover specific failure modes.
### Incompatible key format
Your project uses the [new asymmetric keys](/blog/jwt-signing-keys) for authentication. However, the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks) only understands the legacy format.
**Fix:** Disable the built-in JWT check using one of the below methods and optionally [handle auth in your function code](/docs/guides/functions/auth)
<Accordion
type="default"
@@ -99,51 +145,49 @@ If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/
>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="In the Dashboard"
header="Method A: Dashboard"
id="item-1"
>
In the [Function Dashboard](/dashboard/project/_/functions/), under the offending function's detail tab, you can toggle off the check:
![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png)
In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected function's `detail tab` and toggle off JWT verification.
![image](/docs/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png)
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Method B: Supabase CLI"
id="item-2"
>
Redeploy the edge function from the [Supabase CLI](/docs/guides/functions/quickstart) with the `--no-verify-jwt` flag
```sh
supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt
```
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="With the Management API"
id="item-2"
>
1. Generate a secret token in the [Account Preferences Dashboard](/dashboard/account/tokens)
2. Get your project ID from the [General Settings](/dashboard/project/_/settings/general)
3. Substitute in the `PROJECT_ID`, `FUNCTION_NAME`, and `SECRET_TOKEN` in below [management endpoint](/docs/reference/api/v1-update-a-function). Then execute the request from a terminal environment
```sh
# remember to substitute in your PROJECT_ID, FUNCTION_NAME, and SECRET_TOKEN
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME?verify_jwt=false' \
--request PATCH \
--header 'Content-Type: application/vnd.denoland.eszip' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '""'
````
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="With the Supabase CLI"
header="Method C: Management API"
id="item-3"
>
Disable the legacy auth check with the [Supabase Management API](/docs/reference/api/introduction):
With the [Supabase CLI](/docs/guides/local-development/cli/getting-started), you can redeploy your edge function without the JWT check:
1. Generate a token at [Account Preferences](/dashboard/account/tokens).
2. Get your project ID from [General Settings](/dashboard/project/_/settings/general).
3. Run:
```sh
supabase functions deploy hello-world --no-verify-jwt
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \
--request PATCH \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{"verify_jwt": false}'
```
</AccordionItem>
@@ -151,12 +195,43 @@ supabase functions deploy hello-world --no-verify-jwt
</div>
</Accordion>
If you would like to continue using the JWT check, make sure that your [Supabase Client](/docs/guides/api/rest/client-libs) only uses your [legacy keys](/dashboard/project/_/settings/api-keys/legacy).
### Invalid key
The built-in check is enabled and the key you sent doesn't match your project's keys.
**Fix (recommended):** Disable the built-in check using the steps in [Incompatible key format](#incompatible-key-format).
**Fix (alternative):** If you want to keep the built-in check, ensure you're sending a valid key. Use one of your [legacy API keys](/dashboard/project/_/settings/api-keys/legacy) with the [Supabase client library](/docs/guides/api/rest/client-libs) when making your request.
```js
const supabase = createClient('https://xyzcompany.supabase.co', 'anon-key-or-service_role-key')
```
### Missing authorization header
The built-in check is enabled but your request has no `Authorization` header at all.
If you're using a [Supabase client library](/docs/guides/api/rest/client-libs), the header is added automatically. If you're calling the function from an external client (cURL, fetch, etc.), you need to supply it:
```sh
curl -L -X POST 'https://PROJECT_REF.supabase.co/functions/v1/hello-world' \
-H 'Authorization: Bearer YOUR_ANON_OR_SERVICE_ROLE_KEY' \
--data '{"name":"Functions"}'
```
Alternatively, you can disable the built-in check entirely (see [Incompatible key format](#incompatible-key-format)).
---
## Additional resources
- [Securing Edge Functions](/docs/guides/functions/auth)
- [Debugging Edge Functions](/docs/guides/functions/logging)
- [Quickstart Deployment: Dashboard](/docs/guides/functions/quickstart-dashboard)
- [Quickstart Deployment: CLI](/docs/guides/functions/quickstart)
````
- [Securing Edge Functions with Auth](/docs/guides/functions/auth)
- [Logging Edge Function Requests](/docs/guides/functions/logging)
- [Error Handling Edge Functions](/docs/guides/functions/error-handling)
- [Quickstart: Dashboard deployment](/docs/guides/functions/quickstart-dashboard)
- [Quickstart: CLI deployment](/docs/guides/functions/quickstart)
## Still stuck?
- Check the [Discord](https://discord.com/channels/839993398554656828/1006358244786196510), [Supabase GitHub Discussions](https://github.com/orgs/supabase/discussions), and [Reddit page](https://www.reddit.com/r/Supabase/) for similar reports that can help with debugging
- Open a [support ticket](/dashboard/support/new) for your project if the problem persists and you believe it is a platform issue
@@ -7,6 +7,7 @@ import { IconPanel } from 'ui-patterns/IconPanel'
import SqlToRest from 'ui-patterns/SqlToRest'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex'
import { AiSkillsIndex } from '~/app/guides/getting-started/ai-skills/AiSkillsIndex'
import { AppleSecretGenerator } from '~/components/AppleSecretGenerator'
import AuthProviders from '~/components/AuthProviders'
import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig'
@@ -44,6 +45,7 @@ const components = {
AccordionItem,
Admonition: AdmonitionWithMargin,
AiPromptsIndex,
AiSkillsIndex,
AuthSmsProviderConfig,
AppleSecretGenerator,
AuthProviders,
+34 -9
View File
@@ -265,15 +265,35 @@ function normalizeComment(original: TypedocComment | Comment | undefined): Comme
if (t.name) {
return [t.name, t.content.map((p) => p.text).join('')]
}
const first = t.content[0]
// TypeDoc may produce a tag with an empty content array (e.g. a tag with
// no body text). Guard here so the rest of the parsing can safely assume
// a non-empty first element.
if (!first) return ['', '']
const newline = first.text.indexOf('\n')
const name = (newline >= 0 ? first.text.slice(0, newline) : first.text).trim()
const tail = newline >= 0 ? first.text.slice(newline + 1) : ''
const body = [{ ...first, text: tail }, ...t.content.slice(1)].map((p) => p.text).join('')
if (!t.content[0]) return ['', '']
// The tag name may span multiple content nodes (e.g. "With " + "`select()`"
// when the name contains inline code). Scan through nodes until a newline.
const nameParts: string[] = []
let bodyStartIdx = 0
let bodyStartText = ''
let foundNewline = false
for (let i = 0; i < t.content.length; i++) {
const part = t.content[i]
const newline = part.text.indexOf('\n')
if (newline >= 0) {
nameParts.push(part.text.slice(0, newline))
bodyStartIdx = i
bodyStartText = part.text.slice(newline + 1)
foundNewline = true
break
} else {
nameParts.push(part.text)
}
}
const name = nameParts.join('').trim()
const body = foundNewline
? [{ text: bodyStartText }, ...t.content.slice(bodyStartIdx + 1)]
.map((p) => p.text)
.join('')
: ''
return [name, body]
}
@@ -539,9 +559,14 @@ function parseMethod(
let { params, ret, comment } = parseSignature(signature, map)
// When a method has multiple overload signatures, TypeDoc places the shared
// JSDoc on the method node rather than any individual signature.
if (!comment && node.comment) {
comment = normalizeComment(node.comment)
// JSDoc on the method node rather than any individual signature. Always merge
// node.comment as the base so that block tags (@remarks, @example, etc.) are
// not lost when overload signatures already carry a minimal summary comment.
if (node.comment) {
const nodeComment = normalizeComment(node.comment)
if (nodeComment) {
comment = { ...nodeComment, ...comment }
}
}
const types: MethodTypes = {
File diff suppressed because it is too large. Load diff
@@ -1,7 +1,8 @@
import fs from 'fs'
import path from 'path'
import fs from 'node:fs'
import path from 'node:path'
import { globby } from 'globby'
import matter from 'gray-matter'
import { create as createTar } from 'tar'
const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')
@@ -160,6 +161,16 @@ async function generate() {
const summary = warnings ? ` (${warnings} with warnings)` : ''
console.log(`Generated ${files.length} markdown files under public/docs/guides/${summary}`)
// Create a tar.gz archive of the generated docs, served at /docs/docs.tar.gz.
// Sorted entries, portable headers, and a fixed mtime keep the output deterministic.
const archivePath = 'public/docs.tar.gz'
const entries = (await globby(['**'], { cwd: 'public/docs' })).sort()
await createTar(
{ gzip: true, file: archivePath, cwd: 'public/docs', portable: true, mtime: new Date() },
entries
)
console.log(`Created archive at ${archivePath}`)
}
generate()
+54 -35
View File
@@ -2,11 +2,19 @@ import 'server-only'
import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { retry } from '@octokit/plugin-retry'
import crypto from 'node:crypto'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
let octokitInstance: Octokit
const RetryOctokit = Octokit.plugin(retry)
export const OCTOKIT_RETRY_OPTIONS = {
retries: 5,
retryAfter: 1,
} as const
let octokitInstance: InstanceType<typeof RetryOctokit>
export function octokit() {
if (!octokitInstance) {
@@ -21,7 +29,7 @@ export function octokit() {
format: 'pem',
})
octokitInstance = new Octokit({
octokitInstance = new RetryOctokit({
authStrategy: createAppAuth,
auth: {
appId: process.env.DOCS_GITHUB_APP_ID,
@@ -34,19 +42,13 @@ export function octokit() {
return octokitInstance
}
async function getGitHubFileContents({
org,
repo,
path,
branch,
options: { onError, fetch },
}: {
type GithubFileRequest = {
org: string
repo: string
path: string
branch: string
options: {
onError: (err?: unknown) => void
options?: {
onError?: (err?: unknown) => void
/**
*
* A custom fetch implementation to control Next.js caching.
@@ -55,34 +57,59 @@ async function getGitHubFileContents({
*/
fetch?: (info: RequestInfo, init?: RequestInit) => Promise<Response>
}
}) {
}
export async function getGitHubFileContents({
org,
repo,
path,
branch,
options: { onError, fetch } = {},
}: GithubFileRequest) {
if (path.startsWith('/')) {
path = path.slice(1)
}
const client = octokit()
let response: Awaited<
ReturnType<typeof client.request<'GET /repos/{owner}/{repo}/contents/{path}'>>
>
try {
const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
response = await client.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: org,
repo: repo,
path: path,
ref: branch,
request: OCTOKIT_RETRY_OPTIONS,
options: {
fetch: fetch ?? fetchRevalidatePerDay,
},
})
if (response.status !== 200 || !response.data) {
throw Error(`Could not find contents of ${path} in ${org}/${repo}`)
}
if (!('type' in response.data) || response.data.type !== 'file') {
throw Error(`${path} in ${org}/${repo} is not a file`)
}
const content = Buffer.from(response.data.content, 'base64').toString('utf-8')
return content
} catch (err) {
console.error('Error fetching GitHub file: %o', err)
onError?.(err)
return ''
const error = new Error(
`getGitHubFileContents: request failed for ${org}/${repo}/${path}@${branch}`,
{ cause: err }
)
onError?.(error)
throw error
}
if (Array.isArray(response.data)) {
const error = new Error(
`getGitHubFileContents: ${path} in ${org}/${repo} is a directory, not a file`
)
onError?.(error)
throw error
}
if (!('content' in response.data) || response.data.type !== 'file') {
const error = new Error(
`getGitHubFileContents: unexpected response for ${path} in ${org}/${repo} (type: ${'type' in response.data ? response.data.type : 'unknown'})`
)
onError?.(error)
throw error
}
return Buffer.from(response.data.content, 'base64').toString('utf-8')
}
export async function getGitHubFileContentsImmutableOnly({
@@ -90,17 +117,8 @@ export async function getGitHubFileContentsImmutableOnly({
repo,
branch,
path,
options: { onError, fetch },
}: {
org: string
repo: string
branch: string
path: string
options: {
onError: (error: unknown) => void
fetch: (url: string) => Promise<Response>
}
}): Promise<string> {
options: { onError, fetch } = {},
}: GithubFileRequest): Promise<string> {
const isImmutableCommit = await checkForImmutableCommit({
org,
repo,
@@ -145,6 +163,7 @@ async function checkForImmutableCommit({
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
request: OCTOKIT_RETRY_OPTIONS,
options: {
fetch: _fetch,
},
+4 -2
View File
@@ -49,9 +49,10 @@
"@next/bundle-analyzer": "15.3.1",
"@next/mdx": "15.3.1",
"@octokit/auth-app": "^7.0.0",
"@octokit/core": "^6.0.0",
"@octokit/core": "^7.0.6",
"@octokit/graphql": "^8.0.0",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@octokit/plugin-retry": "^8.1.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@sentry/nextjs": "catalog:",
@@ -91,7 +92,7 @@
"mdast-util-to-string": "^3.1.1",
"micromark-extension-gfm": "^2.0.3",
"micromark-extension-mdxjs": "^1.0.0",
"next": "catalog:",
"next": "^15.5.14",
"next-mdx-remote": "^6.0.0",
"next-plugin-yaml": "^1.0.1",
"next-themes": "^0.3.0",
@@ -162,6 +163,7 @@
"slugify": "^1.6.6",
"smol-toml": "^1.3.1",
"tailwindcss": "catalog:",
"tar": "^7.5.13",
"tsconfig": "workspace:*",
"tsx": "catalog:",
"twoslash": "^0.3.1",
+4
View File
@@ -51,6 +51,7 @@ Chris Martin
Chris Stockton
Chris Ward
Clayton Kast
Colum Ferry
Craig Cannon
Cuong Do
Daniel Velázquez Lara
@@ -58,6 +59,7 @@ Danny White
Darren Cunningham
Dave Wilson
David A. Ventimiglia
David Alvarez
David Birdsong
David Camacho Cateura
David Weitzman
@@ -114,6 +116,7 @@ Jess Shears
Jim Brodeur
Jim Chanco Jr
Joakim Ahrlin
Joanna Michniewicz
Joaquim Moreno
Joel Low
Joey Freeland
@@ -124,6 +127,7 @@ John Pena
John Schaeffer
Jon M
Jonny Summers-Muir
Jordan McQueen
Jordi Enric
José Luis Ledesma
Joshen Lim
+44 -5
View File
@@ -4565,6 +4565,44 @@
"x-endpoint-owners": ["security"]
}
},
"/v1/projects/{ref}/database/openapi": {
"get": {
"description": "Returns the PostgREST OpenAPI specification for the project. This is the replacement for querying `/rest/v1/` directly with the anon key.",
"operationId": "v1-get-database-openapi",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": { "minLength": 20, "maxLength": 20, "pattern": "^[a-z]+$", "type": "string" }
},
{
"name": "schema",
"required": false,
"in": "query",
"description": "The database schema to generate the OpenAPI spec for",
"schema": { "default": "public", "type": "string" }
}
],
"responses": {
"200": {
"description": "",
"content": { "application/json": { "schema": { "type": "object" } } }
},
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to fetch PostgREST OpenAPI spec" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["database_read"] }],
"summary": "Get PostgREST OpenAPI spec",
"tags": ["Database"],
"x-badges": [{ "name": "OAuth scope: database:read", "position": "after" }],
"x-endpoint-owners": ["management-api"],
"x-oauth-scope": "database:read"
}
},
"/v1/projects/{ref}/functions": {
"get": {
"description": "Returns all functions you've previously added to the specified project.",
@@ -5421,7 +5459,7 @@
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" }
},
"security": [{ "bearer": [] }],
"security": [{ "bearer": [] }, { "fga_permissions": ["realtime_config_read"] }],
"summary": "Gets realtime configuration",
"tags": ["Realtime"],
"x-endpoint-owners": ["realtime"]
@@ -5451,7 +5489,7 @@
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" }
},
"security": [{ "bearer": [] }],
"security": [{ "bearer": [] }, { "fga_permissions": ["realtime_config_write"] }],
"summary": "Updates realtime configuration",
"tags": ["Realtime"],
"x-endpoint-owners": ["realtime"]
@@ -5476,7 +5514,7 @@
"404": { "description": "Tenant not found" },
"429": { "description": "Rate limit exceeded" }
},
"security": [{ "bearer": [] }],
"security": [{ "bearer": [] }, { "fga_permissions": ["realtime_config_write"] }],
"summary": "Shutdowns realtime connections for a project",
"tags": ["Realtime"],
"x-endpoint-owners": ["realtime"]
@@ -10814,9 +10852,10 @@
"type": "object",
"properties": {
"name": { "type": "string" },
"status": { "type": "string", "enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"] }
"status": { "type": "string", "enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"] },
"completed_on": { "type": "string", "format": "date-time", "nullable": true }
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
},
"V1UndoBody": {
"type": "object",
+288 -1
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 2.78.1
version: 2.84.2
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -19,6 +19,20 @@ info:
- id: other-commands
title: Additional Commands
flags:
- id: agent
name: --agent <[ auto | yes | no ]>
description: 'Override agent detection: yes, no, or auto (default auto)'
default_value: auto
accepted_values:
- id: auto
name: auto
type: '[ auto | yes | no ]'
- id: "yes"
name: "yes"
type: '[ auto | yes | no ]'
- id: "no"
name: "no"
type: '[ auto | yes | no ]'
- id: create-ticket
name: --create-ticket
description: create a support ticket for any CLI error
@@ -2801,12 +2815,15 @@ commands:
- local-dev
links: []
subcommands:
- supabase-db-advisors
- supabase-db-diff
- supabase-db-dump
- supabase-db-lint
- supabase-db-pull
- supabase-db-push
- supabase-db-query
- supabase-db-reset
- supabase-db-schema
- supabase-db-start
flags: []
- id: supabase-db-start
@@ -2821,6 +2838,145 @@ commands:
name: --from-backup <string>
description: Path to a logical backup file.
default_value: ''
- id: supabase-db-schema
title: supabase db schema
summary: Manage database schema
tags: []
links: []
subcommands:
- supabase-db-schema-declarative
flags: []
- id: supabase-db-schema-declarative
title: supabase db schema declarative
summary: Manage declarative database schemas
tags: []
links: []
subcommands:
- supabase-db-schema-declarative-generate
- supabase-db-schema-declarative-sync
flags: []
- id: supabase-db-schema-declarative-sync
title: supabase db schema declarative sync
summary: Generate a new migration from declarative schema
description: |2
Generate a new migration by diffing your declarative schema files against the current migration state.
When no declarative schema exists yet, the command offers to run `generate` first. After computing the diff, you can optionally name the migration and apply it to the local database.
Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config.
examples:
- id: with-pg-delta
name: Sync declarative schema with pg-delta
code: |
# After editing declarative schema files, generate a migration:
supabase db schema declarative sync --experimental
response: |
Creating shadow database...
Applying declarative schemas via pg-delta...
Applied 239 statements in 1 round(s).
Enter a name for this migration (press Enter to keep 'declarative_sync'): add_updated_at
Created new migration at supabase/migrations/20260317194051_add_updated_at.sql
Apply this migration to local database? [Y/n]
Connecting to local database...
Applying migration 20260317194051_add_updated_at.sql...
Migration applied successfully.
- id: generate-first
name: Generate declarative schema from migrations
code: |
supabase db schema declarative sync --experimental
response: |
No declarative schema found. Generate a new one ? [Y/n]
Reset local database to match migrations first? (local data will be lost) [y/N] y
Resetting database...
...
Declarative schema written to supabase/declarative
Finished supabase db schema declarative generate.
tags: []
links: []
usage: supabase db schema declarative sync [flags]
subcommands: []
flags:
- id: apply
name: --apply
description: |
Apply the generated migration to the local database without prompting.
default_value: 'false'
- id: file
name: -f, --file <string>
description: Saves schema diff to a new migration file.
default_value: declarative_sync
- id: name
name: --name <string>
description: Name for the generated migration file.
default_value: ''
- id: schema
name: -s, --schema <strings>
description: Comma separated list of schema to include.
default_value: '[]'
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: no-cache
name: --no-cache
description: Disable catalog cache and force fresh shadow database setup.
default_value: 'false'
- id: supabase-db-schema-declarative-generate
title: supabase db schema declarative generate
summary: Generate declarative schema from a database
description: |2
Generate declarative schema files from a database.
Exports the schema of a live database (local, linked, or custom URL) into SQL files under the declarative schema directory. This is the entrypoint for bootstrapping declarative mode.
Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config.
tags: []
links: []
usage: supabase db schema declarative generate [flags]
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: |
Generates declarative schema from the database specified by the connection string (must be percent-encoded).
default_value: ''
- id: linked
name: --linked
description: Generates declarative schema from the linked project.
default_value: 'false'
- id: local
name: --local
description: Generates declarative schema from the local database.
default_value: 'false'
- id: overwrite
name: --overwrite
description: Overwrite declarative schema files without confirmation.
default_value: 'false'
- id: password
name: -p, --password <string>
description: Password to your remote Postgres database.
default_value: ''
- id: reset
name: --reset
description: |
Reset local database before generating (local data will be lost).
default_value: 'false'
- id: schema
name: -s, --schema <strings>
description: Comma separated list of schema to include.
default_value: '[]'
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: no-cache
name: --no-cache
description: Disable catalog cache and force fresh shadow database setup.
default_value: 'false'
- id: supabase-db-reset
title: supabase db reset
summary: Resets the local database to current migrations
@@ -2873,6 +3029,52 @@ commands:
name: --version <string>
description: Reset up to the specified version.
default_value: ''
- id: supabase-db-query
title: supabase db query
summary: Execute a SQL query against the database
description: |-
Execute a SQL query against the local or linked database.
When used by an AI coding agent (auto-detected or via --agent=yes), the default
output format is JSON with an untrusted data warning envelope. When used by a
human (--agent=no or no agent detected), the default output format is table
without the envelope.
tags: []
links: []
usage: supabase db query [sql] [flags]
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: |
Queries the database specified by the connection string (must be percent-encoded).
default_value: ''
- id: file
name: -f, --file <string>
description: Path to a SQL file to execute.
default_value: ''
- id: linked
name: --linked
description: Queries the linked project's database via Management API.
default_value: 'false'
- id: local
name: --local
description: Queries the local database.
default_value: 'true'
- id: output
name: -o, --output <[ json | table | csv ]>
description: 'Output format: table, json, or csv.'
default_value: json
accepted_values:
- id: json
name: json
type: '[ json | table | csv ]'
- id: table
name: table
type: '[ json | table | csv ]'
- id: csv
name: csv
type: '[ json | table | csv ]'
- id: supabase-db-push
title: supabase db push
summary: Push new migrations to the remote database
@@ -3019,6 +3221,10 @@ commands:
name: -s, --schema <strings>
description: Comma separated list of schema to include.
default_value: '[]'
- id: use-pg-delta
name: --use-pg-delta
description: Use pg-delta to pull declarative schema.
default_value: 'false'
- id: supabase-db-lint
title: supabase db lint
summary: Checks local database for typing error
@@ -3270,6 +3476,10 @@ commands:
name: -f, --file <string>
description: Saves schema diff to a new migration file.
default_value: ''
- id: from
name: --from <string>
description: Diff from local, linked, migrations, or a Postgres URL.
default_value: ''
- id: linked
name: --linked
description: Diffs local migration files against the linked project.
@@ -3278,10 +3488,18 @@ commands:
name: --local
description: Diffs local migration files against the local database.
default_value: 'true'
- id: output
name: -o, --output <string>
description: Write explicit diff output to a file path.
default_value: ''
- id: schema
name: -s, --schema <strings>
description: Comma separated list of schema to include.
default_value: '[]'
- id: to
name: --to <string>
description: Diff to local, linked, migrations, or a Postgres URL.
default_value: ''
- id: use-migra
name: --use-migra
description: Use migra to generate schema diff.
@@ -3298,6 +3516,75 @@ commands:
name: --use-pgadmin
description: Use pgAdmin to generate schema diff.
default_value: 'false'
- id: supabase-db-advisors
title: supabase db advisors
summary: Checks database for security and performance issues
description: |
Inspects the database for common security and performance issues such as missing RLS policies, unindexed foreign keys, exposed auth.users, and more.
tags: []
links: []
usage: supabase db advisors [flags]
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: |
Checks the database specified by the connection string (must be percent-encoded).
default_value: ''
- id: fail-on
name: --fail-on <[ none | info | warn | error ]>
description: |
Issue level to exit with non-zero status: none, info, warn, error.
default_value: none
accepted_values:
- id: none
name: none
type: '[ none | info | warn | error ]'
- id: info
name: info
type: '[ none | info | warn | error ]'
- id: warn
name: warn
type: '[ none | info | warn | error ]'
- id: error
name: error
type: '[ none | info | warn | error ]'
- id: level
name: --level <[ info | warn | error ]>
description: 'Minimum issue level to display: info, warn, error.'
default_value: warn
accepted_values:
- id: info
name: info
type: '[ info | warn | error ]'
- id: warn
name: warn
type: '[ info | warn | error ]'
- id: error
name: error
type: '[ info | warn | error ]'
- id: linked
name: --linked
description: Checks the linked project for issues.
default_value: 'false'
- id: local
name: --local
description: Checks the local database for issues.
default_value: 'true'
- id: type
name: --type <[ all | security | performance ]>
description: 'Type of advisors to check: all, security, performance.'
default_value: all
accepted_values:
- id: all
name: all
type: '[ all | security | performance ]'
- id: security
name: security
type: '[ all | security | performance ]'
- id: performance
name: performance
type: '[ all | security | performance ]'
- id: supabase-config
title: supabase config
summary: Manage Supabase project configurations
+6
View File
@@ -262,6 +262,12 @@
"slug": "v1-get-database-metadata",
"type": "operation"
},
{
"id": "v1-get-database-openapi",
"title": "Get database openapi",
"slug": "v1-get-database-openapi",
"type": "operation"
},
{
"id": "v1-get-jit-access",
"title": "Get jit access",
File diff suppressed because it is too large. Load diff
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -337,9 +337,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"line": 46,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L46"
}
],
"signatures": [
@@ -365,15 +365,34 @@
"text": "```ts\nimport { FunctionsClient, FunctionRegion } from '@supabase/functions-js'\n\nconst functions = new FunctionsClient('https://xyzcompany.supabase.co/functions/v1', {\n headers: { apikey: 'public-anon-key' },\n region: FunctionRegion.UsEast1,\n})\n```"
}
]
},
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@example",
"name": "Creating a Functions client",
"content": [
{
"kind": "code",
"text": "```ts\nimport { FunctionsClient, FunctionRegion } from '@supabase/functions-js'\n\nconst functions = new FunctionsClient('https://xyzcompany.supabase.co/functions/v1', {\n headers: { apikey: 'public-anon-key' },\n region: FunctionRegion.UsEast1,\n})\n```"
}
]
}
]
},
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"line": 46,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L46"
}
],
"parameters": [
@@ -414,9 +433,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"line": 54,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L54"
}
],
"type": {
@@ -643,9 +662,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"line": 53,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L53"
}
],
"type": {
@@ -680,9 +699,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"line": 55,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L55"
}
],
"type": {
@@ -703,9 +722,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"line": 52,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L52"
}
]
}
@@ -735,7 +754,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +983,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1019,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1040,9 +1059,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"line": 204,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L204"
}
],
"signatures": [
@@ -1068,15 +1087,424 @@
"text": "```ts\nconst { data, error } = await functions.invoke('hello-world', {\n body: { name: 'Ada' },\n})\n```"
}
]
},
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@remarks",
"content": [
{
"kind": "text",
"text": "- Requires an Authorization header.\n- Invoke params generally match the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) spec.\n- When you pass in a body to your function, we automatically attach the Content-Type header for "
},
{
"kind": "code",
"text": "`Blob`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`ArrayBuffer`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`File`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`FormData`"
},
{
"kind": "text",
"text": " and "
},
{
"kind": "code",
"text": "`String`"
},
{
"kind": "text",
"text": ". If it doesn't match any of these types we assume the payload is "
},
{
"kind": "code",
"text": "`json`"
},
{
"kind": "text",
"text": ", serialize it and attach the "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header as "
},
{
"kind": "code",
"text": "`application/json`"
},
{
"kind": "text",
"text": ". You can override this behavior by passing in a "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header of your own.\n- Responses are automatically parsed as "
},
{
"kind": "code",
"text": "`json`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`blob`"
},
{
"kind": "text",
"text": " and "
},
{
"kind": "code",
"text": "`form-data`"
},
{
"kind": "text",
"text": " depending on the "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header sent by your function. Responses are parsed as "
},
{
"kind": "code",
"text": "`text`"
},
{
"kind": "text",
"text": " by default."
}
]
},
{
"tag": "@example",
"name": "Basic invocation",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n body: { foo: 'bar' }\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Error handling\nA "
},
{
"kind": "code",
"text": "`FunctionsHttpError`"
},
{
"kind": "text",
"text": " error is returned if your function throws an error, "
},
{
"kind": "code",
"text": "`FunctionsRelayError`"
},
{
"kind": "text",
"text": " if the Supabase Relay has an error processing your function and "
},
{
"kind": "code",
"text": "`FunctionsFetchError`"
},
{
"kind": "text",
"text": " if there is a network error in calling your function."
}
]
},
{
"tag": "@example",
"name": "Error handling",
"content": [
{
"kind": "code",
"text": "```js\nimport { FunctionsHttpError, FunctionsRelayError, FunctionsFetchError } from \"@supabase/supabase-js\";\n\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' }\n})\n\nif (error instanceof FunctionsHttpError) {\n const errorMessage = await error.context.json()\n console.log('Function returned an error', errorMessage)\n} else if (error instanceof FunctionsRelayError) {\n console.log('Relay error:', error.message)\n} else if (error instanceof FunctionsFetchError) {\n console.log('Fetch error:', error.message)\n}\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Passing custom headers\nYou can pass custom headers to your function. Note: supabase-js automatically passes the "
},
{
"kind": "code",
"text": "`Authorization`"
},
{
"kind": "text",
"text": " header with the signed in user's JWT."
}
]
},
{
"tag": "@example",
"name": "Passing custom headers",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' }\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Calling with DELETE HTTP verb\nYou can also set the HTTP verb to "
},
{
"kind": "code",
"text": "`DELETE`"
},
{
"kind": "text",
"text": " when calling your Edge Function."
}
]
},
{
"tag": "@example",
"name": "Calling with DELETE HTTP verb",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' },\n method: 'DELETE'\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Invoking a Function in the UsEast1 region\nHere are the available regions:\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.Any`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApNortheast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApNortheast2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSouth1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSoutheast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSoutheast2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.CaCentral1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuCentral1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest3`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.SaEast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsEast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsWest1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsWest2`"
}
]
},
{
"tag": "@example",
"name": "Invoking a Function in the UsEast1 region",
"content": [
{
"kind": "code",
"text": "```js\nimport { createClient, FunctionRegion } from '@supabase/supabase-js'\n\nconst { data, error } = await supabase.functions.invoke('hello', {\n body: { foo: 'bar' },\n region: FunctionRegion.UsEast1\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Calling with GET HTTP verb\nYou can also set the HTTP verb to "
},
{
"kind": "code",
"text": "`GET`"
},
{
"kind": "text",
"text": " when calling your Edge Function."
}
]
},
{
"tag": "@example",
"name": "Calling with GET HTTP verb",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n method: 'GET'\n})\n```"
}
]
},
{
"tag": "@example",
"name": "Example 7",
"content": [
{
"kind": "code",
"text": "```ts\nconst { data, error } = await functions.invoke('hello-world', {\n body: { name: 'Ada' },\n})\n```"
}
]
}
]
},
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"line": 204,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L204"
}
],
"typeParameters": [
@@ -1173,9 +1601,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"line": 75,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1193,8 +1621,18 @@
}
],
"blockTags": [
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@example",
"name": "Setting the authorization header",
"content": [
{
"kind": "code",
@@ -1207,9 +1645,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"line": 75,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"parameters": [
@@ -1255,12 +1693,22 @@
"children": [31, 28]
}
],
"categories": [
{
"title": "Functions",
"children": [2, 31, 28]
},
{
"title": "Other",
"children": [20, 18, 19, 17]
}
],
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1749,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1764,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1835,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1859,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1928,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1943,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1991,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +2020,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +2069,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +2084,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +2132,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +2161,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +2210,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +2225,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +2273,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +2302,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +2325,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +2358,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2467,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2483,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2498,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2544,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2594,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2625,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2659,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2679,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2696,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
@@ -23,7 +23,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 88,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L88"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L88"
}
],
"type": {
@@ -42,7 +42,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 89,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L89"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L89"
}
],
"type": {
@@ -61,7 +61,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 90,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L90"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L90"
}
],
"type": {
@@ -80,7 +80,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 91,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L91"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L91"
}
],
"type": {
@@ -99,7 +99,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 92,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L92"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L92"
}
],
"type": {
@@ -118,7 +118,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 93,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L93"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L93"
}
],
"type": {
@@ -137,7 +137,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 94,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L94"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L94"
}
],
"type": {
@@ -156,7 +156,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 95,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L95"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L95"
}
],
"type": {
@@ -175,7 +175,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 96,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L96"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L96"
}
],
"type": {
@@ -194,7 +194,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 97,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L97"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L97"
}
],
"type": {
@@ -213,7 +213,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 98,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L98"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L98"
}
],
"type": {
@@ -232,7 +232,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 99,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L99"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L99"
}
],
"type": {
@@ -251,7 +251,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 100,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L100"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L100"
}
],
"type": {
@@ -270,7 +270,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 101,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L101"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L101"
}
],
"type": {
@@ -289,7 +289,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 102,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L102"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L102"
}
],
"type": {
@@ -309,7 +309,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 87,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L87"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L87"
}
]
},
@@ -337,9 +337,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"line": 46,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L46"
}
],
"signatures": [
@@ -365,15 +365,34 @@
"text": "```ts\nimport { FunctionsClient, FunctionRegion } from '@supabase/functions-js'\n\nconst functions = new FunctionsClient('https://xyzcompany.supabase.co/functions/v1', {\n headers: { apikey: 'public-anon-key' },\n region: FunctionRegion.UsEast1,\n})\n```"
}
]
},
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@example",
"name": "Creating a Functions client",
"content": [
{
"kind": "code",
"text": "```ts\nimport { FunctionsClient, FunctionRegion } from '@supabase/functions-js'\n\nconst functions = new FunctionsClient('https://xyzcompany.supabase.co/functions/v1', {\n headers: { apikey: 'public-anon-key' },\n region: FunctionRegion.UsEast1,\n})\n```"
}
]
}
]
},
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 34,
"line": 46,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L34"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L46"
}
],
"parameters": [
@@ -414,9 +433,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 42,
"line": 54,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L42"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L54"
}
],
"type": {
@@ -643,9 +662,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 41,
"line": 53,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L41"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L53"
}
],
"type": {
@@ -680,9 +699,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 43,
"line": 55,
"character": 6,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L43"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L55"
}
],
"type": {
@@ -703,9 +722,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 40,
"line": 52,
"character": 7,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L40"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L52"
}
]
}
@@ -735,7 +754,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 19,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L19"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L19"
}
],
"type": {
@@ -964,7 +983,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 17,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L17"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L17"
}
],
"type": {
@@ -1000,7 +1019,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 18,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L18"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L18"
}
],
"type": {
@@ -1023,7 +1042,7 @@
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L16"
}
],
"type": {
@@ -1040,9 +1059,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"line": 204,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L204"
}
],
"signatures": [
@@ -1068,15 +1087,424 @@
"text": "```ts\nconst { data, error } = await functions.invoke('hello-world', {\n body: { name: 'Ada' },\n})\n```"
}
]
},
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@remarks",
"content": [
{
"kind": "text",
"text": "- Requires an Authorization header.\n- Invoke params generally match the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) spec.\n- When you pass in a body to your function, we automatically attach the Content-Type header for "
},
{
"kind": "code",
"text": "`Blob`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`ArrayBuffer`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`File`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`FormData`"
},
{
"kind": "text",
"text": " and "
},
{
"kind": "code",
"text": "`String`"
},
{
"kind": "text",
"text": ". If it doesn't match any of these types we assume the payload is "
},
{
"kind": "code",
"text": "`json`"
},
{
"kind": "text",
"text": ", serialize it and attach the "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header as "
},
{
"kind": "code",
"text": "`application/json`"
},
{
"kind": "text",
"text": ". You can override this behavior by passing in a "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header of your own.\n- Responses are automatically parsed as "
},
{
"kind": "code",
"text": "`json`"
},
{
"kind": "text",
"text": ", "
},
{
"kind": "code",
"text": "`blob`"
},
{
"kind": "text",
"text": " and "
},
{
"kind": "code",
"text": "`form-data`"
},
{
"kind": "text",
"text": " depending on the "
},
{
"kind": "code",
"text": "`Content-Type`"
},
{
"kind": "text",
"text": " header sent by your function. Responses are parsed as "
},
{
"kind": "code",
"text": "`text`"
},
{
"kind": "text",
"text": " by default."
}
]
},
{
"tag": "@example",
"name": "Basic invocation",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n body: { foo: 'bar' }\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Error handling\nA "
},
{
"kind": "code",
"text": "`FunctionsHttpError`"
},
{
"kind": "text",
"text": " error is returned if your function throws an error, "
},
{
"kind": "code",
"text": "`FunctionsRelayError`"
},
{
"kind": "text",
"text": " if the Supabase Relay has an error processing your function and "
},
{
"kind": "code",
"text": "`FunctionsFetchError`"
},
{
"kind": "text",
"text": " if there is a network error in calling your function."
}
]
},
{
"tag": "@example",
"name": "Error handling",
"content": [
{
"kind": "code",
"text": "```js\nimport { FunctionsHttpError, FunctionsRelayError, FunctionsFetchError } from \"@supabase/supabase-js\";\n\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' }\n})\n\nif (error instanceof FunctionsHttpError) {\n const errorMessage = await error.context.json()\n console.log('Function returned an error', errorMessage)\n} else if (error instanceof FunctionsRelayError) {\n console.log('Relay error:', error.message)\n} else if (error instanceof FunctionsFetchError) {\n console.log('Fetch error:', error.message)\n}\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Passing custom headers\nYou can pass custom headers to your function. Note: supabase-js automatically passes the "
},
{
"kind": "code",
"text": "`Authorization`"
},
{
"kind": "text",
"text": " header with the signed in user's JWT."
}
]
},
{
"tag": "@example",
"name": "Passing custom headers",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' }\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Calling with DELETE HTTP verb\nYou can also set the HTTP verb to "
},
{
"kind": "code",
"text": "`DELETE`"
},
{
"kind": "text",
"text": " when calling your Edge Function."
}
]
},
{
"tag": "@example",
"name": "Calling with DELETE HTTP verb",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n body: { foo: 'bar' },\n method: 'DELETE'\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Invoking a Function in the UsEast1 region\nHere are the available regions:\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.Any`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApNortheast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApNortheast2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSouth1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSoutheast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.ApSoutheast2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.CaCentral1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuCentral1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest2`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.EuWest3`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.SaEast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsEast1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsWest1`"
},
{
"kind": "text",
"text": "\n- "
},
{
"kind": "code",
"text": "`FunctionRegion.UsWest2`"
}
]
},
{
"tag": "@example",
"name": "Invoking a Function in the UsEast1 region",
"content": [
{
"kind": "code",
"text": "```js\nimport { createClient, FunctionRegion } from '@supabase/supabase-js'\n\nconst { data, error } = await supabase.functions.invoke('hello', {\n body: { foo: 'bar' },\n region: FunctionRegion.UsEast1\n})\n```"
}
]
},
{
"tag": "@exampleDescription",
"content": [
{
"kind": "text",
"text": "Calling with GET HTTP verb\nYou can also set the HTTP verb to "
},
{
"kind": "code",
"text": "`GET`"
},
{
"kind": "text",
"text": " when calling your Edge Function."
}
]
},
{
"tag": "@example",
"name": "Calling with GET HTTP verb",
"content": [
{
"kind": "code",
"text": "```js\nconst { data, error } = await supabase.functions.invoke('hello', {\n headers: {\n \"my-custom-header\": 'my-custom-header-value'\n },\n method: 'GET'\n})\n```"
}
]
},
{
"tag": "@example",
"name": "Example 7",
"content": [
{
"kind": "code",
"text": "```ts\nconst { data, error } = await functions.invoke('hello-world', {\n body: { name: 'Ada' },\n})\n```"
}
]
}
]
},
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 75,
"line": 204,
"character": 8,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L75"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L204"
}
],
"typeParameters": [
@@ -1173,9 +1601,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"line": 75,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"signatures": [
@@ -1193,8 +1621,18 @@
}
],
"blockTags": [
{
"tag": "@category",
"content": [
{
"kind": "text",
"text": "Functions"
}
]
},
{
"tag": "@example",
"name": "Setting the authorization header",
"content": [
{
"kind": "code",
@@ -1207,9 +1645,9 @@
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 60,
"line": 75,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L60"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L75"
}
],
"parameters": [
@@ -1255,12 +1693,22 @@
"children": [31, 28]
}
],
"categories": [
{
"title": "Functions",
"children": [2, 31, 28]
},
{
"title": "Other",
"children": [20, 18, 19, 17]
}
],
"sources": [
{
"fileName": "packages/core/functions-js/src/FunctionsClient.ts",
"line": 15,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/FunctionsClient.ts#L15"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/FunctionsClient.ts#L15"
}
]
},
@@ -1301,7 +1749,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L32"
}
],
"signatures": [
@@ -1316,7 +1764,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 32,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L32"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L32"
}
],
"parameters": [
@@ -1387,7 +1835,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1411,7 +1859,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 30,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L30"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L30"
}
],
"extendedTypes": [
@@ -1480,7 +1928,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L50"
}
],
"signatures": [
@@ -1495,7 +1943,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 50,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L50"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L50"
}
],
"parameters": [
@@ -1543,7 +1991,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1572,7 +2020,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 49,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L49"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L49"
}
],
"extendedTypes": [
@@ -1621,7 +2069,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L82"
}
],
"signatures": [
@@ -1636,7 +2084,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 82,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L82"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L82"
}
],
"parameters": [
@@ -1684,7 +2132,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1713,7 +2161,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 81,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L81"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L81"
}
],
"extendedTypes": [
@@ -1762,7 +2210,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L66"
}
],
"signatures": [
@@ -1777,7 +2225,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 66,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L66"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L66"
}
],
"parameters": [
@@ -1825,7 +2273,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 31,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L31"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L31"
}
],
"type": {
@@ -1854,7 +2302,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 65,
"character": 13,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L65"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L65"
}
],
"extendedTypes": [
@@ -1877,7 +2325,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L105"
}
],
"type": {
@@ -1910,7 +2358,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 121,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L121"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L121"
}
],
"type": {
@@ -2019,7 +2467,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"type": {
@@ -2035,7 +2483,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"indexSignatures": [
@@ -2050,7 +2498,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 109,
"character": 14,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L109"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L109"
}
],
"parameters": [
@@ -2096,7 +2544,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 113,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L113"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L113"
}
],
"type": {
@@ -2146,7 +2594,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 117,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L117"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L117"
}
],
"type": {
@@ -2177,7 +2625,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 132,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L132"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L132"
}
],
"type": {
@@ -2211,7 +2659,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 137,
"character": 2,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L137"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L137"
}
],
"type": {
@@ -2231,7 +2679,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 105,
"character": 36,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L105"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L105"
}
]
}
@@ -2248,7 +2696,7 @@
"fileName": "packages/core/functions-js/src/types.ts",
"line": 16,
"character": 12,
"url": "https://github.com/supabase/supabase-js/blob/b835e3c870f3767980776cc25fd16df72fa416be/packages/core/functions-js/src/types.ts#L16"
"url": "https://github.com/supabase/supabase-js/blob/c6f8e76042dc0460422551fa288eb98dceaa66b0/packages/core/functions-js/src/types.ts#L16"
}
],
"typeParameters": [
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+96
View File
@@ -1424,6 +1424,38 @@
"summary": "Retrieve object info",
"tags": ["object"],
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
@@ -1462,6 +1494,38 @@
"tags": ["object"],
"description": "Object Info",
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
@@ -1648,6 +1712,38 @@
"tags": ["object"],
"description": "returns object info",
"parameters": [
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": { "type": "integer", "minimum": 0 },
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": { "type": "string", "enum": ["cover", "contain", "fill"] },
"in": "query",
"name": "resize",
"required": false
},
{
"schema": { "type": "string", "enum": ["origin", "avif"] },
"in": "query",
"name": "format",
"required": false
},
{
"schema": { "type": "integer", "minimum": 20, "maximum": 100 },
"in": "query",
"name": "quality",
"required": false
},
{
"schema": { "type": "string" },
"example": "avatars",
File diff suppressed because it is too large. Load diff
@@ -16518,6 +16518,78 @@
"x-endpoint-owners": ["security"]
}
},
"/v1/projects/{ref}/database/openapi": {
"get": {
"description": "Returns the PostgREST OpenAPI specification for the project. This is the replacement for querying `/rest/v1/` directly with the anon key.",
"operationId": "v1-get-database-openapi",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"type": "string"
}
},
{
"name": "schema",
"required": false,
"in": "query",
"description": "The database schema to generate the OpenAPI spec for",
"schema": {
"default": "public",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden action"
},
"429": {
"description": "Rate limit exceeded"
},
"500": {
"description": "Failed to fetch PostgREST OpenAPI spec"
}
},
"security": [
{
"bearer": []
},
{
"fga_permissions": ["database_read"]
}
],
"summary": "Get PostgREST OpenAPI spec",
"tags": ["Database"],
"x-badges": [
{
"name": "OAuth scope: database:read",
"position": "after"
}
],
"x-endpoint-owners": ["management-api"],
"x-oauth-scope": "database:read"
}
},
"/v1/projects/{ref}/functions": {
"get": {
"description": "Returns all functions you've previously added to the specified project.",
@@ -19249,6 +19321,9 @@
"security": [
{
"bearer": []
},
{
"fga_permissions": ["realtime_config_read"]
}
],
"summary": "Gets realtime configuration",
@@ -19361,6 +19436,9 @@
"security": [
{
"bearer": []
},
{
"fga_permissions": ["realtime_config_write"]
}
],
"summary": "Updates realtime configuration",
@@ -19405,6 +19483,9 @@
"security": [
{
"bearer": []
},
{
"fga_permissions": ["realtime_config_write"]
}
],
"summary": "Shutdowns realtime connections for a project",
@@ -20653,9 +20734,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
}
}
}
@@ -20729,9 +20815,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
}
}
}
@@ -29558,9 +29649,14 @@
"status": {
"type": "string",
"enum": ["AVAILABLE", "PENDING", "REMOVED", "FAILED"]
},
"completed_on": {
"type": "string",
"format": "date-time",
"nullable": true
}
},
"required": ["name", "status"]
"required": ["name", "status", "completed_on"]
},
"V1UndoBody": {
"type": "object",
@@ -2351,6 +2351,54 @@
"summary": "Retrieve object info",
"tags": ["object"],
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
@@ -2412,6 +2460,54 @@
"tags": ["object"],
"description": "Object Info",
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
@@ -2766,6 +2862,54 @@
"tags": ["object"],
"description": "returns object info",
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "height",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 0
},
"example": 100,
"in": "query",
"name": "width",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["cover", "contain", "fill"]
},
"in": "query",
"name": "resize",
"required": false
},
{
"schema": {
"type": "string",
"enum": ["origin", "avif"]
},
"in": "query",
"name": "format",
"required": false
},
{
"schema": {
"type": "integer",
"minimum": 20,
"maximum": 100
},
"in": "query",
"name": "quality",
"required": false
},
{
"schema": {
"type": "string"
+7 -29
View File
@@ -1,40 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "tsconfig/nextjs.json",
"compilerOptions": {
"incremental": true,
"noImplicitAny": false,
"allowJs": true,
"lib": ["dom", "dom.iterable", "esnext"],
"paths": {
// @deprecated: use @/ instead of ~/
"~/*": ["./*"],
"@/*": ["./*"],
"@ui/*": ["./../../packages/ui/src/*"]
"@/*": ["./*"]
},
"target": "ES2021",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"module": "esnext",
"plugins": [
{
"name": "next"
}
],
"plugins": [{ "name": "next" }],
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"pages/guides/append-test.js",
".next/types/**/*.ts",
"./../../packages/ui/src/**/*.d.ts"
],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "examples"]
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.scss' {
export const styles: Record<string, string>
export default styles
}
+2 -1
View File
@@ -1,6 +1,6 @@
import path from 'path'
import { getHighlighter, loadTheme } from '@shikijs/compat'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files'
import path from 'path'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
@@ -157,6 +157,7 @@ export const Doc = defineDocumentType(() => ({
}))
export default makeSource({
disableImportAliasWarning: true,
contentDirPath: './content',
documentTypes: [Doc],
mdx: {
+2 -2
View File
@@ -7,13 +7,13 @@
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3007",
"build": "pnpm run content:build && next build --turbopack",
"internal:sync": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/sync-internal-content.mts",
"internal:sync": "tsx ./scripts/sync-internal-content.mts",
"start": "next start",
"lint": "eslint .",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo tsconfig.tsbuildinfo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
"typecheck": "contentlayer2 build && tsc --noEmit"
},
"dependencies": {
"common": "workspace:*",
-20
View File
@@ -1,20 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules"]
}
+7 -23
View File
@@ -1,40 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "tsconfig/base.json",
"extends": "tsconfig/nextjs.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
// @deprecated: Don't import directly without suffix. Use @/ always.
"*": ["./*"],
"@/*": ["./*"],
"@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths
"contentlayer/generated": ["./.contentlayer/generated"]
},
"plugins": [
{
"name": "next"
}
]
"plugins": [{ "name": "next" }],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".contentlayer/generated",
"./../../packages/ui/src/**/*.d.ts"
".contentlayer/generated"
],
"exclude": ["node_modules", "./scripts/build-registry.mts"]
}
-13
View File
@@ -1,13 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"isolatedModules": false
},
"include": [".contentlayer/generated", "scripts/**/*.ts"],
"exclude": ["node_modules"]
}
+6 -16
View File
@@ -3,9 +3,9 @@
"react-hooks/exhaustive-deps": 204,
"import/no-anonymous-default-export": 57,
"@tanstack/query/exhaustive-deps": 12,
"@typescript-eslint/no-explicit-any": 1204,
"@typescript-eslint/no-explicit-any": 1190,
"no-restricted-imports": 59,
"no-restricted-exports": 278,
"no-restricted-exports": 273,
"react/no-unstable-nested-components": 66
},
"ruleFiles": {
@@ -319,6 +319,7 @@
"components/interfaces/Docs/ResourceContent.tsx": 1,
"components/interfaces/Docs/RpcContent.tsx": 4,
"components/interfaces/Docs/Snippets.ts": 1,
"components/interfaces/ErrorHandling/error-mappings.tsx": 1,
"components/interfaces/Functions/CommandRender.tsx": 4,
"components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx": 1,
"components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.utils.tsx": 3,
@@ -416,7 +417,6 @@
"components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx": 1,
"components/interfaces/SQLEditor/hooks.ts": 1,
"components/interfaces/SQLEditor/useAddDefinitions.ts": 1,
"components/interfaces/Settings/Addons/Addons.tsx": 1,
"components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx": 1,
"components/interfaces/Settings/Addons/PITRSidePanel.tsx": 1,
"components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword.tsx": 2,
@@ -476,7 +476,6 @@
"components/interfaces/TableGridEditor/SidePanelEditor/ActionBar.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx": 6,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx": 1,
@@ -491,7 +490,7 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SchemaEditor.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx": 14,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types.ts": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 15,
"components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx": 14,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx": 4,
@@ -499,10 +498,7 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.tsx": 18,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 3,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts": 1,
"components/interfaces/TableGridEditor/TableDefinition.tsx": 3,
"components/interfaces/TableGridEditor/TableEntity.utils.ts": 1,
@@ -536,7 +532,7 @@
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SearchList.tsx": 1,
"components/layouts/TableEditorLayout/EntityListItem.tsx": 1,
"components/layouts/TableEditorLayout/TableEditorMenu.tsx": 2,
"components/layouts/TableEditorLayout/TableEditorMenu.tsx": 1,
"components/layouts/Tabs/NewTab.tsx": 1,
"components/to-be-cleaned/KeyMap.tsx": 3,
"components/ui/AIAssistantPanel/AIAssistant.utils.ts": 1,
@@ -571,7 +567,7 @@
"components/ui/DatePicker/index.tsx": 2,
"components/ui/DateRangePicker.tsx": 1,
"components/ui/DebouncedComponent.tsx": 1,
"components/ui/DownloadResultsButton.tsx": 2,
"components/ui/DownloadResultsButton.tsx": 1,
"components/ui/Error.tsx": 1,
"components/ui/ErrorBoundary/ErrorBoundary.tsx": 2,
"components/ui/ErrorBoundary/GlobalErrorBoundaryState.tsx": 1,
@@ -696,7 +692,6 @@
"pages/api/ai/feedback/rate.ts": 2,
"pages/api/check-cname.ts": 1,
"pages/api/edge-functions/test.ts": 1,
"pages/api/integrations/stripe-sync.ts": 1,
"pages/api/platform/auth/[ref]/users/[id]/factors.ts": 1,
"pages/api/platform/database/[ref]/pooling.ts": 1,
"pages/api/platform/integrations/github/authorization.ts": 1,
@@ -968,8 +963,6 @@
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetImportConfiguration.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 1,
"components/interfaces/TableGridEditor/ViewEntityAutofixSecurityModal.tsx": 1,
"components/layouts/APIAuthorizationLayout.tsx": 1,
"components/layouts/APIKeys/APIKeysLayout.tsx": 1,
@@ -987,7 +980,6 @@
"components/layouts/JWTKeys/JWTKeysLayout.tsx": 1,
"components/layouts/LinkAwsMarketplaceLayout.tsx": 1,
"components/layouts/LogsLayout/LogsLayout.tsx": 1,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileViewNav.tsx": 1,
"components/layouts/Navigation/NavigationBar/NavigationIconLink.tsx": 1,
@@ -999,11 +991,9 @@
"components/layouts/ProjectLayout/ConnectingState.tsx": 1,
"components/layouts/ProjectLayout/OrganizationSettingsLayout.tsx": 1,
"components/layouts/ProjectLayout/RestartingState.tsx": 1,
"components/layouts/ProjectLayout/RestoringState.tsx": 1,
"components/layouts/ProjectSettingsLayout/SettingsLayout.tsx": 1,
"components/layouts/RealtimeLayout/RealtimeLayout.tsx": 1,
"components/layouts/RedeemCreditsLayout.tsx": 1,
"components/layouts/ReportsLayout/ReportsLayout.tsx": 1,
"components/layouts/ReportsLayout/ReportsMenu.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorLayout.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorLoadingSnippets.tsx": 1,
+8 -43
View File
@@ -1,4 +1,4 @@
import { keepPreviousData, useQueryClient } from '@tanstack/react-query'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
@@ -15,10 +15,7 @@ import { useTableEditorStateSnapshot } from 'state/table-editor'
import { QueuedOperation } from 'state/table-editor-operation-queue.types'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
useIsQueueOperationsEnabled,
useIsTableFilterBarEnabled,
} from '../interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsTableFilterBarEnabled } from '../interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Shortcuts } from './components/common/Shortcuts'
import { Footer } from './components/footer/Footer'
import { Grid } from './components/grid/Grid'
@@ -29,7 +26,7 @@ import { useTableFilter } from './hooks/useTableFilter'
import { useTableSort } from './hooks/useTableSort'
import { validateMsSqlSorting } from './MsSqlValidation'
import { GridProps } from './types'
import { reapplyOptimisticUpdates } from './utils/queueOperationUtils'
import { formatGridDataWithOperationValues } from './utils/queueOperationUtils'
export const SupabaseGrid = ({
customHeader,
@@ -43,9 +40,6 @@ export const SupabaseGrid = ({
const { id: _id } = useParams()
const tableId = _id ? Number(_id) : undefined
const isQueueOperationsEnabled = useIsQueueOperationsEnabled()
const queryClient = useQueryClient()
const { data: project } = useSelectedProjectQuery()
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
@@ -73,7 +67,6 @@ export const SupabaseGrid = ({
isError,
isPending: isLoading,
isRefetching,
dataUpdatedAt,
} = useTableRowsQuery(
{
projectRef: project?.ref,
@@ -100,39 +93,11 @@ export const SupabaseGrid = ({
if (!mounted) setMounted(true)
}, [])
// Re-apply optimistic updates when table data is loaded/refetched
// This ensures pending changes remain visible when switching tabs or after data refresh
// Skip re-applying during save to avoid race condition where refetch completes before queue clears
const isSaving = tableEditorSnap.operationQueue.status === 'saving'
useEffect(() => {
if (
isSuccess &&
project?.ref &&
tableId &&
isQueueOperationsEnabled &&
tableEditorSnap.hasPendingOperations &&
!isSaving
) {
reapplyOptimisticUpdates({
queryClient,
projectRef: project.ref,
tableId,
operations: tableEditorSnap.operationQueue.operations as readonly QueuedOperation[],
})
}
}, [
isSuccess,
dataUpdatedAt,
project?.ref,
tableId,
isQueueOperationsEnabled,
tableEditorSnap.hasPendingOperations,
tableEditorSnap.operationQueue.operations,
queryClient,
isSaving,
])
const rows = data?.rows ?? EMPTY_ARR
const operations = (tableEditorSnap.operationQueue.operations as QueuedOperation[]).filter(
(op) => op.tableId === tableId
)
const baseRows = data?.rows ?? EMPTY_ARR
const rows = formatGridDataWithOperationValues({ operations, rows: baseRows })
const HeaderComponent = newFilterBarEnabled ? HeaderNew : Header
Loaded 100 of 543 files, more files were not shown because too many files have changed in this diff. Show more