mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
Merge branch 'master' into tomas/com-269-edge-functions-examples
This commit is contained in:
449 files changed
+114161
-32557
No files matched your search
@@ -50,11 +50,12 @@ jobs:
|
||||
echo "Regenerating tsdoc files for JS client libraries..."
|
||||
echo "Source: ${SOURCE}"
|
||||
echo "Version: ${VERSION}"
|
||||
make
|
||||
make download.tsdoc.v2
|
||||
|
||||
- name: Refresh reference-content snapshot
|
||||
working-directory: apps/docs
|
||||
run: npx vitest run --update scripts/build-reference-content.test.ts
|
||||
# Using --dir (not a file path) so Vitest 4 doesn't run unrelated tests.
|
||||
run: npx vitest run --update --dir scripts
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
@@ -65,7 +66,6 @@ jobs:
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
title: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
|
||||
body: |
|
||||
Updates JS sdk documentation following stable release.
|
||||
Ran `make` in apps/docs/spec to regenerate tsdoc files.
|
||||
Ran `make download.tsdoc.v2` in apps/docs/spec and refreshed the reference-content snapshot.
|
||||
|
||||
**Details:**
|
||||
- **Version:** `${{ github.event.inputs.version }}`
|
||||
|
||||
@@ -17,19 +17,74 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config: [default, logs, envoy, rustfs, envoy-rustfs]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
docker/
|
||||
- name: Run docker-compose up
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Generate keys
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
cp .env.example .env
|
||||
sh utils/generate-keys.sh --update-env
|
||||
sh utils/add-new-auth-keys.sh --update-env
|
||||
|
||||
- name: Start self-hosted Supabase
|
||||
shell: bash
|
||||
# Ensure all services can be started and healthy with default config
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
cp .env.example .env
|
||||
yq -i '.services.supavisor.environment.RLIMIT_NOFILE=1024' docker-compose.yml
|
||||
docker compose up --quiet-pull --wait --wait-timeout 180
|
||||
if [ "${{ matrix.config }}" = "logs" ]; then
|
||||
docker compose -f docker-compose.yml -f docker-compose.logs.yml up --quiet-pull --wait --wait-timeout 180
|
||||
elif [ "${{ matrix.config }}" = "envoy" ]; then
|
||||
docker compose -f docker-compose.yml -f docker-compose.envoy.yml up --quiet-pull --wait --wait-timeout 180
|
||||
elif [ "${{ matrix.config }}" = "rustfs" ]; then
|
||||
docker compose -f docker-compose.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180
|
||||
elif [ "${{ matrix.config }}" = "envoy-rustfs" ]; then
|
||||
docker compose -f docker-compose.yml -f docker-compose.envoy.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180
|
||||
else
|
||||
docker compose up --quiet-pull --wait --wait-timeout 180
|
||||
fi
|
||||
|
||||
- name: Run container log tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
sh tests/test-container-logs.sh
|
||||
|
||||
- name: Run smoke tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
sh tests/test-self-hosted.sh
|
||||
|
||||
- name: Run API keys and asymmetric auth tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
sh tests/test-auth-keys.sh
|
||||
|
||||
- name: Run S3 protocol tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd docker
|
||||
sh tests/test-s3.sh
|
||||
@@ -24,7 +24,6 @@ jobs:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
@@ -87,7 +86,7 @@ jobs:
|
||||
- name: Pre-start diagnostics
|
||||
run: |
|
||||
docker ps -a
|
||||
sudo ss -tlnp | grep 5432 || echo "5432 free"
|
||||
sudo ss -tlnp | grep 54322 || echo "54322 free"
|
||||
|
||||
- name: Start supabase
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
@@ -97,7 +96,7 @@ jobs:
|
||||
if: failure()
|
||||
run: |
|
||||
docker ps -a
|
||||
sudo ss -tlnp | grep 5432
|
||||
sudo ss -tlnp | grep 54322 || echo "54322 not listening"
|
||||
docker logs $(docker ps -aq) 2>&1 || true
|
||||
|
||||
- name: Build studio
|
||||
|
||||
@@ -31,3 +31,5 @@ apps/**/.contentlayer
|
||||
packages/ui/src/components/Form/examples/PhoneProvidersSchema.json
|
||||
# ignore because of <br/><br/>
|
||||
apps/www/_blog/2025-07-14-supabase-ui-platform-kit.mdx
|
||||
# vitest file snapshots — JSON.stringify owns the format, not Prettier
|
||||
apps/docs/scripts/__snapshots__/
|
||||
@@ -29,8 +29,8 @@ yarn-error.log*
|
||||
public/sitemap.xml
|
||||
# Per-source llms files (generated by build:llms, served by www)
|
||||
public/llms/
|
||||
# Generated guide markdown files
|
||||
public/docs/
|
||||
# Generated guide and reference markdown files
|
||||
public/markdown/
|
||||
public/docs.tar.gz
|
||||
|
||||
# Copied examples folder
|
||||
|
||||
@@ -31,7 +31,7 @@ To test locally, within the `apps/docs` directory:
|
||||
1. Run `pnpm build:guides-markdown`
|
||||
2. Run `pnpm dev`
|
||||
|
||||
This creates Markdown files for all routes under the `public/docs/guides` directory, ignored by Git.
|
||||
This creates Markdown files for all routes under the `public/markdown/guides` directory, ignored by Git.
|
||||
|
||||
For production this setup runs as a `prebuild` task to allow Vercel to bundle these files with middleware and functions.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params
|
||||
const baseDir = path.join(process.cwd(), 'public/docs/guides')
|
||||
const baseDir = path.join(process.cwd(), 'public/markdown/guides')
|
||||
const filePath = path.join(baseDir, `${slug.join('/')}.md`)
|
||||
|
||||
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) {
|
||||
|
||||
@@ -354,7 +354,7 @@ If your image has alternate light and dark versions, or you want to make it zoom
|
||||
|
||||
### Project Variables
|
||||
|
||||
Some guides and tutorials will require that users copy their Supabase project URL and anon key. You can provide those inline if the user is signed in:
|
||||
Some guides and tutorials will require that users copy their Supabase project URL and publishable or secret key. You can provide those inline if the user is signed in:
|
||||
|
||||
```mdx
|
||||
<ProjectConfigVariables variable="url" />
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
wrapInMarkdownCodeBlock,
|
||||
} from '~/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { source } from 'common-tags'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str
|
||||
|
||||
const prompt = await getAiPrompt(slug)
|
||||
if (!prompt) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/ai-tools/ai-prompts/${slug}`)
|
||||
}
|
||||
|
||||
let { heading, content } = prompt
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -76,7 +75,7 @@ const getContent = async ({ slug }: Params) => {
|
||||
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/ai/python/${slug}`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
genGuidesStaticParams,
|
||||
removeRedundantH1,
|
||||
} from '~/features/docs/GuidesMdx.utils'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { newEditLink } from '~/features/helpers.edit-link'
|
||||
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide'
|
||||
// End of third-party imports
|
||||
@@ -20,7 +21,6 @@ import type { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import matter from 'gray-matter'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import emoji from 'remark-emoji'
|
||||
import { Button } from 'ui'
|
||||
@@ -334,11 +334,14 @@ interface Params {
|
||||
}
|
||||
|
||||
const WrappersDocs = async (props: { params: Promise<Params> }) => {
|
||||
const params = await props.params
|
||||
|
||||
if (!isFeatureEnabled('docs:fdw')) {
|
||||
notFound()
|
||||
notFoundWithPathname(
|
||||
`/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}`
|
||||
)
|
||||
}
|
||||
|
||||
const params = await props.params
|
||||
const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params)
|
||||
|
||||
// Create a combined URL transformer that handles both regular URLs and asset URLs
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { relative } from 'node:path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -75,7 +74,7 @@ const getContent = async ({ slug }: Params) => {
|
||||
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/deployment/ci/${slug}`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { getEmptyArray } from '~/features/helpers.fn'
|
||||
import { IS_DEV } from '~/lib/constants'
|
||||
import { isValidGuideFrontmatter } from '~/lib/docs'
|
||||
@@ -10,7 +11,6 @@ import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import matter from 'gray-matter'
|
||||
import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import {
|
||||
@@ -106,7 +106,7 @@ const getContent = async ({ slug }: Params) => {
|
||||
const page = pageMap.find((page) => page.slug === requestedSlug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}`)
|
||||
}
|
||||
|
||||
const { meta, remoteFile, useRoot } = page
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isAbsolute, relative } from 'path'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { getEmptyArray } from '~/features/helpers.fn'
|
||||
import { IS_DEV } from '~/lib/constants'
|
||||
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
@@ -9,7 +10,6 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
@@ -128,7 +128,7 @@ const getContent = async ({ slug }: Params) => {
|
||||
const page = pageMap.find((page) => page.slug === slug?.at(0))
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}`)
|
||||
}
|
||||
|
||||
const { remoteFile, meta } = page
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import TroubleshootingPage from '~/features/docs/Troubleshooting.page'
|
||||
import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils'
|
||||
import { PROD_URL } from '~/lib/constants'
|
||||
@@ -20,7 +19,7 @@ export default async function TroubleshootingEntryPage(props: {
|
||||
const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug)
|
||||
|
||||
if (!entry) {
|
||||
notFound()
|
||||
notFoundWithPathname(`/guides/troubleshooting/${slug}`)
|
||||
}
|
||||
|
||||
return <TroubleshootingPage entry={entry} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { REFERENCES } from '~/content/navigation.references'
|
||||
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
|
||||
import { ApiReferencePage } from '~/features/docs/Reference.apiPage'
|
||||
import { CliReferencePage } from '~/features/docs/Reference.cliPage'
|
||||
import { ClientSdkReferencePage } from '~/features/docs/Reference.sdkPage'
|
||||
@@ -18,9 +17,10 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
|
||||
const params = await props.params
|
||||
|
||||
const { slug } = params
|
||||
const referencePath = `/reference/${slug.join('/')}`
|
||||
|
||||
if (!Object.keys(REFERENCES).includes(slug[0].replaceAll('-', '_'))) {
|
||||
notFound()
|
||||
notFoundWithPathname(referencePath)
|
||||
}
|
||||
|
||||
const parsedPath = parseReferencePath(slug)
|
||||
@@ -34,7 +34,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
|
||||
|
||||
const sdkData = REFERENCES[sdkId]
|
||||
if (sdkData.enabled === false) {
|
||||
notFound()
|
||||
notFoundWithPathname(referencePath)
|
||||
}
|
||||
|
||||
const latestVersion = sdkData.versions[0]
|
||||
@@ -52,7 +52,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
|
||||
<SelfHostingReferencePage service={parsedPath.service} servicePath={parsedPath.servicePath} />
|
||||
)
|
||||
} else {
|
||||
notFound()
|
||||
notFoundWithPathname(referencePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1575,10 +1575,6 @@ export const api: NavMenuConstant = {
|
||||
{ name: 'Generating TypeScript Types', url: '/guides/api/rest/generating-types' },
|
||||
{ name: 'Generating Python Types', url: '/guides/api/rest/generating-python-types' },
|
||||
{ name: 'Error Codes', url: '/guides/api/rest/postgrest-error-codes' },
|
||||
{
|
||||
name: 'Handling Errors in supabase-js',
|
||||
url: '/guides/api/handling-errors-in-supabase-js',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1619,6 +1615,16 @@ export const api: NavMenuConstant = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Debugging',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Handling Errors in supabase-js',
|
||||
url: '/guides/api/handling-errors-in-supabase-js',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1738,6 +1744,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Status codes',
|
||||
url: '/guides/functions/status-codes' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Error codes',
|
||||
url: '/guides/functions/error-codes' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Recursive/Nested function calls',
|
||||
url: '/guides/functions/recursive-functions' as `/${string}`,
|
||||
@@ -2183,6 +2193,11 @@ export const storage: NavMenuConstant = {
|
||||
name: 'Querying Vectors',
|
||||
url: '/guides/storage/vector/querying-vectors' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Local Development',
|
||||
url: '/guides/storage/vector/local-development' as `/${string}`,
|
||||
enabled: billingEnabled,
|
||||
},
|
||||
{
|
||||
name: 'Limits',
|
||||
url: '/guides/storage/vector/limits' as `/${string}`,
|
||||
|
||||
@@ -52,7 +52,7 @@ The Supabase MCP server provides tools organized into feature groups. All groups
|
||||
### Development
|
||||
|
||||
- `get_project_url` - Get the API URL for a project
|
||||
- `get_publishable_keys` - Get anon/public keys
|
||||
- `get_publishable_keys` - Get publishable and legacy anon API keys for a project
|
||||
- `generate_typescript_types` - Generate TypeScript types from schema
|
||||
|
||||
### Edge Functions
|
||||
|
||||
@@ -84,7 +84,7 @@ const onSubmit = (e: Event) => {
|
||||
|
||||
const query = new URLSearchParams({ query: inputRef.current!.value })
|
||||
const projectUrl = `https://your-project-ref.supabase.co/functions/v1`
|
||||
const queryURL = `${projectURL}/${query}`
|
||||
const queryURL = `${projectUrl}/${query}`
|
||||
const eventSource = new EventSource(queryURL)
|
||||
|
||||
eventSource.addEventListener("error", (err) => {
|
||||
|
||||
@@ -87,7 +87,7 @@ Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linki
|
||||
|
||||
```dart
|
||||
Future<void> signInWithEmail() async {
|
||||
final AuthResponse res = await supabase.auth.signinwithotp(email: 'valid.email@supabase.io');
|
||||
final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -148,6 +148,12 @@ Let us develop a Hook locally and then deploy it to the cloud. As a recap, here
|
||||
|
||||
Edit `config.toml` to set up the Auth Hook locally.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The hook name used in the `config.toml` must correspond to one of the available Hooks listed above. For example, the Send SMS hook would be configured as: `[auth.hook.send_sms]`
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
|
||||
@@ -142,7 +142,7 @@ as $$
|
||||
(
|
||||
user_id,
|
||||
factor_id,
|
||||
last_refreshed_at
|
||||
last_failed_at
|
||||
)
|
||||
values
|
||||
(
|
||||
@@ -151,7 +151,7 @@ as $$
|
||||
now()
|
||||
)
|
||||
on conflict do update
|
||||
set last_refreshed_at = now();
|
||||
set last_failed_at = now();
|
||||
|
||||
-- finally let Supabase Auth do the default behavior for a failed attempt
|
||||
return jsonb_build_object('decision', 'continue');
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ import Supabase
|
||||
|
||||
let supabase = SupabaseClient(
|
||||
supabaseURL: URL(string: "https://project-ref.supabase.io")!,
|
||||
supabaseKey: "supabase.anon.key",
|
||||
supabaseKey: "supabase.publishable.key",
|
||||
options: SupabaseClientOptions(
|
||||
auth: SupabaseClientOptions.AuthOptions(
|
||||
accessToken: {
|
||||
|
||||
@@ -338,7 +338,7 @@ const { initializeApp } = require('firebase-admin/app');
|
||||
const { getAuth } = require('firebase-admin/auth');
|
||||
initializeApp();
|
||||
|
||||
async function setRoleCustomClaim() => {
|
||||
async function setRoleCustomClaim() {
|
||||
let nextPageToken = undefined
|
||||
|
||||
do {
|
||||
|
||||
@@ -6,7 +6,7 @@ subtitle: 'Developing locally using the Supabase CLI.'
|
||||
sidebar_label: 'Overview'
|
||||
---
|
||||
|
||||
You can use the Supabase CLI to run the entire Supabase stack locally on your machine, by running `supabase init` and then `supabase start`. To install the CLI, see the [installation guide](/docs/guides/cli/getting-started#installing-the-supabase-cli).
|
||||
You can use the Supabase CLI to run the entire Supabase stack locally on your machine, by running `supabase init` and then `supabase start`. To install the CLI, read [the installation guide](/docs/guides/cli/getting-started#installing-the-supabase-cli).
|
||||
|
||||
The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ For more details on using these monitoring charts, see the [Reports guide](/docs
|
||||
|
||||
#### Grafana Dashboard
|
||||
|
||||
Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/platform/metrics).
|
||||
Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/telemetry/metrics).
|
||||
|
||||
Its "Client Connections" graph displays connections for both Supavisor and Postgres
|
||||

|
||||
|
||||
@@ -44,7 +44,7 @@ create table instruments (
|
||||
name text
|
||||
);
|
||||
|
||||
insert into books
|
||||
insert into instruments
|
||||
(id, name)
|
||||
values
|
||||
(1, 'violin'),
|
||||
|
||||
@@ -81,8 +81,8 @@ If you plan on solely using Drizzle instead of the Supabase Data API (PostgREST)
|
||||
import postgres from 'postgres'
|
||||
|
||||
let connectionString = process.env.DATABASE_URL
|
||||
if (host.includes('postgres:postgres@supabase_db_')) {
|
||||
const url = URL.parse(host)!
|
||||
if (connectionString.includes('postgres:postgres@supabase_db_')) {
|
||||
const url = URL.parse(connectionString)!
|
||||
url.hostname = url.hostname.split('_')[1]
|
||||
connectionString = url.href
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ To disable an extension call `drop extension`.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Most extensions are installed under the `extensions` schema, which is accessible to public by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema.
|
||||
Most extensions are installed under the `extensions` schema, which is accessible to `public` by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema.
|
||||
|
||||
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ returning the JSON
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"id": 1
|
||||
"id": 1,
|
||||
"name": "My Blog"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ explain select * from book;
|
||||
(1 row)
|
||||
```
|
||||
|
||||
Now we can choose a `statement_cost_filter` value between the total cost for the single select (2.49) and the whole table select (135.0) so one statement will succeed and one will fail.
|
||||
Now we can choose a `statement_cost_limit` value between the total cost for the single select (2.49) and the whole table select (135.0) so one statement will succeed and one will fail.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
|
||||
@@ -391,5 +391,5 @@ PGAudit's [official documentation](https://www.pgaudit.org) focuses on system an
|
||||
|
||||
- [Official `PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Database Function Logging](/docs/guides/database/functions#general-logging)
|
||||
- [Supabase Logging](/docs/guides/platform/logs)
|
||||
- [Supabase Logging](/docs/guides/telemetry/logs)
|
||||
- [Self-Hosting Logs](/docs/reference/self-hosting-analytics/introduction)
|
||||
@@ -55,8 +55,8 @@ It's good practice to create the extension within a separate schema (like `exten
|
||||
|
||||
## API
|
||||
|
||||
- [`sign(payload json, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containing _payload_ with _secret_ using _algorithm_.
|
||||
- [`verify(token text, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_.
|
||||
- [`sign(payload json, secret text, algorithm text default 'HS256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containing _payload_ with _secret_ using _algorithm_.
|
||||
- [`verify(token text, secret text, algorithm text default 'HS256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_.
|
||||
|
||||
Where:
|
||||
|
||||
|
||||
@@ -262,4 +262,4 @@ Using the query plan analyzer to optimize your queries is a large topic, with a
|
||||
- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN)
|
||||
- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze)
|
||||
|
||||
You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it.
|
||||
You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../telemetry/metrics) to better understand the behavior of your DB and the queries you're executing against it.
|
||||
@@ -134,7 +134,7 @@ supabase migration new create_posts_table
|
||||
user_id text,
|
||||
title text,
|
||||
content text,
|
||||
created_at timestamptz default now()
|
||||
created_at timestamptz default now(),
|
||||
updated_at timestamptz default now()
|
||||
);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ WHERE id IN (
|
||||
|
||||
This approach has the benefit of controlling when it runs, locking for a shorter period of time and minimising impact on other transactions.
|
||||
|
||||
If you know in advance that such large deletes will have to happen in the business cycle of your database, then you should seriously think about using (table parititioning)[/docs/guides/database/partitions] as a management tool.
|
||||
If you know in advance that such large deletes will have to happen in the business cycle of your database, then you should seriously think about using [table partitioning](/docs/guides/database/partitions) as a management tool.
|
||||
|
||||
### Soft deletes
|
||||
|
||||
|
||||
@@ -32,10 +32,9 @@ select distinct
|
||||
points
|
||||
from
|
||||
seasons
|
||||
order BY
|
||||
id,
|
||||
points desc,
|
||||
team;
|
||||
order by
|
||||
team,
|
||||
points desc;
|
||||
```
|
||||
|
||||
The important bits here are:
|
||||
|
||||
@@ -113,7 +113,15 @@ Used by the Auth middleware to connect to the database and run migration. Access
|
||||
|
||||
### `supabase_etl_admin`
|
||||
|
||||
Used by [Replication powered by Supabase ETL](/docs/guides/database/replication) to replicate database changes to external destinations. Has read-all access and replication privileges for change data capture, bypasses Row Level Security, and can write to the `etl` schema.
|
||||
`supabase_etl_admin` is used by [Database replication](/docs/guides/database/replication) through Supabase ETL.
|
||||
|
||||
This role:
|
||||
|
||||
- Replicates database changes to destination systems
|
||||
- Has read-all access
|
||||
- Has replication privileges for change data capture, bypasses Row Level Security
|
||||
- Can create event triggers
|
||||
- Can write to the `etl` schema
|
||||
|
||||
### `dashboard_user`
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: 'replication'
|
||||
title: 'Database replication'
|
||||
description: 'Compare read replicas, external replication, and manual replication.'
|
||||
description: 'Compare read replicas, external replication (ETL), and manual replication.'
|
||||
subtitle: 'An introduction to database replication and change data capture.'
|
||||
sidebar_label: 'Overview'
|
||||
---
|
||||
@@ -18,7 +18,7 @@ You might use database replication for:
|
||||
|
||||
## Replication methods
|
||||
|
||||
Supabase supports three replication methods. Choose based on whether you need another Supabase Postgres database, a managed pipeline to an external system, or full control over your own logical replication setup.
|
||||
Supabase supports three replication methods. Choose based on whether you need another Supabase Postgres database, a managed pipeline to a destination system, or full control over your own logical replication setup.
|
||||
|
||||
### Read replicas
|
||||
|
||||
@@ -26,21 +26,21 @@ Read replicas are additional Supabase Postgres databases kept in sync with your
|
||||
|
||||
- [Set up read replicas](/docs/guides/platform/read-replicas)
|
||||
|
||||
### External replication
|
||||
### External replication (ETL) [#external-replication]
|
||||
|
||||
<Admonition type="caution" title="Private Alpha">
|
||||
|
||||
External replication is currently in private alpha. Access is limited and features may change.
|
||||
External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
External replication is powered by [Supabase ETL](https://github.com/supabase/etl). It uses Postgres logical replication under the hood and provides a managed Dashboard workflow for replicating data from Supabase Postgres to external data systems.
|
||||
External replication (ETL) is powered by [Supabase ETL](https://github.com/supabase/etl). It uses Postgres logical replication under the hood and provides a managed Dashboard workflow for replicating data from Supabase Postgres to destination systems. In this context, "external" means outside the source database. Destinations can be Supabase-managed or third-party systems as support expands.
|
||||
|
||||
- [Set up external replication](/docs/guides/database/replication/external-replication-setup)
|
||||
- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup)
|
||||
|
||||
#### Supported destinations
|
||||
|
||||
External replication currently supports BigQuery as the managed destination. We are working on new destinations, and this table will be updated as support expands.
|
||||
External replication (ETL) currently supports BigQuery as the managed destination. We are working on new destinations, and this table will be updated as support expands.
|
||||
|
||||
| Destination | Insert | Update | Delete | Truncate | Schema change | Description |
|
||||
| ------------------------------------------------------ | ------------ | ------------ | ------------ | ------------ | ------------- | ------------------------------------------------------------------- |
|
||||
@@ -48,7 +48,7 @@ External replication currently supports BigQuery as the managed destination. We
|
||||
|
||||
### Manual replication
|
||||
|
||||
Manual replication uses the same underlying Postgres logical replication features as external replication, but you configure and operate the pieces yourself. Use this path when you want to connect tools such as Airbyte, Estuary, Fivetran, Materialize, Stitch, AWS DMS, or another system that supports Postgres logical replication.
|
||||
Manual replication uses the same underlying Postgres logical replication features as external replication (ETL), but you configure and operate the pieces yourself. Use this path when you want to connect tools such as Airbyte, Estuary, Fivetran, Materialize, Stitch, AWS DMS, or another system that supports Postgres logical replication.
|
||||
|
||||
- [Set up manual replication](/docs/guides/database/replication/manual-replication-setup)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
id: 'bigquery-destination'
|
||||
title: 'BigQuery destination'
|
||||
description: 'Configure BigQuery as an external replication destination.'
|
||||
description: 'Configure BigQuery as an external replication (ETL) destination.'
|
||||
subtitle: 'Replicate Supabase Postgres tables to BigQuery.'
|
||||
sidebar_label: 'BigQuery'
|
||||
---
|
||||
|
||||
<Admonition type="caution" title="Private Alpha">
|
||||
|
||||
External replication is currently in private alpha. Access is limited and features may change.
|
||||
External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -29,17 +29,22 @@ Before configuring BigQuery as a destination, set up the following in Google Clo
|
||||
3. **GCP service account key**: Create a [service account](https://cloud.google.com/iam/docs/keys-create-delete) with appropriate permissions
|
||||
- Go to **IAM & Admin > Service Accounts**
|
||||
- Click **Create Service Account**
|
||||
- Grant the "BigQuery Data Editor" role
|
||||
- Grant the "BigQuery Data Editor" and "BigQuery Job User" roles
|
||||
- Create and download the JSON key file
|
||||
|
||||
Required permissions:
|
||||
|
||||
- `bigquery.datasets.get`
|
||||
- `bigquery.jobs.create`
|
||||
- `bigquery.tables.create`
|
||||
- `bigquery.tables.delete`
|
||||
- `bigquery.tables.get`
|
||||
- `bigquery.tables.getData`
|
||||
- `bigquery.tables.list`
|
||||
- `bigquery.tables.update`
|
||||
- `bigquery.tables.updateData`
|
||||
- `bigquery.routines.get`
|
||||
- `bigquery.routines.list`
|
||||
|
||||
## Configure BigQuery as a destination
|
||||
|
||||
@@ -85,19 +90,25 @@ Once configured, replication to BigQuery:
|
||||
|
||||
## Source table requirements
|
||||
|
||||
BigQuery replication requires each source table to have a primary key, and the publication must include the primary-key columns. External replication declares those columns as the BigQuery destination primary key so BigQuery CDC can apply `UPSERT` and `DELETE` rows.
|
||||
BigQuery replication requires each source table to have a primary key, and the publication must include the primary-key columns. Supabase ETL declares those columns as the BigQuery destination primary key so BigQuery change data capture (CDC) can apply `UPSERT` and `DELETE` rows.
|
||||
|
||||
BigQuery primary keys are `NOT ENFORCED`, and BigQuery CDC supports composite primary keys with up to 16 columns. Your source primary key must stay unique and non-null because BigQuery uses it to match CDC rows.
|
||||
BigQuery primary keys are `NOT ENFORCED`, and BigQuery change data capture (CDC) supports composite primary keys with up to 16 columns. Your source primary key must stay unique and non-null because BigQuery uses it to match CDC rows.
|
||||
|
||||
Source tables must also use a BigQuery-compatible Postgres `REPLICA IDENTITY` setting:
|
||||
Source tables must also use a BigQuery-compatible Postgres `REPLICA IDENTITY` setting. Most tables can keep the Postgres default, as long as they have a primary key and all primary-key columns are included in the publication.
|
||||
|
||||
- `DEFAULT` with a primary key is supported and works for most tables.
|
||||
- `FULL` is supported and is recommended for tables with large `text`, `jsonb`, `bytea`, or other values that Postgres may store out-of-line using TOAST.
|
||||
- `USING INDEX`, `NOTHING`, or `DEFAULT` without a primary key are not supported for BigQuery replication.
|
||||
| Source table setting | BigQuery support | Guidance |
|
||||
| ------------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REPLICA IDENTITY DEFAULT` with a primary key | Supported | Recommended for most tables. BigQuery uses the replicated source primary key to apply upserts and deletes. |
|
||||
| `REPLICA IDENTITY FULL` | Supported | Recommended for tables with large `text`, `jsonb`, `bytea`, or other values that Postgres may store out-of-line using TOAST, especially when those rows update. |
|
||||
| `REPLICA IDENTITY USING INDEX` | Not supported | BigQuery change data capture (CDC) rows are keyed by the source primary key, not by an alternative unique index. |
|
||||
| `REPLICA IDENTITY NOTHING` | Not supported | Updates and deletes do not include enough row identity for BigQuery to apply them safely. |
|
||||
| `REPLICA IDENTITY DEFAULT` without a primary key | Not supported | BigQuery requires a source primary key. |
|
||||
|
||||
For a general explanation of how replica identity affects update and delete events, see [How does replica identity affect updates and deletes?](/docs/guides/database/replication/external-replication-faq#how-does-replica-identity-affect-updates-and-deletes).
|
||||
|
||||
For updates, Postgres does not always send a complete old row through logical replication. It can also mark unchanged toasted values as `unchanged toast` instead of resending the value. The replication pipeline can reconstruct a complete update when the old row image contains the missing value, which is reliable with `REPLICA IDENTITY FULL`. BigQuery CDC upserts require a complete new row, so updates can fail for tables with toasted columns if the pipeline receives only a partial update row.
|
||||
For updates, Postgres does not always send a complete old row through logical replication. It can also mark unchanged toasted values as `unchanged toast` instead of resending the value. BigQuery change data capture (CDC) upserts require a complete new row because omitted columns are not preserved in the destination. The replication pipeline can reconstruct a complete update when the old row image contains the missing value, which is reliable with `REPLICA IDENTITY FULL`.
|
||||
|
||||
If a BigQuery pipeline fails with an error about a partial update row, set `REPLICA IDENTITY FULL` on the affected source table and restart the pipeline. Changing replica identity only affects new WAL records, so a retained update that was written before the change may still need to be skipped by recreating the pipeline or re-copying the affected table.
|
||||
|
||||
Check a table's current replica identity:
|
||||
|
||||
@@ -134,20 +145,26 @@ This structure handles table truncations while maintaining query compatibility.
|
||||
|
||||
## Schema change support
|
||||
|
||||
Schema change support for BigQuery is currently in beta. External replication supports a limited set of schema changes while the feature is developed further.
|
||||
Schema change support for BigQuery is currently in beta. Supabase ETL supports a limited set of schema changes for BigQuery while the feature is developed further.
|
||||
|
||||
Supported schema changes:
|
||||
|
||||
- Adding a column
|
||||
- Adding a nullable column
|
||||
- Removing a column
|
||||
- Renaming a column
|
||||
- Dropping a `NOT NULL` constraint
|
||||
- Setting or dropping supported column default metadata
|
||||
|
||||
Unsupported schema changes:
|
||||
Unsupported or limited schema changes:
|
||||
|
||||
- Changing a column's data type
|
||||
- Replicating column default values
|
||||
- Adding `NOT NULL` with `SET NOT NULL`
|
||||
- Filling existing rows for `ADD COLUMN ... DEFAULT`
|
||||
- Unsupported default expressions
|
||||
|
||||
We plan to expand schema change support over time as the feature evolves.
|
||||
BigQuery requires added columns to be nullable. When a replicated `ADD COLUMN` includes a default, external replication (ETL) can apply supported default metadata for future rows, but BigQuery does not backfill existing rows through that DDL. Existing destination rows remain `NULL` unless you run a separate backfill.
|
||||
|
||||
Supported defaults are best-effort translations to BigQuery SQL. Unsupported defaults are skipped with a warning instead of failing replication.
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -160,4 +177,4 @@ We plan to expand schema change support over time as the feature evolves.
|
||||
## Additional resources
|
||||
|
||||
- [BigQuery documentation](https://cloud.google.com/bigquery/docs) - Official Google BigQuery documentation
|
||||
- [BigQuery change data capture](https://cloud.google.com/bigquery/docs/change-data-capture) - BigQuery CDC requirements and limitations
|
||||
- [BigQuery change data capture](https://cloud.google.com/bigquery/docs/change-data-capture) - BigQuery change data capture (CDC) requirements and limitations
|
||||
@@ -1,26 +1,26 @@
|
||||
---
|
||||
id: 'external-replication-faq'
|
||||
title: 'External replication FAQ'
|
||||
description: 'Frequently asked questions about external replication.'
|
||||
subtitle: 'Common questions and answers about external replication.'
|
||||
title: 'External replication (ETL) FAQ'
|
||||
description: 'Frequently asked questions about external replication (ETL).'
|
||||
subtitle: 'Common questions and answers about external replication (ETL).'
|
||||
sidebar_label: 'FAQ'
|
||||
---
|
||||
|
||||
<Admonition type="caution" title="Private Alpha">
|
||||
|
||||
External replication is currently in private alpha. Access is limited and features may change.
|
||||
External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## What destinations are supported?
|
||||
|
||||
External replication currently supports **BigQuery** as the managed destination. See the [BigQuery destination guide](/docs/guides/database/replication/bigquery) for configuration details.
|
||||
External replication (ETL) currently supports **BigQuery** as the managed destination. See the [BigQuery destination guide](/docs/guides/database/replication/bigquery) for configuration details.
|
||||
|
||||
We are working on new destinations. Availability may continue to vary based on the planned roll-out strategy.
|
||||
We are working on new destinations. "External" means outside the source database, so supported destinations can be Supabase-managed or third-party systems as support expands. Availability may continue to vary based on the planned roll-out strategy.
|
||||
|
||||
## Which plans support external replication?
|
||||
## Which plans support external replication (ETL)? [#which-plans-support-external-replication]
|
||||
|
||||
External replication is available on the Pro, Team, and Enterprise plans.
|
||||
External replication (ETL) is available on the Pro, Team, and Enterprise plans.
|
||||
|
||||
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
|
||||
|
||||
@@ -30,11 +30,11 @@ We are currently working on a new Supabase Warehouse product designed to address
|
||||
|
||||
As a result, managed replication to Analytics Buckets is no longer available. Right now, **BigQuery** is the only supported managed destination, and we are actively working on expanding capabilities.
|
||||
|
||||
## What does external replication install in the database?
|
||||
## What does external replication (ETL) install in the database? [#what-does-external-replication-install-in-the-database]
|
||||
|
||||
When you enable external replication, Supabase installs database objects that help track replication state and support schema changes:
|
||||
When you enable external replication (ETL), Supabase installs database objects that help Supabase ETL track replication state and support schema changes:
|
||||
|
||||
- An event trigger that runs on every `ALTER TABLE` statement. External replication uses this to support schema change handling.
|
||||
- An event trigger that runs on every `ALTER TABLE` statement. Supabase ETL uses this to support schema change handling.
|
||||
- A set of tables in the `etl` schema. These tables track replication state for your pipelines.
|
||||
|
||||
The replication state tables are not updated very often, especially after the initial copy phase is complete.
|
||||
@@ -45,19 +45,21 @@ Schema change support is currently in beta and limited to the BigQuery destinati
|
||||
|
||||
Supported schema changes:
|
||||
|
||||
- Adding a column
|
||||
- Adding a nullable column
|
||||
- Removing a column
|
||||
- Renaming a column
|
||||
- Dropping a `NOT NULL` constraint
|
||||
- Setting or dropping supported column default metadata
|
||||
|
||||
External replication does not currently support changing column data types or replicating column default values. See [BigQuery schema change support](/docs/guides/database/replication/bigquery#schema-change-support) for details.
|
||||
External replication (ETL) does not currently support changing column data types, adding `NOT NULL` with `SET NOT NULL`, or filling existing destination rows for `ADD COLUMN ... DEFAULT`. Supported defaults are applied as destination metadata for future rows where BigQuery can represent them. See [BigQuery schema change support](/docs/guides/database/replication/bigquery#schema-change-support) for details.
|
||||
|
||||
## What happens when you disable external replication?
|
||||
## What happens when you disable external replication (ETL)? [#what-happens-when-you-disable-external-replication]
|
||||
|
||||
Disabling external replication removes the database objects that were installed in your source database, including the replication state tables in the `etl` schema and the DDL event trigger.
|
||||
Disabling external replication (ETL) removes the database objects that were installed in your source database, including the replication state tables in the `etl` schema and the DDL event trigger.
|
||||
|
||||
You must delete all external replication pipelines before the disable action is available. For the Dashboard steps, see [Disabling external replication](/docs/guides/database/replication/external-replication-setup#disabling-external-replication).
|
||||
To remove external replication (ETL) from a project, delete all replication pipelines first. After all pipelines are deleted, the disable action becomes available. For the Dashboard steps, see [Disabling external replication (ETL)](/docs/guides/database/replication/external-replication-setup#disabling-external-replication).
|
||||
|
||||
Disabling external replication stops Supabase from managing external replication for the project. It does not delete tables or data that were already written to your destination.
|
||||
Disabling external replication (ETL) stops Supabase from managing replication for the project. It does not delete tables or data that were already written to your destination.
|
||||
|
||||
## Why is a table not being replicated?
|
||||
|
||||
@@ -65,10 +67,20 @@ Common reasons:
|
||||
|
||||
- **Missing primary key**: Tables must have a primary key to be replicated (Postgres logical replication requirement)
|
||||
- **Not in publication**: Ensure the table is included in your Postgres publication
|
||||
- **Unsupported data types**: Tables with custom data types are not supported
|
||||
- **Generated columns**: Generated columns are skipped during replication
|
||||
|
||||
Check your publication settings and verify your table meets the requirements.
|
||||
|
||||
Custom data types replicate as strings. Check that your destination can interpret those string values correctly.
|
||||
|
||||
## Why are partitioned tables replicated as separate tables?
|
||||
|
||||
Postgres controls this with the publication's `publish_via_partition_root` setting. If the setting is `false`, or if you created the publication manually with SQL and did not set it, Postgres publishes changes from the leaf partitions. External replication (ETL) then creates destination tables for those leaf partitions. If `publish_via_partition_root = true`, Postgres publishes changes as the partition root, so the partition hierarchy is treated as the published partition root.
|
||||
|
||||
Publications created from the Dashboard replication flow use `publish_via_partition_root = true`.
|
||||
|
||||
See [Partitioned tables](/docs/guides/database/replication/external-replication-setup#partitioned-tables) for examples and the full behavior.
|
||||
|
||||
## How does replica identity affect updates and deletes?
|
||||
|
||||
If inserts replicate but updates or deletes fail, check the table's `REPLICA IDENTITY` setting.
|
||||
@@ -132,6 +144,20 @@ Pipeline failures occur during the streaming phase when an error happens while r
|
||||
|
||||
See [Handling errors](/docs/guides/database/replication/external-replication-monitoring#handling-errors) for more details.
|
||||
|
||||
## Why is replication lag increasing?
|
||||
|
||||
Lag increases when Postgres produces WAL faster than the pipeline can confirm it has processed. Common causes include a slow or rate-limited destination, a pipeline issue, heavy source database activity, long transactions, network latency between the pipeline and source database, or a stopped/disconnected pipeline.
|
||||
|
||||
Open [**Database > Replication**](/dashboard/project/_/database/replication), click **View status**, and check **Waiting to sync**, **Room before pausing**, **Last check-in**, **Connected**, and **Slot status**. See [Dealing with replication lag](/docs/guides/database/replication/external-replication-monitoring#dealing-with-replication-lag) for the full investigation and response flow.
|
||||
|
||||
## What does a `Lost` slot status mean?
|
||||
|
||||
`Lost` means Postgres has already removed WAL files that the pipeline's replication slot needed. The pipeline cannot continue from that slot.
|
||||
|
||||
You can recreate the pipeline, or open the pipeline's **Advanced settings**, set **Invalidated slot behavior** to **Recreate**, and restart the pipeline. On restart, the pipeline creates a new replication slot and starts replication from scratch for all tables. This is required for consistency because the old slot can no longer provide every change the pipeline missed.
|
||||
|
||||
See [Slot statuses](/docs/guides/database/replication/external-replication-monitoring#slot-statuses) for all slot states and what to do next.
|
||||
|
||||
## Why is a table in error state?
|
||||
|
||||
Table errors occur during the copy phase. To recover, click **View status**, find the affected table, and reset the table state. This will restart the table copy from the beginning.
|
||||
@@ -145,7 +171,7 @@ Check the [**Database > Replication**](/dashboard/project/_/database/replication
|
||||
3. Ensure all tables show **Live** state (actively replicating)
|
||||
4. Monitor replication lag metrics
|
||||
|
||||
See the [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring) for comprehensive monitoring instructions.
|
||||
See the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring) for comprehensive monitoring instructions.
|
||||
|
||||
## How to stop or pause replication
|
||||
|
||||
@@ -159,13 +185,13 @@ Stopping replication causes changes to queue up in the WAL.
|
||||
|
||||
## What happens if a project becomes inactive?
|
||||
|
||||
If your project becomes inactive, external replication stops any running pipelines and does not automatically resume them after the project is restarted.
|
||||
If your project becomes inactive, external replication (ETL) stops any running pipelines and does not automatically resume them after the project is restarted.
|
||||
|
||||
After restarting the project, restart each replication pipeline manually from the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard.
|
||||
|
||||
## What happens after a downgrade to the free plan?
|
||||
|
||||
When a project is downgraded to the Free Plan, all external replication pipelines for that project are deleted.
|
||||
When a project is downgraded to the Free Plan, all external replication (ETL) pipelines for that project are deleted.
|
||||
|
||||
## What happens if a table is deleted at the destination?
|
||||
|
||||
@@ -219,6 +245,6 @@ Navigate to the [**Logs > Replication**](/dashboard/project/_/logs/explorer) sec
|
||||
|
||||
If you need assistance:
|
||||
|
||||
1. Check the [external replication setup guide](/docs/guides/database/replication/external-replication-setup) and [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring)
|
||||
1. Check the [external replication (ETL) setup guide](/docs/guides/database/replication/external-replication-setup) and [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring)
|
||||
2. Review this FAQ for common issues
|
||||
3. Contact support with your error details and logs
|
||||
@@ -1,18 +1,18 @@
|
||||
---
|
||||
id: 'external-replication-monitoring'
|
||||
title: 'External replication monitoring'
|
||||
description: 'Monitor the status and health of your external replication pipelines.'
|
||||
title: 'External replication (ETL) monitoring'
|
||||
description: 'Monitor the status and health of your external replication (ETL) pipelines.'
|
||||
subtitle: 'Track replication status, view logs, and troubleshoot issues.'
|
||||
sidebar_label: 'Monitoring'
|
||||
---
|
||||
|
||||
<Admonition type="caution" title="Private Alpha">
|
||||
|
||||
External replication is currently in private alpha. Access is limited and features may change.
|
||||
External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
After setting up external replication, you can monitor the status and health of your replication pipelines directly from the Dashboard. The pipeline is the active Postgres replication process that continuously streams changes from your database to your destination.
|
||||
After setting up external replication (ETL), you can monitor the status and health of your replication pipelines directly from the Dashboard. The pipeline is the active Postgres replication process that continuously streams changes from your database to your destination.
|
||||
|
||||
### Viewing pipeline status
|
||||
|
||||
@@ -55,7 +55,35 @@ height={2146}
|
||||
|
||||
#### Replication lag metrics
|
||||
|
||||
The status page shows replication lag metrics that help you determine how fast your pipeline is replicating data. These metrics are loaded directly from Postgres itself.
|
||||
The status page shows replication lag metrics that help you determine how far the pipeline is behind Postgres. These metrics are loaded directly from Postgres replication slot state.
|
||||
|
||||
The destinations list also shows a compact lag value. This value is byte-based: it shows how much WAL the pipeline has not confirmed as flushed yet. A value of **Caught up** means the pipeline has confirmed every change currently available for its slot.
|
||||
|
||||
The detailed status page shows:
|
||||
|
||||
| Metric | What it means | What to watch for |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Waiting to sync** | Bytes of WAL between the pipeline's confirmed flush position and the current Postgres WAL position. This is the main byte-based replication lag. | A value that keeps growing means the pipeline is receiving changes more slowly than Postgres produces them. |
|
||||
| **Room before pausing** | How much WAL can still accumulate before Postgres can no longer safely keep all WAL needed by the replication slot. This is controlled by `max_slot_wal_keep_size`. | A small or shrinking value means the slot is getting closer to being invalidated. `Unlimited` means Postgres is not reporting a slot WAL retention limit. |
|
||||
| **Last check-in** | How long it has been since the pipeline last sent replication feedback to Postgres. | An old value can mean the pipeline is stopped, disconnected, overloaded, or unable to make progress. |
|
||||
| **Connected** | Whether the pipeline's replication slot is active and currently being used. | `Not connected` while the pipeline should be running usually means you should check pipeline status and logs. |
|
||||
| **Slot status** | How safely Postgres is keeping the WAL files the pipeline still needs. | `Unreserved` and `Lost` require action. See [Slot statuses](#slot-statuses). |
|
||||
|
||||
External replication (ETL) uses one main pipeline replication slot for ongoing changes. During the initial copy phase, it can also create temporary table-sync replication slots. These temporary slots let multiple tables copy in parallel, make large table copies faster, and allow individual tables to be retried or copied again without restarting the whole pipeline.
|
||||
|
||||
Temporary table-sync slots show the same kind of lag and slot health metrics while they are active. After a table finishes copying and catches up, its temporary slot is removed and ongoing changes continue through the main pipeline slot. For overall replication health, focus first on the main pipeline slot.
|
||||
|
||||
#### Slot statuses
|
||||
|
||||
Replication slot status tells you whether Postgres is still retaining the WAL that the pipeline needs to continue from its current position.
|
||||
|
||||
| Status | Meaning |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Reserved** | Healthy. Postgres is keeping the WAL files this pipeline's replication slot needs, and they are within the normal WAL size limit. |
|
||||
| **Extended** | Healthy, but growing. The slot is holding on to more WAL than usual, but Postgres is still keeping everything the pipeline needs. |
|
||||
| **Unreserved** | At risk. Postgres is no longer reserving all WAL files this pipeline's replication slot needs. If the pipeline does not catch up soon, those files may be removed. |
|
||||
| **Lost** | Broken. Some WAL files this pipeline's replication slot needs have already been removed. The pipeline can no longer continue from this slot. Recreate the pipeline, or set **Invalidated slot behavior** to **Recreate** in the pipeline's advanced settings and restart it. |
|
||||
| **Unknown** | Postgres reported an unknown or unavailable state for this pipeline's replication slot. |
|
||||
|
||||
#### Table states
|
||||
|
||||
@@ -69,6 +97,57 @@ The pipeline status page also shows the state of individual tables being replica
|
||||
| **Live** | Table is now replicating data in near real-time |
|
||||
| **Error** | Table has experienced an error during replication |
|
||||
|
||||
### Dealing with replication lag
|
||||
|
||||
Replication lag means the pipeline is behind the source database. Some lag is expected during the initial copy phase, after a burst of writes, or after restarting a stopped pipeline. Lag becomes a problem when it keeps increasing, when **Room before pausing** is running low, or when the slot status moves to **Unreserved** or **Lost**.
|
||||
|
||||
Lag can come from several places:
|
||||
|
||||
- **Destination throughput**: The destination is slow, rate-limited, unavailable, or rejecting writes.
|
||||
- **Pipeline throughput**: The pipeline is overloaded, processing a very large transaction, or not performing as expected for the project workload.
|
||||
- **Source database activity**: Postgres is producing WAL faster than the pipeline can consume it, often during bulk writes, migrations, or backfill jobs.
|
||||
- **Network latency**: Latency or instability between the pipeline and source database can slow down WAL streaming.
|
||||
- **Stopped or disconnected pipeline**: When a pipeline is stopped, disconnected, or failed, Postgres keeps WAL for the slot until the retention limit is reached.
|
||||
- **Slow initial table copy**: A temporary table-sync slot can fall behind if a table is copied more slowly than new changes are written to that table.
|
||||
|
||||
#### Initial copy and table-sync slots
|
||||
|
||||
A common initial sync issue happens when a large or busy table is still in **Copying** while new rows keep being inserted or updated. The temporary table-sync slot needs to keep the changes that happen during the copy. If the copy is too slow compared to the table's write rate, the slot can move to **Unreserved** and then **Lost** if Postgres removes changes the copy still needs.
|
||||
|
||||
When a table-sync slot is lost, the affected table needs to be copied again. Tune the copy settings, then retry the table copy:
|
||||
|
||||
- Increase **Copy connections per table** when one large table is the bottleneck. This lets the pipeline copy chunks of that table in parallel, up to the point where the source database or network becomes the limit.
|
||||
- Increase **Table sync workers** when several tables need to copy at the same time. Each worker can copy one table, and each worker uses an additional temporary replication slot during initial sync.
|
||||
- If possible, run the initial copy during a quieter write period or reduce bulk writes until the table reaches **Live**.
|
||||
|
||||
After the affected table finishes copying and catches up, the temporary slot is deleted. The table then continues through the main pipeline replication slot.
|
||||
|
||||
#### Investigate the lag
|
||||
|
||||
1. Open [**Database > Replication**](/dashboard/project/_/database/replication) and check the destination's lag column.
|
||||
2. Click **View status** and check **Waiting to sync**, **Room before pausing**, **Last check-in**, **Connected**, and **Slot status**.
|
||||
3. Check table states. Tables in **Copying** can create temporary lag while the initial snapshot catches up to live changes. If a table-sync slot is **Unreserved** or **Lost**, tune copy parallelism and retry the affected table copy.
|
||||
4. Open [**Logs > Replication**](/dashboard/project/_/logs/explorer) and look for destination errors, retries, rate limits, schema errors, or repeated restarts.
|
||||
5. Compare the lag trend with recent database activity, such as imports, migrations, bulk updates, or long transactions.
|
||||
|
||||
#### Respond based on the slot status
|
||||
|
||||
| Slot status | What to do |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Reserved** | If **Waiting to sync** is stable or decreasing, continue monitoring. If it keeps increasing, check destination write performance, logs, and whether the publication includes more tables or write volume than expected. |
|
||||
| **Extended** | Treat it as an early warning. Confirm the pipeline is connected, check logs for retries or destination slowness, and reduce avoidable write bursts if possible until the pipeline catches up. |
|
||||
| **Unreserved** | Act quickly. The slot is at risk of losing required WAL. Check whether the pipeline is connected and making progress, fix destination or pipeline errors, and contact support if the lag continues to grow. |
|
||||
| **Lost** | The pipeline cannot continue from the existing slot because required WAL has been removed. Recreate the pipeline, or set **Invalidated slot behavior** to **Recreate** in the pipeline's advanced settings and restart the pipeline. This creates a new slot and starts replication from scratch for all tables. |
|
||||
| **Unknown** | Check replication logs for errors or missing slot details. If the status remains unknown while the pipeline should be running, contact support with the pipeline ID and recent log details. |
|
||||
|
||||
#### Reduce future lag risk
|
||||
|
||||
- Keep publications focused on the tables and operations you need at the destination.
|
||||
- Avoid leaving pipelines stopped for long periods while the source database is still receiving writes.
|
||||
- Schedule bulk updates, imports, and migrations during lower-traffic windows when possible.
|
||||
- For BigQuery, verify that service account permissions, table requirements, and replica identity settings match the [BigQuery destination guide](/docs/guides/database/replication/bigquery).
|
||||
- If the initial copy is the bottleneck, review **Table sync workers** and **Copy connections per table** in the pipeline's advanced settings. Increasing them can speed up copying, but it uses more database connections and replication slots.
|
||||
|
||||
### Handling errors
|
||||
|
||||
Errors can occur at two levels: per table or per pipeline.
|
||||
@@ -186,9 +265,9 @@ If you notice issues with your replication:
|
||||
4. **Verify publication**: Ensure your Postgres publication is properly configured
|
||||
5. **Monitor replication lag**: High lag may indicate performance issues
|
||||
|
||||
For more troubleshooting tips, see the [external replication FAQ](/docs/guides/database/replication/external-replication-faq).
|
||||
For more troubleshooting tips, see the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq).
|
||||
|
||||
### Next steps
|
||||
|
||||
- [Set up external replication](/docs/guides/database/replication/external-replication-setup)
|
||||
- [View external replication FAQ](/docs/guides/database/replication/external-replication-faq)
|
||||
- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup)
|
||||
- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq)
|
||||
@@ -1,26 +1,26 @@
|
||||
---
|
||||
id: 'external-replication-setup'
|
||||
title: 'Set up external replication'
|
||||
description: 'Set up external replication using Postgres logical replication.'
|
||||
subtitle: 'Configure publications and destinations for external replication.'
|
||||
title: 'Set up external replication (ETL)'
|
||||
description: 'Set up external replication (ETL) using Postgres logical replication.'
|
||||
subtitle: 'Configure publications and destinations for external replication (ETL).'
|
||||
sidebar_label: 'Setting up'
|
||||
---
|
||||
|
||||
<Admonition type="caution" title="Private Alpha">
|
||||
|
||||
External replication is currently in private alpha. Access is limited and features may change.
|
||||
External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change.
|
||||
|
||||
</Admonition>
|
||||
|
||||
External replication is powered by [Supabase ETL](https://github.com/supabase/etl) and uses **Postgres logical replication** to stream changes from your database to external data systems. It provides a managed interface through the Dashboard to configure and monitor replication pipelines.
|
||||
External replication (ETL) is powered by [Supabase ETL](https://github.com/supabase/etl) and uses **Postgres logical replication** to stream changes from your database to destination systems. It provides a managed interface through the Dashboard to configure and monitor replication pipelines. In this context, "external" means outside the source database; destinations can be Supabase-managed or third-party systems as support expands.
|
||||
|
||||
## Setup overview
|
||||
|
||||
External replication requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Follow these steps to set up your replication pipeline.
|
||||
External replication (ETL) requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Supabase ETL runs the managed pipeline that reads from the publication and writes to the destination. Follow these steps to set up your replication pipeline.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
If you already have a Postgres publication set up, you can skip to [Step 2: Enable external replication](#step-2-enable-external-replication).
|
||||
If you already have a Postgres publication set up, you can skip to [Step 2: Enable external replication (ETL)](#step-2-enable-external-replication).
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -88,6 +88,44 @@ create publication pub_recent_orders
|
||||
for table orders where (created_at > '2024-01-01');
|
||||
```
|
||||
|
||||
##### Partitioned tables
|
||||
|
||||
External replication (ETL) follows Postgres publication semantics for partitioned tables. The `publish_via_partition_root` publication setting controls whether changes from partitions are emitted as the partition root or as the leaf partitions.
|
||||
|
||||
| Publication setting | What gets replicated | Destination shape |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `publish_via_partition_root = true` | Rows from the published partition root, including rows stored in its leaf partitions | One table matching the published partition root |
|
||||
| `publish_via_partition_root = false` | Rows from the leaf partitions under the published partition root | One table per replicated leaf partition |
|
||||
| Not set in SQL | Same as `false`, because Postgres defaults `publish_via_partition_root` to `false` | One table per replicated leaf partition |
|
||||
| Publishing an individual leaf partition | The leaf partition itself, regardless of `publish_via_partition_root` | One table for that leaf partition |
|
||||
| `FOR ALL TABLES` or `FOR TABLES IN SCHEMA` | Partition roots plus regular tables when `true`; leaf partitions plus regular tables when `false` or unset | Destination tables follow the effective Postgres publication table list |
|
||||
|
||||
For example, if `orders` is partitioned by month:
|
||||
|
||||
```sql
|
||||
-- Replicate the whole partition hierarchy as the parent table.
|
||||
create publication pub_orders_root
|
||||
for table orders
|
||||
with (publish_via_partition_root = true);
|
||||
|
||||
-- Replicate each leaf partition as its own table.
|
||||
create publication pub_orders_leaves
|
||||
for table orders
|
||||
with (publish_via_partition_root = false);
|
||||
```
|
||||
|
||||
Use `publish_via_partition_root = true` when you want analytics queries to read from a single destination table that has the parent table's schema. Use `false` when each partition should remain a separate destination table.
|
||||
|
||||
Publications created from the Dashboard replication flow use `publish_via_partition_root = true`. If you create or alter a publication manually with SQL, set this option explicitly so the destination shape matches what you expect.
|
||||
|
||||
On Postgres 15 and newer, row filters on partition publications apply during both the initial copy and streaming phases. External replication (ETL) uses the row filter attached to the effective publication table entry: the published partition root when `publish_via_partition_root = true`, and the published leaf relation when `publish_via_partition_root = false`.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
With `publish_via_partition_root = true`, truncating an individual leaf partition is not replicated as a truncate event for the published parent. If you need truncate replication, run `TRUNCATE` on the published partition root.
|
||||
|
||||
</Admonition>
|
||||
|
||||
#### Viewing publications in the Dashboard
|
||||
|
||||
After creating a publication via SQL, you can view it in the Dashboard:
|
||||
@@ -95,17 +133,17 @@ After creating a publication via SQL, you can view it in the Dashboard:
|
||||
1. Navigate to the [**Database > Publications**](/dashboard/project/_/database/publications) section of the Dashboard
|
||||
2. You'll see all your publications listed with their tables
|
||||
|
||||
### Step 2: Enable external replication
|
||||
### Step 2: Enable external replication (ETL) [#step-2-enable-external-replication]
|
||||
|
||||
Before creating an external replication pipeline, enable external replication for your project:
|
||||
Before creating an external replication (ETL) pipeline, enable it for your project:
|
||||
|
||||
1. Navigate to the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard
|
||||
2. Click **Add destination** to show the replication side panel
|
||||
3. Select an external replication destination, such as **BigQuery**
|
||||
4. Click **Enable external replication**
|
||||
3. Select an external replication (ETL) destination, such as **BigQuery**
|
||||
4. Click **Enable external replication (ETL)**
|
||||
|
||||
<Image
|
||||
alt="Enable external replication"
|
||||
alt="Enable external replication (ETL)"
|
||||
src="/docs/img/database/replication/replication-enable-replication.png"
|
||||
width={3560}
|
||||
height={2156}
|
||||
@@ -113,7 +151,7 @@ Before creating an external replication pipeline, enable external replication fo
|
||||
|
||||
### Step 3: Configure a destination
|
||||
|
||||
Once external replication is enabled and you have a Postgres publication, configure a destination. The destination is where your replicated data will be stored, while the pipeline is the active Postgres replication process that continuously streams changes from your database to that destination.
|
||||
Once external replication (ETL) is enabled and you have a Postgres publication, configure a destination. The destination is where your replicated data will be stored, while the pipeline is the active Postgres replication process that continuously streams changes from your database to that destination.
|
||||
|
||||
#### Choose and configure your destination
|
||||
|
||||
@@ -132,15 +170,17 @@ Follow these steps to configure your destination. Each destination has its own s
|
||||
|
||||
5. Optionally expand **Advanced settings** to tune pipeline behavior. These settings apply to the pipeline rather than the destination:
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Batch wait time** | `10000` milliseconds | Maximum time the pipeline waits to collect additional changes before flushing them. Lower values reduce replication latency. Higher values can improve batching efficiency. |
|
||||
| **Table sync workers** | `4` workers | Number of tables copied in parallel during the initial snapshot phase. Each worker uses one replication slot, up to `N + 1` total replication slots while syncing. |
|
||||
| **Copy connections per table** | `2` connections | Number of parallel database connections each table copy can use during the initial sync. Increasing this can speed up large table copies, but uses more database connections. |
|
||||
| **Invalidated slot behavior** | `Error` | What the pipeline does when its replication slot is invalidated. **Error** blocks startup so you can recover manually. **Recreate** rebuilds the slot and starts replication from scratch. |
|
||||
| Setting | Default | Description |
|
||||
| ------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Batch wait time** | `10000` milliseconds | Maximum time the pipeline waits to collect additional changes before flushing them. Lower values reduce replication latency. Higher values can improve batching efficiency. |
|
||||
| **Table sync workers** | `4` workers | Number of tables copied in parallel during the initial snapshot phase. Each worker uses one replication slot, up to `N + 1` total replication slots while syncing. |
|
||||
| **Copy connections per table** | `2` connections | Number of parallel database connections each table copy can use during the initial sync. Increasing this can speed up large table copies, but uses more database connections. |
|
||||
| **Invalidated slot behavior** | `Error` | What the pipeline does when its replication slot is invalidated. **Error** blocks startup so you can recover manually. **Recreate** rebuilds the slot on the next pipeline restart and starts replication from scratch for all tables. |
|
||||
|
||||
Leave these settings at their defaults unless you need to tune initial copy speed, latency, or recovery behavior.
|
||||
|
||||
Use **Invalidated slot behavior** carefully. If **Recreate** is selected and the pipeline restarts after Postgres has invalidated the main replication slot, the pipeline creates a new slot and copies all replicated tables again. This full restart is required for consistency because the old slot can no longer provide every change the pipeline missed.
|
||||
|
||||
6. Click **Create and start** to begin replication
|
||||
|
||||
Your replication pipeline now starts copying data from your database to your destination.
|
||||
@@ -150,14 +190,14 @@ Your replication pipeline now starts copying data from your database to your des
|
||||
After creating a destination, the replication pipeline starts and appears in the destinations list. You can monitor the pipeline's status and performance from the Dashboard.
|
||||
|
||||
<Image
|
||||
alt="External replication destinations list"
|
||||
alt="External replication (ETL) destinations list"
|
||||
src="/docs/img/database/replication/replication-destinations-list.png"
|
||||
width={3560}
|
||||
height={2146}
|
||||
|
||||
/>
|
||||
|
||||
For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring).
|
||||
For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring).
|
||||
|
||||
### Managing your pipeline
|
||||
|
||||
@@ -179,18 +219,18 @@ Available actions:
|
||||
- **Edit destination**: Modify destination settings like credentials or advanced options
|
||||
- **Delete**: Remove the destination and permanently stop replication
|
||||
|
||||
### Disabling external replication
|
||||
### Disabling external replication (ETL) [#disabling-external-replication]
|
||||
|
||||
To turn off external replication for a project, delete all external replication pipelines first. After all pipelines are removed, open the three-dot actions menu on the Replication page and click **Disable external replication**.
|
||||
To turn off external replication (ETL) for a project, delete all replication pipelines first. After all pipelines are removed, open the three-dot actions menu on the Replication page and click **Disable external replication (ETL)**.
|
||||
|
||||
<Image
|
||||
alt="Disable external replication from the Replication page actions menu"
|
||||
alt="Disable external replication (ETL) from the Replication page actions menu"
|
||||
src="/docs/img/database/replication/replication-disable-external-replication.png"
|
||||
width={3560}
|
||||
height={2156}
|
||||
/>
|
||||
|
||||
For cleanup details, see [What happens when you disable external replication?](/docs/guides/database/replication/external-replication-faq#what-happens-when-you-disable-external-replication).
|
||||
For cleanup details, see [What happens when you disable external replication (ETL)?](/docs/guides/database/replication/external-replication-faq#what-happens-when-you-disable-external-replication).
|
||||
|
||||
### Adding or removing tables
|
||||
|
||||
@@ -232,7 +272,7 @@ If your Postgres publication uses `FOR ALL TABLES` or `FOR TABLES IN SCHEMA`, ne
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
When a table is deleted at the destination, the behavior depends on the destination. In general, the pipeline tries to recreate the table so replication can continue. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [external replication FAQ](/docs/guides/database/replication/external-replication-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details.
|
||||
When a table is deleted at the destination, the behavior depends on the destination. In general, the pipeline tries to recreate the table so replication can continue. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -242,13 +282,13 @@ Schema change support depends on the destination. BigQuery is currently the only
|
||||
|
||||
### How it works
|
||||
|
||||
Once configured, external replication:
|
||||
Once configured, external replication (ETL):
|
||||
|
||||
1. **Captures** changes from your Postgres database using Postgres publications and logical replication
|
||||
2. **Streams** the changes through the replication pipeline
|
||||
2. **Streams** the changes through Supabase ETL
|
||||
3. **Loads** the data to your destination
|
||||
|
||||
External replication automatically optimizes how changes are delivered to the destination. The replication pipeline currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination.
|
||||
External replication (ETL) automatically optimizes how changes are delivered to the destination. Supabase ETL currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -259,14 +299,16 @@ If you encounter issues during setup:
|
||||
- **Pipeline failed to start**: Check the error message in the status view for specific details
|
||||
- **No data being replicated**: Verify your Postgres publication includes the correct tables and event types
|
||||
|
||||
For more troubleshooting help, see the [external replication FAQ](/docs/guides/database/replication/external-replication-faq).
|
||||
For more troubleshooting help, see the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq).
|
||||
|
||||
### Limitations
|
||||
|
||||
External replication has the following limitations:
|
||||
External replication (ETL) has the following limitations:
|
||||
|
||||
- **Primary keys required**: Tables must have primary keys (Postgres logical replication requirement)
|
||||
- **Custom data types**: Not supported
|
||||
- **Custom data types**: Custom values replicate as strings. Check that your destination can interpret those string values correctly.
|
||||
- **Generated columns**: Generated columns are skipped. Use triggers to store derived values in regular columns if you need them in the destination.
|
||||
- **Replica identity**: `REPLICA IDENTITY FULL` is strongly recommended. Updates and deletes need enough row data to apply changes correctly at the destination.
|
||||
- **Schema changes**: Currently in beta and limited to BigQuery
|
||||
- **No data transformation**: Data is replicated as-is without transformation
|
||||
- **Data duplicates**: Duplicates can occur when stopping a pipeline if your database has transactions that take longer than a few minutes to complete. See [Can data duplicates occur during pipeline operations?](/docs/guides/database/replication/external-replication-faq#can-data-duplicates-occur-during-pipeline-operations) for details
|
||||
@@ -276,5 +318,5 @@ Destination-specific limitations, such as BigQuery's row size limits, are docume
|
||||
### Next steps
|
||||
|
||||
- [Set up BigQuery](/docs/guides/database/replication/bigquery)
|
||||
- [Monitor external replication](/docs/guides/database/replication/external-replication-monitoring)
|
||||
- [View external replication FAQ](/docs/guides/database/replication/external-replication-faq)
|
||||
- [Monitor external replication (ETL)](/docs/guides/database/replication/external-replication-monitoring)
|
||||
- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq)
|
||||
@@ -6,11 +6,11 @@ subtitle: 'Set up replication with Airbyte, Estuary, Fivetran, and other tools.'
|
||||
sidebar_label: 'Setting up'
|
||||
---
|
||||
|
||||
This guide covers setting up **manual logical replication** using external tools. If you prefer a managed solution, read the [external replication setup guide](/docs/guides/database/replication/external-replication-setup) instead.
|
||||
This guide covers setting up **manual logical replication** using your own tools. If you prefer a managed solution through Supabase ETL, read the [external replication (ETL) setup guide](/docs/guides/database/replication/external-replication-setup) instead.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
This guide is for replicating data to external systems using your own tools. For deploying read-only databases across multiple regions, see [read replicas](/docs/guides/platform/read-replicas) instead.
|
||||
This guide is for replicating data to destination systems using your own tools. For deploying read-only databases across multiple regions, see [read replicas](/docs/guides/platform/read-replicas) instead.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -21,7 +21,7 @@ To set up replication, the following is recommended:
|
||||
- Instance size of XL or greater
|
||||
- [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled
|
||||
|
||||
To create a replication slot, you will need to use the `postgres` user and follow the instructions in the [external replication setup guide](/docs/guides/database/postgres/setup-replication-external).
|
||||
To create a replication slot, you will need to use the `postgres` user and follow the instructions in the [logical replication example](/docs/guides/database/postgres/setup-replication-external).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -29,7 +29,7 @@ If you are running Postgres 17 or higher, you can create a new user and grant th
|
||||
|
||||
</Admonition>
|
||||
|
||||
If you are replicating to an external system and using any of the tools below, check their documentation first. Additional information is provided where the setup with Supabase can vary.
|
||||
If you are replicating to a destination system and using any of the tools below, check their documentation first. Additional information is provided where the setup with Supabase can vary.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -77,7 +77,7 @@ Materialize has the following [documentation](https://materialize.com/docs/sql/c
|
||||
|
||||
You can follow those steps with the following modifications:
|
||||
|
||||
1. Follow the steps in the [external replication setup guide](/docs/guides/database/postgres/setup-replication-external) to create a publication slot
|
||||
1. Follow the steps in the [logical replication example](/docs/guides/database/postgres/setup-replication-external) to create a publication slot
|
||||
|
||||
</TabPanel>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ You can subscribe to webhook notifications when an action run completes on a per
|
||||
"details_url": "https://supabase.com/dashboard/project/xuqpsshjxdecrwdyuxvs/branches",
|
||||
"action_run": {
|
||||
"id": "d5f8b4298d0a4d37b99e255c7837e7af",
|
||||
"created_at": "2025-10-17T02:27:10.133329324Z"
|
||||
"created_at": "2025-10-17T02:27:10.133329324Z",
|
||||
"steps": [
|
||||
{
|
||||
"name": "clone",
|
||||
|
||||
@@ -73,19 +73,18 @@ Generate text embeddings using the built-in [`gte-small`](https://huggingface.co
|
||||
</Admonition>
|
||||
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const input = params.get('input')
|
||||
const output = await model.run(input, { mean_pool: true, normalize: true })
|
||||
return new Response(JSON.stringify(output), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
})
|
||||
})
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const input = params.get('input')
|
||||
const output = await model.run(input, { mean_pool: true, normalize: true })
|
||||
return Response.json(output)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -157,42 +156,46 @@ We are progressively rolling out support for the hosted solution. To sign up for
|
||||
|
||||
```ts supabase/functions/ollama-test/index.ts
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const session = new Supabase.ai.Session('mistral')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
|
||||
// Get the output as a stream
|
||||
const output = await session.run(prompt, { stream: true })
|
||||
// Get the output as a stream
|
||||
const output = await session.run(prompt, { stream: true })
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
try {
|
||||
for await (const chunk of output) {
|
||||
controller.enqueue(encoder.encode(chunk.response ?? ''))
|
||||
try {
|
||||
for await (const chunk of output) {
|
||||
controller.enqueue(encoder.encode(chunk.response ?? ''))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
@@ -201,7 +204,7 @@ We are progressively rolling out support for the hosted solution. To sign up for
|
||||
<StepHikeCompact.Step step={5} fullWidth>
|
||||
<StepHikeCompact.Details title="Serve the function" fullWidth>
|
||||
```bash
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
@@ -265,36 +268,40 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
|
||||
|
||||
```ts supabase/functions/llamafile-test/index.ts
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const session = new Supabase.ai.Session('LLaMA_CPP')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
|
||||
// Get the output as a stream
|
||||
const output = await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: false,
|
||||
}
|
||||
)
|
||||
// Get the output as a stream
|
||||
const output = await session.run(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
|
||||
stream: false,
|
||||
}
|
||||
)
|
||||
|
||||
console.log('done')
|
||||
return Response.json(output)
|
||||
})
|
||||
console.log('done')
|
||||
return Response.json(output)
|
||||
}),
|
||||
}
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -302,7 +309,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
|
||||
<StepHikeCompact.Step step={4} fullWidth>
|
||||
<StepHikeCompact.Details title="Serve the function" fullWidth>
|
||||
```bash
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
@@ -350,60 +357,63 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
|
||||
</Admonition>
|
||||
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const client = new OpenAI()
|
||||
const { prompt } = await req.json()
|
||||
const stream = true
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
const client = new OpenAI()
|
||||
const { prompt } = await req.json()
|
||||
const stream = true
|
||||
|
||||
const chatCompletion = await client.chat.completions.create({
|
||||
model: 'LLaMA_CPP',
|
||||
stream,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (stream) {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
const chatCompletion = await client.chat.completions.create({
|
||||
model: 'LLaMA_CPP',
|
||||
stream,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
if (stream) {
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
try {
|
||||
for await (const part of chatCompletion) {
|
||||
controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
try {
|
||||
for await (const part of chatCompletion) {
|
||||
controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
return Response.json(chatCompletion)
|
||||
})
|
||||
return Response.json(chatCompletion)
|
||||
}),
|
||||
}
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
@@ -411,7 +421,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
|
||||
<StepHikeCompact.Step step={4} fullWidth>
|
||||
<StepHikeCompact.Details title="Serve the function" fullWidth>
|
||||
```bash
|
||||
supabase functions serve --env-file supabase/functions/.env
|
||||
supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
@@ -455,7 +465,7 @@ Once the function is working locally, it's time to deploy to production.
|
||||
<StepHikeCompact.Step step={2} fullWidth>
|
||||
<StepHikeCompact.Details title="Deploy the function" fullWidth>
|
||||
```bash
|
||||
supabase functions deploy
|
||||
supabase functions deploy --no-verify-jwt
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
@@ -19,29 +19,39 @@ This allows you to:
|
||||
You can use `EdgeRuntime.waitUntil(promise)` to explicitly mark background tasks. The Function instance continues to run until the promise provided to `waitUntil` completes.
|
||||
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
// Mark the asyncLongRunningTask's returned promise as a background task.
|
||||
// ⚠️ We are NOT using `await` because we don't want it to block!
|
||||
EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
return new Response(...)
|
||||
})
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
return new Response(...)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
You can call `EdgeRuntime.waitUntil` in the request handler too. This will not block the request.
|
||||
|
||||
```ts
|
||||
Deno.serve(async (req) => {
|
||||
// Won't block the request, runs in background.
|
||||
EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
return new Response(...)
|
||||
})
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
// Won't block the request, runs in background.
|
||||
EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
|
||||
return new Response(...)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
You can listen to the `beforeunload` event handler to be notified when the Function is about to be shut down.
|
||||
|
||||
```tsx
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
|
||||
// Use beforeunload event handler to be notified when function is about to shutdown
|
||||
@@ -50,9 +60,11 @@ addEventListener('beforeunload', (ev) => {
|
||||
// Save state or log the current progress
|
||||
})
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
return new Response(...)
|
||||
})
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
return new Response(...)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Handling errors
|
||||
|
||||
@@ -13,33 +13,28 @@ You can also use other Postgres clients like [Deno Postgres](https://deno.land/x
|
||||
|
||||
## Using supabase-js
|
||||
|
||||
The `supabase-js` client handles authorization with Row Level Security and automatically formats responses as JSON. This is the recommended approach for most applications:
|
||||
The [`withSupabase`](/docs/guides/functions/auth) wrapper from `@supabase/server` hands you a `supabase-js` client (`ctx.supabase`) already scoped to the caller's Row Level Security policies, so you don't manage keys or authorization headers yourself. It also provides `ctx.supabaseAdmin` for privileged operations that bypass Row Level Security. Responses are automatically formatted as JSON. This is the recommended approach for most applications:
|
||||
|
||||
```ts index.ts
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '',
|
||||
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } }
|
||||
)
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
try {
|
||||
// ctx.supabase respects the caller's RLS policies.
|
||||
// ctx.supabaseAdmin bypasses RLS for privileged operations.
|
||||
const { data, error } = await ctx.supabase.from('countries').select('*')
|
||||
|
||||
const { data, error } = await supabase.from('countries').select('*')
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
return Response.json({ data })
|
||||
} catch (err) {
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ data }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (err) {
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
This enables:
|
||||
|
||||
@@ -6,9 +6,24 @@ description: 'Add CORS headers to invoke Edge Functions from the browser.'
|
||||
|
||||
To invoke edge functions from the browser, you need to handle [CORS Preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) requests.
|
||||
|
||||
See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
|
||||
## Automatic CORS handling
|
||||
|
||||
### Recommended setup
|
||||
The [`withSupabase`](/docs/guides/functions/auth) wrapper handles CORS and preflight (`OPTIONS`) requests for you, so you don't add headers manually:
|
||||
|
||||
```ts index.ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
const { name } = await req.json()
|
||||
return Response.json({ message: `Hello ${name}!` })
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Manual CORS handling
|
||||
|
||||
If your function doesn't use `withSupabase`, add the headers yourself. See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
@@ -23,34 +38,26 @@ import { corsHeaders } from '@supabase/supabase-js/cors'
|
||||
|
||||
console.log(`Function "browser-with-cors" up and running!`)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
export default {
|
||||
fetch: async (req) => {
|
||||
// Handle the CORS preflight request.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
})
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 400, headers: corsHeaders })
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This approach ensures that when new headers are added to the Supabase SDK, your Edge Functions automatically include them, preventing CORS errors.
|
||||
|
||||
#### For versions before 2.95.0
|
||||
### For versions before 2.95.0
|
||||
|
||||
If you're using `@supabase/supabase-js` before v2.95.0, you'll need to hardcode the CORS headers. Add a `cors.ts` file within a [`_shared` folder](/docs/guides/functions/development-environment#recommended-project-structure):
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
id: 'functions-error-codes'
|
||||
title: 'Error codes'
|
||||
description: 'Edge Functions can return the following error codes.'
|
||||
subtitle: 'Understand the error codes returned by Edge Functions to properly debug issues and handle responses.'
|
||||
---
|
||||
|
||||
{/* supa-mdx-lint-disable Rule001HeadingCase */}
|
||||
|
||||
When an Edge Function request fails, the response includes a `sb-error-code` header that identifies the specific error.
|
||||
You can inspect this header in your HTTP client or application code to detect and handle errors programmatically.
|
||||
|
||||
```js
|
||||
const response = await fetch('<your-function-url>')
|
||||
|
||||
if (!response.ok) {
|
||||
const errorCode = response.headers.get('sb-error-code')
|
||||
console.error('Edge Function error:', errorCode)
|
||||
}
|
||||
```
|
||||
|
||||
## Bad Implementation Errors
|
||||
|
||||
These errors are caused by issues in your function's code or logic which requires updating its implementation.
|
||||
|
||||
### EDGE_FUNCTION_ERROR
|
||||
|
||||
**Cause:** Your Edge Function is throwing an unhandled error or resulting a 5XX code.
|
||||
|
||||
```ts
|
||||
// ...
|
||||
|
||||
function process() {
|
||||
throw new Error('Some unhandled error')
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async () => {
|
||||
process()
|
||||
|
||||
return new Response()
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are catching errors in your code logic with try-catch blocks.
|
||||
|
||||
```ts
|
||||
function process() {
|
||||
throw new Error('Some unhandled error')
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
try {
|
||||
process()
|
||||
return new Response()
|
||||
} catch (e) {
|
||||
console.error('Process fail:', e)
|
||||
return new Response(null, { status: 500 })
|
||||
}
|
||||
```
|
||||
|
||||
### IDLE_TIMEOUT
|
||||
|
||||
**Cause:** Your Edge Function did not respond within the [request timeout limit](/docs/guides/functions/limits).
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Long-running database queries
|
||||
- Slow external API calls
|
||||
- Infinite loops or blocking operations
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Optimize slow operations
|
||||
- Add timeout handling to external requests
|
||||
- Consider breaking large operations into smaller chunks
|
||||
|
||||
### WORKER_RESOURCE_LIMIT, WORKER_LIMIT
|
||||
|
||||
**Cause:** Your Edge Function execution was stopped due to exceeding resource limits. Edge Function logs should indicate which [resource limit](/docs/guides/functions/limits) was exceeded.
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Memory usage exceeded available limits
|
||||
- CPU time exceeded execution quotas
|
||||
- Too many concurrent operations
|
||||
|
||||
**Solution:** Check your Edge Function logs to see which resource limit was exceeded, then optimize your function accordingly.
|
||||
|
||||
### WORKER_ERROR
|
||||
|
||||
**Cause:** Your Edge Function threw an uncaught exception.
|
||||
|
||||
```ts
|
||||
// ...
|
||||
|
||||
function initSomething() {
|
||||
throw new Error('Some unhandled error')
|
||||
}
|
||||
|
||||
initSomething() // Error threw outside request handler
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async () => {
|
||||
return new Response()
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Unhandled JavaScript errors in your function code, outside request handler
|
||||
- Missing error handling for async operations
|
||||
- Invalid JSON parsing
|
||||
|
||||
**Solution:** Check your Edge Function logs to identify the specific error and add proper error handling to your code.
|
||||
|
||||
### INVALID_RESPONSE_STATUS_CODE
|
||||
|
||||
**Cause:** Your Edge Function is returning an invalid HTTP status code — not equal to `101` and outside the range `[200, 599]`
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Proxying an external service that returns an invalid HTTP status code
|
||||
|
||||
```ts
|
||||
// ...
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
// Fails in case this proxied server return a status >599
|
||||
return fetch('https://some-server-to-proxy', {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are returning a valid HTTP status code
|
||||
- For proxy endpoints, do not return the `fetch()` result directly; instead return a new `Response` wrapped in a try-catch block
|
||||
|
||||
```ts
|
||||
// ...
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
try {
|
||||
const res = await fetch('https://some-server-to-proxy', {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
})
|
||||
|
||||
// Creating a 'new Response()' ensures contructor checks
|
||||
return new Response(await res.body, {
|
||||
headers: res.headers,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Proxy Error', e)
|
||||
return new Response(null, { status: 502 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication Errors
|
||||
|
||||
These errors occur when the request contains a missing, malformed, or unsupported JWT token. Fixing them requires ensuring your requests include a valid authorization header, or disabling JWT verification for public endpoints.
|
||||
For further information, see [Authorization headers](/docs/guides/functions/auth-headers) and [Securing Edge Functions](/guides/functions/auth).
|
||||
|
||||
### UNAUTHORIZED_NO_AUTH_HEADER
|
||||
|
||||
**Cause:** The Edge Function has JWT verification enabled, but the request is missing the `Authorization` or `apikey` header.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are passing a valid JWT token in the `Authorization` header
|
||||
- Check that you are sending an API key in the `apikey` header
|
||||
- For webhooks or public endpoints, consider disabling JWT verification
|
||||
|
||||
### UNAUTHORIZED_ASYMMETRIC_JWT
|
||||
|
||||
**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header contains an invalid asymmetric `ES256 | RS256` token.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are passing a valid user JWT token in the `Authorization` header
|
||||
- Check that your token has not expired
|
||||
|
||||
### UNAUTHORIZED_LEGACY_JWT
|
||||
|
||||
**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header contains an invalid legacy `HS256` token.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are passing a valid legacy JWT token in the `Authorization` header
|
||||
- Check that your token has not expired
|
||||
- Verify that the legacy JWT secret has not been revoked or disabled
|
||||
|
||||
### UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM
|
||||
|
||||
**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header does not contain an `ES256 | RS256 | HS256` token.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are passing a valid Supabase-issued JWT token in the `Authorization` header
|
||||
|
||||
### UNAUTHORIZED_INVALID_JWT_FORMAT
|
||||
|
||||
**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header does not follow the `Bearer <JWT Token>` format.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Check that you are passing `Bearer <JWT Token>` in the `Authorization` header
|
||||
- Ensure you are sending an API key in the `apikey` header instead of `Authorization`
|
||||
- For webhooks or public endpoints, consider disabling JWT verification
|
||||
|
||||
## Request Errors
|
||||
|
||||
These errors indicate issues with the request itself, which typically require changing how the function is called.
|
||||
|
||||
### RATE_LIMIT_EXCEEDED
|
||||
|
||||
**Cause:** The platform detected [recursive or nested function call](/docs/guides/functions/recursive-functions) behavior.
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Multiple function-to-function calls
|
||||
- Recursive or circular calls
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Use the suggested retry window in seconds from the error message before calling your function again
|
||||
- Ensure you are not performing unnecessary individual calls; use batch operations where possible
|
||||
- Delegate large workloads to queues instead of recursively calling other Edge Functions
|
||||
|
||||
### INVALID_URL
|
||||
|
||||
**Cause:** The platform rejected a malformed URL.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure you are calling with a valid [formatted URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
|
||||
|
||||
---
|
||||
|
||||
## Server Errors
|
||||
|
||||
These errors indicate issues with function loading, execution, or the underlying platform.
|
||||
|
||||
### NOT_FOUND
|
||||
|
||||
**Cause:** The Edge Function metadata or files were not found or are missing in the specific region.
|
||||
|
||||
**Solution:** Try redeploying your function and wait a few minutes to make sure all regions have been updated.
|
||||
|
||||
### BOOT_ERROR
|
||||
|
||||
**Cause:** Your Edge Function failed to start.
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Syntax errors preventing the function from loading
|
||||
- Import errors or missing dependencies
|
||||
- Invalid function configuration
|
||||
|
||||
**Solution:** Check your Edge Function logs and also verify that your function code can be executed locally with `supabase functions serve`.
|
||||
|
||||
### LOAD_FUNCTION_ERROR
|
||||
|
||||
**Cause:** The platform was unable to load your function metadata or files.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Try calling your function again after a short delay
|
||||
- If the problem persists, contact support
|
||||
|
||||
### LOAD_FUNCTION_METADATA_ERROR
|
||||
|
||||
**Cause:** The platform could not fetch your function metadata, possibly due to external cache issues.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Wait a few minutes before calling your function again
|
||||
- If the problem persists, contact support
|
||||
|
||||
### LOAD_FUNCTION_INVALID_ENTRYPOINT_PATH_ERROR
|
||||
|
||||
**Cause:** Your Edge Function metadata is broken or contains an invalid entrypoint.
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Try redeploying your function
|
||||
- If the problem persists, contact support
|
||||
@@ -46,91 +46,83 @@ And add the code to the `index.ts` file:
|
||||
```ts index.ts
|
||||
// We need to mock the file system for the AWS SDK to work.
|
||||
import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts'
|
||||
|
||||
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime'
|
||||
import { createClient } from 'npm:@supabase/supabase-js'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { decode } from 'npm:base64-arraybuffer'
|
||||
|
||||
console.log('Hello from Amazon Bedrock!')
|
||||
|
||||
const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
|
||||
// Called with a publishable key on the `apikey` header. Deploy with `verify_jwt = false`.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
prepareVirtualFile('./aws/config')
|
||||
prepareVirtualFile('./aws/credentials')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
prepareVirtualFile('./aws/config')
|
||||
prepareVirtualFile('./aws/credentials')
|
||||
|
||||
const client = new BedrockRuntimeClient({
|
||||
region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2',
|
||||
credentials: {
|
||||
accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
|
||||
secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
|
||||
sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
const { prompt, seed } = await req.json()
|
||||
console.log(prompt)
|
||||
const input = {
|
||||
contentType: 'application/json',
|
||||
accept: '*/*',
|
||||
modelId: 'amazon.titan-image-generator-v1',
|
||||
body: JSON.stringify({
|
||||
taskType: 'TEXT_IMAGE',
|
||||
textToImageParams: { text: prompt },
|
||||
imageGenerationConfig: {
|
||||
numberOfImages: 1,
|
||||
quality: 'standard',
|
||||
cfgScale: 8.0,
|
||||
height: 512,
|
||||
width: 512,
|
||||
seed: seed ?? 0,
|
||||
const client = new BedrockRuntimeClient({
|
||||
region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2',
|
||||
credentials: {
|
||||
accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
|
||||
secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
|
||||
sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const command = new InvokeModelCommand(input)
|
||||
const response = await client.send(command)
|
||||
console.log(response)
|
||||
|
||||
if (response.$metadata.httpStatusCode === 200) {
|
||||
const { body, $metadata } = response
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const jsonString = textDecoder.decode(body.buffer)
|
||||
const parsedData = JSON.parse(jsonString)
|
||||
console.log(parsedData)
|
||||
const image = parsedData.images[0]
|
||||
|
||||
const supabaseClient = createClient(
|
||||
// Supabase API URL - env var exported by default.
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
// Using the default Supabase API PUB KEY.
|
||||
// If you want to use a different api key, change 'default' to your preferred key name
|
||||
SUPABASE_PUBLISHABLE_KEYS['default']
|
||||
)
|
||||
|
||||
const { data: upload, error: uploadError } = await supabaseClient.storage
|
||||
.from('images')
|
||||
.upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (!upload) {
|
||||
return Response.json(uploadError)
|
||||
const { prompt, seed } = await req.json()
|
||||
console.log(prompt)
|
||||
const input = {
|
||||
contentType: 'application/json',
|
||||
accept: '*/*',
|
||||
modelId: 'amazon.titan-image-generator-v1',
|
||||
body: JSON.stringify({
|
||||
taskType: 'TEXT_IMAGE',
|
||||
textToImageParams: { text: prompt },
|
||||
imageGenerationConfig: {
|
||||
numberOfImages: 1,
|
||||
quality: 'standard',
|
||||
cfgScale: 8.0,
|
||||
height: 512,
|
||||
width: 512,
|
||||
seed: seed ?? 0,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!)
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
return Response.json(response)
|
||||
})
|
||||
const command = new InvokeModelCommand(input)
|
||||
const response = await client.send(command)
|
||||
console.log(response)
|
||||
|
||||
if (response.$metadata.httpStatusCode === 200) {
|
||||
const { body, $metadata } = response
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8')
|
||||
const jsonString = textDecoder.decode(body.buffer)
|
||||
const parsedData = JSON.parse(jsonString)
|
||||
console.log(parsedData)
|
||||
const image = parsedData.images[0]
|
||||
|
||||
const { data: upload, error: uploadError } = await ctx.supabase.storage
|
||||
.from('images')
|
||||
.upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
|
||||
contentType: 'image/png',
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
})
|
||||
if (!upload) {
|
||||
return Response.json(uploadError)
|
||||
}
|
||||
const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!)
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
return Response.json(response)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Run the function locally
|
||||
|
||||
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
|
||||
2. Start with env: `supabase functions serve --env-file supabase/.env`
|
||||
2. Start with env: `supabase functions serve --no-verify-jwt --env-file supabase/.env`
|
||||
3. Make an HTTP request:
|
||||
|
||||
```bash
|
||||
@@ -146,7 +138,7 @@ Deno.serve(async (req) => {
|
||||
|
||||
```bash
|
||||
supabase link
|
||||
supabase functions deploy amazon-bedrock
|
||||
supabase functions deploy amazon-bedrock --no-verify-jwt
|
||||
supabase secrets set --env-file supabase/.env
|
||||
```
|
||||
|
||||
|
||||
+62
-66
@@ -34,84 +34,80 @@ supabase functions new send-email
|
||||
Paste the following code into the `index.ts` file:
|
||||
|
||||
```tsx supabase/functions/send-email/index.ts
|
||||
import React from 'npm:react@18.3.1'
|
||||
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
||||
import { Resend } from 'npm:resend@4.0.0'
|
||||
import { renderAsync } from 'npm:@react-email/components@0.0.22'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import React from 'npm:react@18.3.1'
|
||||
import { Resend } from 'npm:resend@4.0.0'
|
||||
|
||||
import { MagicLinkEmail } from './_templates/magic-link.tsx'
|
||||
|
||||
const resend = new Resend(Deno.env.get('RESEND_API_KEY') as string)
|
||||
const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v1,whsec_', '')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method !== 'POST') {
|
||||
return new Response('not allowed', { status: 400 })
|
||||
}
|
||||
|
||||
const payload = await req.text()
|
||||
const headers = Object.fromEntries(req.headers)
|
||||
const wh = new Webhook(hookSecret)
|
||||
try {
|
||||
const {
|
||||
user,
|
||||
email_data: { token, token_hash, redirect_to, email_action_type },
|
||||
} = wh.verify(payload, headers) as {
|
||||
user: {
|
||||
email: string
|
||||
}
|
||||
email_data: {
|
||||
token: string
|
||||
token_hash: string
|
||||
redirect_to: string
|
||||
email_action_type: string
|
||||
site_url: string
|
||||
token_new: string
|
||||
token_hash_new: string
|
||||
}
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
if (req.method !== 'POST') {
|
||||
return new Response('not allowed', { status: 400 })
|
||||
}
|
||||
|
||||
const html = await renderAsync(
|
||||
React.createElement(MagicLinkEmail, {
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
const payload = await req.text()
|
||||
const headers = Object.fromEntries(req.headers)
|
||||
const wh = new Webhook(hookSecret)
|
||||
try {
|
||||
const {
|
||||
user,
|
||||
email_data: { token, token_hash, redirect_to, email_action_type },
|
||||
} = wh.verify(payload, headers) as {
|
||||
user: {
|
||||
email: string
|
||||
}
|
||||
email_data: {
|
||||
token: string
|
||||
token_hash: string
|
||||
redirect_to: string
|
||||
email_action_type: string
|
||||
site_url: string
|
||||
token_new: string
|
||||
token_hash_new: string
|
||||
}
|
||||
}
|
||||
|
||||
const html = await renderAsync(
|
||||
React.createElement(MagicLinkEmail, {
|
||||
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
|
||||
token,
|
||||
token_hash,
|
||||
redirect_to,
|
||||
email_action_type,
|
||||
})
|
||||
)
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: 'welcome <onboarding@resend.dev>',
|
||||
to: [user.email],
|
||||
subject: 'Supa Custom MagicLink!',
|
||||
html,
|
||||
})
|
||||
)
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: 'welcome <onboarding@resend.dev>',
|
||||
to: [user.email],
|
||||
subject: 'Supa Custom MagicLink!',
|
||||
html,
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
http_code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
http_code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers()
|
||||
responseHeaders.set('Content-Type', 'application/json')
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
})
|
||||
return Response.json({})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create React Email templates
|
||||
|
||||
@@ -23,7 +23,7 @@ supabase functions new cloudflare-turnstile
|
||||
And add the code to the `index.ts` file:
|
||||
|
||||
```ts index.ts
|
||||
import { corsHeaders } from '@supabase/supabase-js/cors' // v2.95.0+
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log('Hello from Cloudflare Trunstile!')
|
||||
|
||||
@@ -31,36 +31,34 @@ function ips(req: Request) {
|
||||
return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/)
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// This is needed if you're planning to invoke your function from a browser.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
// `withSupabase` handles CORS and preflight requests for you.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
const { token } = await req.json()
|
||||
const clientIps = ips(req) || ['']
|
||||
const ip = clientIps[0]
|
||||
|
||||
const { token } = await req.json()
|
||||
const clientIps = ips(req) || ['']
|
||||
const ip = clientIps[0]
|
||||
// Validate the token by calling the
|
||||
// "/siteverify" API endpoint.
|
||||
let formData = new FormData()
|
||||
formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '')
|
||||
formData.append('response', token)
|
||||
formData.append('remoteip', ip)
|
||||
|
||||
// Validate the token by calling the
|
||||
// "/siteverify" API endpoint.
|
||||
let formData = new FormData()
|
||||
formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '')
|
||||
formData.append('response', token)
|
||||
formData.append('remoteip', ip)
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const outcome = await result.json()
|
||||
console.log(outcome)
|
||||
if (outcome.success) {
|
||||
return new Response('success', { headers: corsHeaders })
|
||||
}
|
||||
return new Response('failure', { headers: corsHeaders })
|
||||
})
|
||||
const outcome = await result.json()
|
||||
console.log(outcome)
|
||||
if (outcome.success) {
|
||||
return new Response('success')
|
||||
}
|
||||
return new Response('failure')
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Deploy the server-side validation Edge Functions
|
||||
@@ -68,7 +66,7 @@ Deno.serve(async (req) => {
|
||||
- https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
|
||||
|
||||
```bash
|
||||
supabase functions deploy cloudflare-turnstile
|
||||
supabase functions deploy cloudflare-turnstile --no-verify-jwt
|
||||
supabase secrets set CLOUDFLARE_SECRET_KEY=your_secret_key
|
||||
```
|
||||
|
||||
|
||||
@@ -100,95 +100,91 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the
|
||||
```ts supabase/functions/text-to-speech/index.ts
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
|
||||
import * as hash from 'npm:object-hash'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
|
||||
// If you want to use a different api key, change 'default' to your preferred key name
|
||||
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default'])
|
||||
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
|
||||
})
|
||||
|
||||
// Upload audio to Supabase Storage in a background task
|
||||
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('audio')
|
||||
.upload(`${requestHash}.mp3`, stream, {
|
||||
contentType: 'audio/mp3',
|
||||
})
|
||||
// Deploy with verify_jwt = false
|
||||
// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
// Upload audio to Supabase Storage in a background task
|
||||
async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
|
||||
const { data, error } = await ctx.supabaseAdmin.storage
|
||||
.from('audio')
|
||||
.upload(`${requestHash}.mp3`, stream, {
|
||||
contentType: 'audio/mp3',
|
||||
})
|
||||
|
||||
console.log('Storage upload result', { data, error })
|
||||
console.log('Storage upload result', { data, error })
|
||||
}
|
||||
|
||||
// To secure your function for production, you can for example validate the request origin,
|
||||
// or append a user access token and validate it with Supabase Auth.
|
||||
console.log('Request origin', req.headers.get('host'))
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
|
||||
|
||||
const requestHash = hash.MD5({ text, voiceId })
|
||||
console.log('Request hash', requestHash)
|
||||
|
||||
// Check storage for existing audio file
|
||||
const { data } = await ctx.supabaseAdmin.storage
|
||||
.from('audio')
|
||||
.createSignedUrl(`${requestHash}.mp3`, 60)
|
||||
|
||||
if (data) {
|
||||
console.log('Audio file found in storage', data)
|
||||
const storageRes = await fetch(data.signedUrl)
|
||||
if (storageRes.ok) return storageRes
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return Response.json({ error: 'Text parameter is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('ElevenLabs API call')
|
||||
const response = await client.textToSpeech.convertAsStream(voiceId, {
|
||||
output_format: 'mp3_44100_128',
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
text,
|
||||
})
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of response) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
|
||||
// Upload to Supabase Storage in the background
|
||||
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
|
||||
|
||||
// Return the streaming response immediately
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// To secure your function for production, you can for example validate the request origin,
|
||||
// or append a user access token and validate it with Supabase Auth.
|
||||
console.log('Request origin', req.headers.get('host'))
|
||||
const url = new URL(req.url)
|
||||
const params = new URLSearchParams(url.search)
|
||||
const text = params.get('text')
|
||||
const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
|
||||
|
||||
const requestHash = hash.MD5({ text, voiceId })
|
||||
console.log('Request hash', requestHash)
|
||||
|
||||
// Check storage for existing audio file
|
||||
const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60)
|
||||
|
||||
if (data) {
|
||||
console.log('Audio file found in storage', data)
|
||||
const storageRes = await fetch(data.signedUrl)
|
||||
if (storageRes.ok) return storageRes
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('ElevenLabs API call')
|
||||
const response = await client.textToSpeech.convertAsStream(voiceId, {
|
||||
output_format: 'mp3_44100_128',
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
text,
|
||||
})
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of response) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
// Branch stream to Supabase Storage
|
||||
const [browserStream, storageStream] = stream.tee()
|
||||
|
||||
// Upload to Supabase Storage in the background
|
||||
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
|
||||
|
||||
// Return the streaming response immediately
|
||||
return new Response(browserStream, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('error', { error })
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
@@ -108,8 +108,11 @@ In your newly created `scribe-bot/index.ts` file, add the following code:
|
||||
|
||||
```ts supabase/functions/scribe-bot/index.ts
|
||||
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts'
|
||||
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5'
|
||||
|
||||
console.log(`Function "elevenlabs-scribe-bot" up and running!`)
|
||||
@@ -117,13 +120,9 @@ console.log(`Function "elevenlabs-scribe-bot" up and running!`)
|
||||
const elevenLabsClient = new ElevenLabsClient({
|
||||
apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '',
|
||||
})
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') || '',
|
||||
SUPABASE_SECRET_KEYS['default'] || ''
|
||||
)
|
||||
|
||||
async function scribe({
|
||||
supabaseAdmin,
|
||||
fileURL,
|
||||
fileType,
|
||||
duration,
|
||||
@@ -131,6 +130,7 @@ async function scribe({
|
||||
messageId,
|
||||
username,
|
||||
}: {
|
||||
supabaseAdmin: SupabaseClient
|
||||
fileURL: string
|
||||
fileType: string
|
||||
duration: number
|
||||
@@ -178,9 +178,13 @@ async function scribe({
|
||||
error: errorMsg,
|
||||
}
|
||||
console.log({ logLine })
|
||||
await supabase.from('transcription_logs').insert({ ...logLine, transcript })
|
||||
await supabaseAdmin.from('transcription_logs').insert({ ...logLine, transcript })
|
||||
}
|
||||
|
||||
// Set by the request handler before delegating to grammY, so bot handlers
|
||||
// can write transcription logs with the admin client.
|
||||
let supabaseAdmin: SupabaseClient
|
||||
|
||||
const telegramBotToken = Deno.env.get('TELEGRAM_BOT_TOKEN')
|
||||
const bot = new Bot(telegramBotToken || '')
|
||||
const startMessage = `Welcome to the ElevenLabs Scribe Bot\\! I can transcribe speech in 99 languages with super high accuracy\\!
|
||||
@@ -202,6 +206,7 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => {
|
||||
// Run the transcription in the background.
|
||||
EdgeRuntime.waitUntil(
|
||||
scribe({
|
||||
supabaseAdmin,
|
||||
fileURL,
|
||||
fileType: fileMeta.mime_type!,
|
||||
duration: fileMeta.duration,
|
||||
@@ -223,18 +228,24 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => {
|
||||
|
||||
const handleUpdate = webhookCallback(bot, 'std/http')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
// Deploy with verify_jwt = false
|
||||
// The bot is called by Telegram, so we verify the request with FUNCTION_SECRET in code.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
}
|
||||
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
supabaseAdmin = ctx.supabaseAdmin
|
||||
|
||||
return await handleUpdate(req)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Deploy to Supabase
|
||||
|
||||
@@ -21,8 +21,8 @@ Generate Open Graph images with Deno and Supabase Edge Functions. [View on GitHu
|
||||
Create a `handler.tsx` file to construct the OG image in React:
|
||||
|
||||
```tsx handler.tsx
|
||||
import React from 'https://esm.sh/react@18.2.0'
|
||||
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
import React from 'https://esm.sh/react@18.2.0'
|
||||
|
||||
export default function handler(req: Request) {
|
||||
return new ImageResponse(
|
||||
@@ -46,9 +46,12 @@ export default function handler(req: Request) {
|
||||
Create an `index.ts` file to execute the handler on incoming requests:
|
||||
|
||||
```ts index.ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import handler from './handler.tsx'
|
||||
|
||||
console.log('Hello from og-image Function!')
|
||||
|
||||
Deno.serve(handler)
|
||||
// Public image endpoint, so deploy with --no-verify-jwt.
|
||||
export default { fetch: withSupabase({ auth: 'none' }, handler) }
|
||||
```
|
||||
@@ -53,7 +53,7 @@ Push notifications are an important part of any mobile app. They allow you to se
|
||||
1. `supabase secrets set --env-file .env.local`
|
||||
|
||||
```ts supabase/functions/push/index.ts
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log('Hello from Functions!')
|
||||
|
||||
@@ -71,39 +71,33 @@ Push notifications are an important part of any mobile app. They allow you to se
|
||||
old_record: null | Notification
|
||||
}
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
// Triggered by a Database Webhook, which authenticates with a secret key.
|
||||
// Deploy with `verify_jwt = false`.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { data } = await ctx.supabaseAdmin
|
||||
.from('profiles')
|
||||
.select('expo_push_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
|
||||
// If you want to use a different api key, change 'default' to your preferred key name
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
SUPABASE_SECRET_KEYS['default']
|
||||
)
|
||||
const res = await fetch('https://exp.host/--/api/v2/push/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: data?.expo_push_token,
|
||||
sound: 'default',
|
||||
body: payload.record.body,
|
||||
}),
|
||||
}).then((res) => res.json())
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('expo_push_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
|
||||
const res = await fetch('https://exp.host/--/api/v2/push/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: data?.expo_push_token,
|
||||
sound: 'default',
|
||||
body: payload.record.body,
|
||||
}),
|
||||
}).then((res) => res.json())
|
||||
|
||||
return new Response(JSON.stringify(res), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
return Response.json(res)
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Create the database webhook
|
||||
@@ -169,7 +163,7 @@ Push notifications are an important part of any mobile app. They allow you to se
|
||||
Add the following code to `supabase/functions/push/index.ts`:
|
||||
|
||||
```ts supabase/functions/push/index.ts
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { JWT } from 'npm:google-auth-library@9'
|
||||
import serviceAccount from '../service-account.json' with { type: 'json' }
|
||||
|
||||
@@ -186,59 +180,53 @@ Push notifications are an important part of any mobile app. They allow you to se
|
||||
schema: 'public'
|
||||
}
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
// Triggered by a Database Webhook, which authenticates with a secret key.
|
||||
// Deploy with `verify_jwt = false`.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
|
||||
// If you want to use a different api key, change 'default' to your preferred key name
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
SUPABASE_SECRET_KEYS['default']
|
||||
)
|
||||
const { data } = await ctx.supabaseAdmin
|
||||
.from('profiles')
|
||||
.select('fcm_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const fcmToken = data!.fcm_token as string
|
||||
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('fcm_token')
|
||||
.eq('id', payload.record.user_id)
|
||||
.single()
|
||||
const accessToken = await getAccessToken({
|
||||
clientEmail: serviceAccount.client_email,
|
||||
privateKey: serviceAccount.private_key,
|
||||
})
|
||||
|
||||
const fcmToken = data!.fcm_token as string
|
||||
|
||||
const accessToken = await getAccessToken({
|
||||
clientEmail: serviceAccount.client_email,
|
||||
privateKey: serviceAccount.private_key,
|
||||
})
|
||||
|
||||
const res = await fetch(
|
||||
`https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
token: fcmToken,
|
||||
notification: {
|
||||
title: `Notification from Supabase`,
|
||||
body: payload.record.body,
|
||||
},
|
||||
const res = await fetch(
|
||||
`https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
token: fcmToken,
|
||||
notification: {
|
||||
title: `Notification from Supabase`,
|
||||
body: payload.record.body,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const resData = await res.json()
|
||||
if (res.status < 200 || 299 < res.status) {
|
||||
throw resData
|
||||
}
|
||||
)
|
||||
|
||||
const resData = await res.json()
|
||||
if (res.status < 200 || 299 < res.status) {
|
||||
throw resData
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(resData), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
return Response.json(resData)
|
||||
}),
|
||||
}
|
||||
|
||||
const getAccessToken = ({
|
||||
clientEmail,
|
||||
|
||||
@@ -37,28 +37,34 @@ create index on embeddings using hnsw (embedding vector_ip_ops);
|
||||
|
||||
You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table:
|
||||
|
||||
```tsx
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { content, id } = payload.record
|
||||
// Triggered by a Database Webhook, which authenticates with a secret key.
|
||||
// Deploy with `verify_jwt = false`.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { content, id } = payload.record
|
||||
|
||||
// Generate embedding.
|
||||
const embedding = await model.run(content, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
// Generate embedding.
|
||||
const embedding = await model.run(content, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Store in database.
|
||||
const { error } = await supabase
|
||||
.from('embeddings')
|
||||
.update({ embedding: JSON.stringify(embedding) })
|
||||
.eq('id', id)
|
||||
if (error) console.warn(error.message)
|
||||
// Store in database.
|
||||
const { error } = await ctx.supabaseAdmin
|
||||
.from('embeddings')
|
||||
.update({ embedding: JSON.stringify(embedding) })
|
||||
.eq('id', id)
|
||||
if (error) console.warn(error.message)
|
||||
|
||||
return new Response('ok')
|
||||
})
|
||||
return new Response('ok')
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Create a Database Function and RPC
|
||||
@@ -98,32 +104,36 @@ $$;
|
||||
|
||||
You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts):
|
||||
|
||||
```tsx
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) return new Response('Please provide a search param!')
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Query embeddings.
|
||||
const { data: result, error } = await supabase
|
||||
.rpc('query_embeddings', {
|
||||
embedding,
|
||||
match_threshold: 0.8,
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) return new Response('Please provide a search param!')
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
})
|
||||
// Query embeddings.
|
||||
const { data: result, error } = await ctx.supabase
|
||||
.rpc('query_embeddings', {
|
||||
embedding,
|
||||
match_threshold: 0.8,
|
||||
})
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
You now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions!
|
||||
@@ -30,6 +30,8 @@ Store the `RESEND_API_KEY` in your `.env` file.
|
||||
Paste the following code into the `index.ts` file:
|
||||
|
||||
```tsx
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
|
||||
|
||||
const handler = async (_request: Request): Promise<Response> => {
|
||||
@@ -49,15 +51,10 @@ const handler = async (_request: Request): Promise<Response> => {
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
Deno.serve(handler)
|
||||
export default { fetch: withSupabase({ auth: ['user', 'secret'] }, handler) }
|
||||
```
|
||||
|
||||
### 3. Deploy and send email
|
||||
@@ -69,7 +66,12 @@ supabase start
|
||||
supabase functions serve --no-verify-jwt --env-file .env
|
||||
```
|
||||
|
||||
Test it: http://localhost:54321/functions/v1/resend
|
||||
The function accepts a signed-in user's JWT or a secret key, so it can be triggered from your app (via `supabase.functions.invoke`) or from a database function. Test it locally with a secret key:
|
||||
|
||||
```bash
|
||||
curl -i --request POST 'http://localhost:54321/functions/v1/resend' \
|
||||
--header 'apikey: <SUPABASE_SECRET_KEY>'
|
||||
```
|
||||
|
||||
Deploy function to Supabase:
|
||||
|
||||
@@ -83,8 +85,6 @@ When you deploy to Supabase, make sure that your `RESEND_API_KEY` is set in [Edg
|
||||
|
||||
</Admonition>
|
||||
|
||||
Open the endpoint URL to send an email:
|
||||
|
||||
### 4. Try it yourself
|
||||
|
||||
Find the complete example on [GitHub](https://github.com/resendlabs/resend-supabase-edge-functions-example).
|
||||
@@ -24,6 +24,7 @@ Handle exceptions within your function and send them to Sentry.
|
||||
|
||||
```tsx
|
||||
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Sentry.init({
|
||||
// https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn
|
||||
@@ -39,25 +40,25 @@ Sentry.init({
|
||||
Sentry.setTag('region', Deno.env.get('SB_REGION'))
|
||||
Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID'))
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
// This will throw, as `name` in our example call will be `undefined`
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
const { name } = await req.json()
|
||||
// This will throw, as `name` in our example call will be `undefined`
|
||||
const data = {
|
||||
message: `Hello ${name}!`,
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
// Flush Sentry before the running process closes
|
||||
await Sentry.flush(2000)
|
||||
return new Response(JSON.stringify({ msg: 'error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
return Response.json(data)
|
||||
} catch (e) {
|
||||
Sentry.captureException(e)
|
||||
// Flush Sentry before the running process closes
|
||||
await Sentry.flush(2000)
|
||||
return Response.json({ msg: 'error' }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Deploy and test
|
||||
|
||||
@@ -28,37 +28,38 @@ Here's the code of the Edge Function, you can change the response to handle the
|
||||
|
||||
```ts index.ts
|
||||
import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? ''
|
||||
const botClient = new WebClient(slackBotToken)
|
||||
|
||||
console.log(`Slack URL verification function up and running!`)
|
||||
Deno.serve(async (req) => {
|
||||
try {
|
||||
const reqBody = await req.json()
|
||||
console.log(JSON.stringify(reqBody, null, 2))
|
||||
const { token, challenge, type, event } = reqBody
|
||||
|
||||
if (type == 'url_verification') {
|
||||
return new Response(JSON.stringify({ challenge }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
} else if (event.type == 'app_mention') {
|
||||
const { user, text, channel, ts } = event
|
||||
// Here you should process the text received and return a response:
|
||||
const response = await botClient.chat.postMessage({
|
||||
channel: channel,
|
||||
text: `Hello <@${user}>!`,
|
||||
thread_ts: ts,
|
||||
})
|
||||
return new Response('ok', { status: 200 })
|
||||
// Slack calls this endpoint, so deploy with --no-verify-jwt.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
try {
|
||||
// Implement your Slack request signature verification here before trusting the payload
|
||||
// (validate `x-slack-signature` / `x-slack-request-timestamp` with your signing secret).
|
||||
const reqBody = await req.json()
|
||||
console.log(JSON.stringify(reqBody, null, 2))
|
||||
const { token, challenge, type, event } = reqBody
|
||||
|
||||
if (type == 'url_verification') {
|
||||
return Response.json({ challenge })
|
||||
} else if (event.type == 'app_mention') {
|
||||
const { user, text, channel, ts } = event
|
||||
// Here you should process the text received and return a response:
|
||||
const response = await botClient.chat.postMessage({
|
||||
channel: channel,
|
||||
text: `Hello <@${user}>!`,
|
||||
thread_ts: ts,
|
||||
})
|
||||
return new Response('ok', { status: 200 })
|
||||
}
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -40,39 +40,43 @@ And add the code to the `index.ts` file:
|
||||
|
||||
```ts index.ts
|
||||
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "upstash-redis-counter" up and running!`)
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
try {
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
|
||||
const deno_region = Deno.env.get('DENO_REGION')
|
||||
if (deno_region) {
|
||||
// Increment region counter
|
||||
await redis.hincrby('supa-edge-counter', deno_region, 1)
|
||||
} else {
|
||||
// Increment localhost counter
|
||||
await redis.hincrby('supa-edge-counter', 'localhost', 1)
|
||||
}
|
||||
|
||||
// Get all values
|
||||
const counterHash: Record<string, number> | null = await redis.hgetall('supa-edge-counter')
|
||||
const counters = Object.entries(counterHash!)
|
||||
.sort(([, a], [, b]) => b - a) // sort desc
|
||||
.reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), {
|
||||
total: 0,
|
||||
regions: {},
|
||||
// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
|
||||
try {
|
||||
const redis = new Redis({
|
||||
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
|
||||
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
|
||||
})
|
||||
|
||||
return new Response(JSON.stringify({ counters }), { status: 200 })
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), { status: 200 })
|
||||
}
|
||||
})
|
||||
const deno_region = Deno.env.get('DENO_REGION')
|
||||
if (deno_region) {
|
||||
// Increment region counter
|
||||
await redis.hincrby('supa-edge-counter', deno_region, 1)
|
||||
} else {
|
||||
// Increment localhost counter
|
||||
await redis.hincrby('supa-edge-counter', 'localhost', 1)
|
||||
}
|
||||
|
||||
// Get all values
|
||||
const counterHash: Record<string, number> | null = await redis.hgetall('supa-edge-counter')
|
||||
const counters = Object.entries(counterHash!)
|
||||
.sort(([, a], [, b]) => b - a) // sort desc
|
||||
.reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), {
|
||||
total: 0,
|
||||
regions: {},
|
||||
})
|
||||
|
||||
return Response.json({ counters })
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
@@ -198,6 +198,7 @@ import {
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
} from 'https://esm.sh/kysely@0.23.4'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { PostgresDriver } from './DenoPostgresDriver.ts'
|
||||
|
||||
@@ -245,31 +246,36 @@ const db = new Kysely<Database>({
|
||||
},
|
||||
})
|
||||
|
||||
Deno.serve(async (_req) => {
|
||||
try {
|
||||
// Run a query
|
||||
const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
|
||||
try {
|
||||
// Run a query
|
||||
const animals = await db
|
||||
.selectFrom('animals')
|
||||
.select(['id', 'animal', 'created_at'])
|
||||
.execute()
|
||||
|
||||
// Neat, it's properly typed \o/
|
||||
console.log(animals[0].created_at.getFullYear())
|
||||
// Neat, it's properly typed \o/
|
||||
console.log(animals[0].created_at.getFullYear())
|
||||
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
)
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
)
|
||||
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
})
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -186,14 +186,14 @@ console.log(data) // { message: "Hello JavaScript!" }
|
||||
const response = await fetch('https://[YOUR_PROJECT_ID].supabase.co/functions/v1/hello-world', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer YOUR_PUBLISHABLE_KEY',
|
||||
apikey: '<SUPABASE_PUBLISHABLE_KEY>',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name: 'Fetch' }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
console.log(data) // { message: "Hello Fetch!" }
|
||||
console.log(data) // { message: "Hello there!" }
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -97,7 +97,7 @@ Deno.serve(async (req) => {
|
||||
const response = await fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/other-function`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${SUPABASE_DEFAULT_PUBLISHABLE_KEY}`,
|
||||
apikey: SUPABASE_DEFAULT_PUBLISHABLE_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ foo: 'bar' }),
|
||||
|
||||
@@ -39,16 +39,20 @@ Here's a simple hello world example using some popular web frameworks:
|
||||
<TabPanel id="deno" label="Deno">
|
||||
|
||||
```ts
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'GET') {
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
if (req.method === 'GET') {
|
||||
return new Response('Hello World!')
|
||||
}
|
||||
const { name } = await req.json()
|
||||
if (name) {
|
||||
return new Response(`Hello ${name}!`)
|
||||
}
|
||||
return new Response('Hello World!')
|
||||
}
|
||||
const { name } = await req.json()
|
||||
if (name) {
|
||||
return new Response(`Hello ${name}!`)
|
||||
}
|
||||
return new Response('Hello World!')
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -123,9 +127,11 @@ app.get('/hello-world', (c) => {
|
||||
return new Response('Hello World!')
|
||||
})
|
||||
|
||||
Deno.serve(app.fetch)
|
||||
export default { fetch: app.fetch }
|
||||
```
|
||||
|
||||
To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
@@ -155,6 +161,8 @@ Keep in mind paths must be prefixed by function name. Route parameters can only
|
||||
<TabPanel id="deno" label="Deno">
|
||||
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
name: string
|
||||
@@ -204,42 +212,44 @@ async function deleteTask(id: string): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const url = new URL(req.url)
|
||||
const method = req.method
|
||||
// Extract the last part of the path as the command
|
||||
const command = url.pathname.split('/').pop()
|
||||
// Assuming the last part of the path is the task ID
|
||||
const id = command
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
if (id) {
|
||||
return getTask(id)
|
||||
} else {
|
||||
return getAllTasks()
|
||||
}
|
||||
case 'POST':
|
||||
return createTask(req)
|
||||
case 'PUT':
|
||||
if (id) {
|
||||
return updateTask(id, req)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
}
|
||||
case 'DELETE':
|
||||
if (id) {
|
||||
return deleteTask(id)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
}
|
||||
default:
|
||||
return new Response('Method Not Allowed', { status: 405 })
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
const url = new URL(req.url)
|
||||
const method = req.method
|
||||
// Extract the last part of the path as the command
|
||||
const command = url.pathname.split('/').pop()
|
||||
// Assuming the last part of the path is the task ID
|
||||
const id = command
|
||||
try {
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
if (id) {
|
||||
return getTask(id)
|
||||
} else {
|
||||
return getAllTasks()
|
||||
}
|
||||
case 'POST':
|
||||
return createTask(req)
|
||||
case 'PUT':
|
||||
if (id) {
|
||||
return updateTask(id, req)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
}
|
||||
case 'DELETE':
|
||||
if (id) {
|
||||
return deleteTask(id)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
}
|
||||
default:
|
||||
return new Response('Method Not Allowed', { status: 405 })
|
||||
}
|
||||
} catch (error) {
|
||||
return new Response(`Internal Server Error: ${error}`, { status: 500 })
|
||||
}
|
||||
} catch (error) {
|
||||
return new Response(`Internal Server Error: ${error}`, { status: 500 })
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -388,9 +398,11 @@ app.delete('/:id', async (c) => {
|
||||
}
|
||||
})
|
||||
|
||||
Deno.serve(app.fetch)
|
||||
export default { fetch: app.fetch }
|
||||
```
|
||||
|
||||
To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
|
||||
|
||||
</TabPanel>
|
||||
|
||||
</Tabs>
|
||||
@@ -407,6 +419,6 @@ This works well for small apps with only a couple of routes:
|
||||
|
||||
<$CodeSample
|
||||
path="/edge-functions/supabase/functions/restful-tasks/index.ts"
|
||||
lines={[[92, 116]]}
|
||||
lines={[[48, -1]]}
|
||||
meta="restful-tasks/index.ts"
|
||||
/>
|
||||
@@ -47,7 +47,7 @@ select
|
||||
url:= (select decrypted_secret from vault.decrypted_secrets where name = 'project_url') || '/functions/v1/function-name',
|
||||
headers:=jsonb_build_object(
|
||||
'Content-type', 'application/json',
|
||||
'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'publishable_key')
|
||||
'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'publishable_key')
|
||||
),
|
||||
body:=concat('{"time": "', now(), '"}')::jsonb
|
||||
) as request_id;
|
||||
|
||||
@@ -31,26 +31,28 @@ Here are some basic examples of setting up WebSocket servers using Deno and Node
|
||||
<TabPanel id="deno" label="Deno">
|
||||
|
||||
```ts
|
||||
Deno.serve((req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
export default {
|
||||
fetch: (req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
|
||||
return response
|
||||
})
|
||||
return response
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -123,7 +125,7 @@ WebSocket browser clients don't have the option to send custom headers. Because
|
||||
|
||||
You can skip the default authorization header checks by explicitly providing `--no-verify-jwt` when serving and deploying functions.
|
||||
|
||||
To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol.
|
||||
To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol. The [`withSupabase`](/docs/guides/functions/auth) wrapper validates credentials on request headers, so it can't authenticate WebSocket clients. Verify the JWT yourself, as shown below.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -144,46 +146,48 @@ const supabase = createClient(
|
||||
SUPABASE_SECRET_KEYS['default']
|
||||
)
|
||||
|
||||
Deno.serve((req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'WebSocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
export default {
|
||||
fetch: async (req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
|
||||
// Please be aware query params may be logged in some logging systems.
|
||||
const url = new URL(req.url)
|
||||
const jwt = url.searchParams.get('jwt')
|
||||
// Please be aware query params may be logged in some logging systems.
|
||||
const url = new URL(req.url)
|
||||
const jwt = url.searchParams.get('jwt')
|
||||
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
}
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
}
|
||||
|
||||
const { error, data } = await supabase.auth.getClaims()
|
||||
const { error, data } = await supabase.auth.getUser(jwt)
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return new Response('Invalid token provided', { status: 403 })
|
||||
}
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return new Response('Invalid token provided', { status: 403 })
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
}
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
|
||||
return response
|
||||
})
|
||||
return response
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -199,47 +203,50 @@ const supabase = createClient(
|
||||
SUPABASE_SECRET_KEYS['default']
|
||||
)
|
||||
|
||||
Deno.serve((req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'WebSocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
export default {
|
||||
fetch: async (req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
}
|
||||
|
||||
// Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2`
|
||||
const customProtocols = (req.headers.get('Sec-WebSocket-Protocol') ?? '')
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
const jwt = customProtocols.find((p) => p.startsWith('jwt')).replace('jwt-', '')
|
||||
// Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2`
|
||||
const customProtocols = (req.headers.get('Sec-WebSocket-Protocol') ?? '')
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
const jwtProtocol = customProtocols.find((p) => p.startsWith('jwt-'))
|
||||
const jwt = jwtProtocol ? jwtProtocol.replace('jwt-', '') : null
|
||||
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
}
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
}
|
||||
|
||||
const { error, data } = await supabase.auth.getClaims()
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return new Response('Invalid token provided', { status: 403 })
|
||||
}
|
||||
const { error, data } = await supabase.auth.getUser(jwt)
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return new Response('Invalid token provided', { status: 403 })
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
}
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
socket.onopen = () => console.log('socket opened')
|
||||
socket.onmessage = (e) => {
|
||||
console.log('socket message:', e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
socket.onerror = (e) => console.log('socket errored:', e.message)
|
||||
socket.onclose = () => console.log('socket closed')
|
||||
|
||||
return response
|
||||
})
|
||||
return response
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
|
||||
@@ -10,7 +10,7 @@ hideToc: true
|
||||
<div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 not-prose">
|
||||
<Link href="/guides/ai" className="col-span-12 md:col-span-4" passHref>
|
||||
<Link href="/guides/ai-tools" className="col-span-12 md:col-span-4" passHref>
|
||||
<GlassPanel
|
||||
title="Build with AI tools"
|
||||
hasLightIcon={true}
|
||||
@@ -25,7 +25,7 @@ hideToc: true
|
||||
Learn about the different API keys in Supabase and how to use them.
|
||||
</GlassPanel>
|
||||
</Link>
|
||||
<Link href="/guides/cli/getting-started" className="col-span-12 md:col-span-4" passHref>
|
||||
<Link href="/guides/local-development" className="col-span-12 md:col-span-4" passHref>
|
||||
<GlassPanel title="Local Development" hasLightIcon={true} background={false} showIconBg={true}>
|
||||
Use the Supabase CLI to develop locally and collaborate between teams.
|
||||
</GlassPanel>
|
||||
|
||||
@@ -34,7 +34,7 @@ Encrypt sensitive data and store secrets using our Postgres extension, Supabase
|
||||
|
||||
### Replication
|
||||
|
||||
Automatically replicate your database to external destinations like data warehouses and analytics platforms. Read [the external replication documentation](/docs/guides/database/replication/external-replication-setup) for more details.
|
||||
Automatically replicate your database to destination systems like data warehouses and analytics platforms with external replication (ETL), powered by Supabase ETL. Read [the documentation](/docs/guides/database/replication/external-replication-setup) for more details.
|
||||
|
||||
## Platform
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ hideToc: true
|
||||
|
||||
await Supabase.initialize(
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY',
|
||||
publishableKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY',
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ hideToc: true
|
||||
<StepHikeCompact.Details title="Update seed script">
|
||||
Let's seed the database with a few instruments.
|
||||
|
||||
Update the file `scripts/seeds.ts` to contain the following code:
|
||||
Update the file `scripts/seed.ts` to contain the following code:
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
@@ -67,7 +67,7 @@ Add `CFBundleURLTypes` to enable deep linking:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```xml name=ios/Runner/Info.plist"
|
||||
```xml name=ios/Runner/Info.plist
|
||||
<!-- ... other tags -->
|
||||
<plist>
|
||||
<dict>
|
||||
@@ -314,7 +314,7 @@ Let's create a new widget called `account_page.dart` for that.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```dart name=lib/pages/account_page.dart"
|
||||
```dart name=lib/pages/account_page.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/main.dart';
|
||||
|
||||
@@ -160,7 +160,7 @@ curl 'https://api.supabase.com/v1/projects/{ref}/branches' \
|
||||
--data '{
|
||||
"branch_name": "DEV",
|
||||
"secrets": {
|
||||
"STRIPE_SECRET_KEY":"sk_test_123..."
|
||||
"STRIPE_SECRET_KEY":"sk_test_123...",
|
||||
"STRIPE_PUBLISHABLE_KEY":"pk_test_123..."
|
||||
}
|
||||
}'
|
||||
|
||||
@@ -72,28 +72,12 @@ 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>
|
||||
|
||||
@@ -157,7 +141,7 @@ npx supabase@beta --help
|
||||
|
||||
## Updating the Supabase CLI
|
||||
|
||||
When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods.
|
||||
When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same channels.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -248,46 +232,7 @@ supabase stop --no-backup
|
||||
|
||||
## Running Supabase locally
|
||||
|
||||
The Supabase CLI uses Docker containers to manage the local development stack. Follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop):
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="macos"
|
||||
queryGroup="platform"
|
||||
>
|
||||
<TabPanel id="macos" label="macOS">
|
||||
|
||||
<Image
|
||||
alt="Docker settings on Mac: Select Integrated, Virtualization Framework, and osxfs"
|
||||
src={{
|
||||
dark: '/docs/img/guides/cli/docker-mac.png',
|
||||
light: '/docs/img/guides/cli/docker-mac-light.png',
|
||||
}}
|
||||
|
||||
width={2880}
|
||||
height={1800}
|
||||
/>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="windows" label="Windows">
|
||||
|
||||
<Image
|
||||
alt="Docker settings on Windows: Select Integrated, Expose Daemon, WSL2, and Add to /etc/hosts file."
|
||||
src={{
|
||||
dark: '/docs/img/guides/cli/docker-win.png',
|
||||
light: '/docs/img/guides/cli/docker-win-light.png',
|
||||
}}
|
||||
|
||||
width={2560}
|
||||
height={1520}
|
||||
/>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
The Supabase CLI uses Docker containers to manage the local development stack. Follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop) on your machine.
|
||||
|
||||
Alternately, you can use a different container tool that offers Docker compatible APIs.
|
||||
|
||||
@@ -296,15 +241,13 @@ Alternately, you can use a different container tool that offers Docker compatibl
|
||||
- [OrbStack](https://orbstack.dev/) (macOS)
|
||||
- [colima](https://github.com/abiosoft/colima) (macOS)
|
||||
|
||||
</Admonition>
|
||||
|
||||
Inside the folder where you want to create your project, run:
|
||||
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
|
||||
This will create a new `supabase` folder. It's safe to commit this folder to your version control system.
|
||||
This creates a new `supabase` folder. It's safe to commit this folder to version control.
|
||||
|
||||
Now, to start the Supabase stack, run:
|
||||
|
||||
@@ -312,11 +255,11 @@ Now, to start the Supabase stack, run:
|
||||
supabase start
|
||||
```
|
||||
|
||||
This takes time on your first run because the CLI needs to download the Docker images to your local machine. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool).
|
||||
This takes time on your first run because the CLI needs to download the Docker images to your local machine. The CLI includes the entire Supabase stack, and a few additional images useful for local development (like a local SMTP server and a database diff tool).
|
||||
|
||||
## Access your project's services
|
||||
|
||||
Once all of the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like this, with urls and keys that you'll use in your local project:
|
||||
Once all the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like the below, with urls and keys that you use in your local project:
|
||||
|
||||
```
|
||||
Started supabase local development setup.
|
||||
@@ -427,7 +370,7 @@ For advanced logs analysis using the Logs Explorer, it is advised to use the Big
|
||||
|
||||
</Admonition>
|
||||
|
||||
All logs will be stored in the local database under the `_analytics` schema.
|
||||
All logs are stored in the local database under the `_analytics` schema.
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Using the CLI to test your Supabase project.'
|
||||
subtitle: 'Using the CLI to test your Supabase project.'
|
||||
---
|
||||
|
||||
The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge` Functions.
|
||||
The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge Functions.
|
||||
|
||||
## Testing your database
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Database size is consumed primarily by your data, indexes, and materialized view
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Depending on your billing plan, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Disk Management](#disk-management) section.
|
||||
Depending on your billing plan, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Read-Only Mode](#read-only-mode) section.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ In such a scenario, you can consider:
|
||||
|
||||
### Configuring clients to use fewer connections
|
||||
|
||||
You can use the [pg_stat_activity](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW) view to debug which clients are holding open connections on your DB. `pg_stat_activity` only exposes information on direct connections to the database. Information on the number of connections to Supavisor is available [via the metrics endpoint](../platform/metrics).
|
||||
You can use the [pg_stat_activity](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW) view to debug which clients are holding open connections on your DB. `pg_stat_activity` only exposes information on direct connections to the database. Information on the number of connections to Supavisor is available [via the metrics endpoint](../telemetry/metrics).
|
||||
|
||||
Depending on the clients involved, you might be able to configure them to work with fewer connections (e.g. by imposing a limit on the maximum number of connections they're allowed to use), or shift specific workloads to connect via [Supavisor](/docs/guides/database/connecting-to-postgres#connection-pooler) instead. Transient workflows, which can quickly scale up and down in response to traffic (e.g. serverless functions), can especially benefit from using a connection pooler rather than connecting to the DB directly.
|
||||
|
||||
|
||||
@@ -178,9 +178,3 @@ The PrivateLink endpoint is a layer 3 solution so behaves like a standard Postgr
|
||||
Ready to enhance your database security with PrivateLink? [Contact our Enterprise team](/contact/enterprise) to discuss your requirements and begin the setup process.
|
||||
|
||||
Our support team will guide you through the configuration and ensure your private database connectivity meets your security and performance requirements.
|
||||
|
||||
## Regional availability
|
||||
|
||||
PrivateLink is not currently available in the following regions:
|
||||
|
||||
- **eu-central-2 (Zurich)** - Expected availability: April 2026
|
||||
@@ -143,7 +143,7 @@ When a Read Replica is deployed, it emits logs from the following services:
|
||||
- [PostgREST](/dashboard/project/_/logs/postgrest-logs)
|
||||
- [Supavisor](/dashboard/project/_/logs/pooler-logs)
|
||||
|
||||
Views on [Log Explorer](/docs/guides/platform/logs) are automatically filtered by databases, with the logs of the Primary database displayed by default. Viewing logs from other databases can be toggled with the `Source` button found on the upper-right part section of the Logs Explorer page.
|
||||
Views on [Log Explorer](/docs/guides/telemetry/logs) are automatically filtered by databases, with the logs of the Primary database displayed by default. Viewing logs from other databases can be toggled with the `Source` button found on the upper-right part section of the Logs Explorer page.
|
||||
|
||||
For API logs, logs can originate from the API Load Balancer as well. The upstream database or the one that eventually handles the request can be found under the `Redirect Identifier` field. This is equivalent to `metadata.load_balancer_redirect_identifier` when querying the underlying logs.
|
||||
|
||||
@@ -151,7 +151,7 @@ For API logs, logs can originate from the API Load Balancer as well. The upstrea
|
||||
|
||||
Observability and metrics for Read Replicas are available on the Supabase Dashboard. Resource utilization for a specific Read Replica can be viewed on the [Database Reports page](/dashboard/project/_/observability/database) by toggling for `Source`. Likewise, metrics on API requests going through either a Read Replica or Load Balancer API endpoint are also available on the dashboard through the [API Reports page](/dashboard/project/_/observability/api-overview)
|
||||
|
||||
We recommend ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment. If you have an existing ingestion pipeline set up for your project, you can [update it](https://github.com/supabase/supabase-grafana?tab=readme-ov-file#read-replica-support) to additionally ingest metrics from your Read Replicas.
|
||||
We recommend ingesting your [project's metrics](/docs/guides/telemetry/metrics) into your own environment. If you have an existing ingestion pipeline set up for your project, you can [update it](https://github.com/supabase/supabase-grafana?tab=readme-ov-file#read-replica-support) to additionally ingest metrics from your Read Replicas.
|
||||
|
||||
### Centralized configuration management
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ There is no single threshold to indicate when you should address replication lag
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
If you are already ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric.
|
||||
If you are already ingesting your [project's metrics](/docs/guides/telemetry/metrics) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -128,6 +128,12 @@ Get the Project URL and key from [the project's **Connect** dialog](/dashboard/p
|
||||
|
||||
You can receive Broadcast messages by providing a callback to the channel.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. On older SDK versions, binary messages are silently dropped and never reach the callback.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -255,6 +261,12 @@ You can receive Broadcast messages by providing a callback to the channel.
|
||||
|
||||
You can use the Supabase client libraries to send Broadcast messages.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Broadcast payloads can be binary (`ArrayBuffer` or `ArrayBufferView`, e.g. `Uint8Array`) over WebSocket from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. Binary payloads sent to clients running older SDK versions are **silently dropped** and never arrive over the WebSocket. The Dart, Kotlin, and Python clients don't support binary payloads yet.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -298,6 +310,16 @@ You can use the Supabase client libraries to send Broadcast messages.
|
||||
payload: { message: 'Hi' },
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The payload can be binary (ArrayBuffer / ArrayBufferView) from supabase-js 2.91.0.
|
||||
* Receivers on older SDK versions will not get the message.
|
||||
*/
|
||||
myChannel.send({
|
||||
type: 'broadcast',
|
||||
event: 'cursor-pos',
|
||||
payload: new Uint8Array([1, 2, 3]).buffer,
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -335,6 +357,12 @@ You can use the Supabase client libraries to send Broadcast messages.
|
||||
<$Show if="sdk:swift">
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Binary payloads over WebSocket are supported from supabase-swift 2.44.0. Receivers on older SDK versions will not get binary messages.
|
||||
|
||||
</Admonition>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```swift
|
||||
let myChannel = await supabase.channel("test-channel") {
|
||||
@@ -440,11 +468,31 @@ By default, all database broadcasts are private, meaning clients must authentica
|
||||
|
||||
</Admonition>
|
||||
|
||||
To broadcast a binary payload from your database, use the `realtime.send_binary()` function with a `bytea` payload:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
select
|
||||
realtime.send_binary(
|
||||
'\x012345'::bytea, -- bytea payload
|
||||
'event', -- Event name
|
||||
'topic', -- Topic
|
||||
true -- Private / Public flag (defaults to true)
|
||||
);
|
||||
```
|
||||
|
||||
The same public/private matching rule applies: a binary broadcast only reaches channels with the same private setting. Binary messages only reach clients on **supabase-js 2.91.0** and **supabase-swift 2.44.0** or later; older clients silently drop them.
|
||||
|
||||
You can use the `realtime.broadcast_changes()` helper function to broadcast messages when a record is created, updated, or deleted. For more details, read [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes).
|
||||
|
||||
### Broadcast using the REST API
|
||||
|
||||
You can send a Broadcast message by making an HTTP request to Realtime servers.
|
||||
You can send a single Broadcast message by making an HTTP request to Realtime servers. The endpoint embeds the topic and event in the path, and the `Content-Type` header determines the payload type:
|
||||
|
||||
- `application/json` — JSON payload
|
||||
- `application/octet-stream` — binary payload
|
||||
|
||||
Add `?private=true` to broadcast to a private channel.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -457,19 +505,19 @@ You can send a Broadcast message by making an HTTP request to Realtime servers.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```bash
|
||||
# JSON payload
|
||||
curl -v \
|
||||
-H 'apikey: <SUPABASE_TOKEN>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"messages": [
|
||||
{
|
||||
"topic": "test",
|
||||
"event": "event",
|
||||
"payload": { "test": "test" }
|
||||
}
|
||||
]
|
||||
}' \
|
||||
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast'
|
||||
--data-raw '{ "test": "test" }' \
|
||||
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast/test/events/event'
|
||||
|
||||
# Binary payload
|
||||
curl -v \
|
||||
-H 'apikey: <SUPABASE_TOKEN>' \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary @payload.bin \
|
||||
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast/test/events/event?private=true'
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -477,26 +525,39 @@ You can send a Broadcast message by making an HTTP request to Realtime servers.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```bash
|
||||
POST /realtime/v1/api/broadcast HTTP/1.1
|
||||
POST /realtime/v1/api/broadcast/test/events/event HTTP/1.1
|
||||
Host: {PROJECT_REF}.supabase.co
|
||||
Content-Type: application/json
|
||||
apikey: {SUPABASE_TOKEN}
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"topic": "test",
|
||||
"event": "event",
|
||||
"payload": {
|
||||
"test": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{ "test": "test" }
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
To send multiple messages in a single request, the batch endpoint `POST /realtime/v1/api/broadcast` is still available. It accepts a JSON body with a `messages` array (JSON payloads only):
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```bash
|
||||
curl -v \
|
||||
-H 'apikey: <SUPABASE_TOKEN>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"messages": [
|
||||
{
|
||||
"topic": "test",
|
||||
"event": "event",
|
||||
"payload": { "test": "test" }
|
||||
}
|
||||
]
|
||||
}' \
|
||||
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast'
|
||||
```
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Broadcast options
|
||||
|
||||
You can pass configuration options while initializing the Supabase Client.
|
||||
@@ -783,7 +844,7 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
<Admonition type="note">
|
||||
|
||||
This is currently available only in the Supabase JavaScript client version 2.37.0 and later.
|
||||
`channel.httpSend()` always uses the REST API regardless of WebSocket connection state, and is available from the Supabase JavaScript client version 2.107.0 and later. `ArrayBuffer` and `ArrayBufferView` (e.g. `Uint8Array`) payloads are sent as `application/octet-stream`; all other payloads are JSON-encoded.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -792,13 +853,11 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv
|
||||
|
||||
// No need to subscribe to channel
|
||||
|
||||
channel
|
||||
.send({
|
||||
type: 'broadcast',
|
||||
event: 'test',
|
||||
payload: { message: 'Hi' },
|
||||
})
|
||||
.then((resp) => console.log(resp))
|
||||
// JSON payload
|
||||
await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
|
||||
|
||||
// Binary payload (ArrayBuffer / ArrayBufferView) — sent as application/octet-stream
|
||||
await channel.httpSend('cursor-pos', new Uint8Array([1, 2, 3]).buffer)
|
||||
|
||||
// Remember to clean up the channel
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Upgrade your plan to increase your limits. Without a spend cap, or on an Enterpr
|
||||
| **Channels per connection** | 100 | 100 | 100 | 100 | 100+ |
|
||||
| **Presence keys per object** | 10 | 10 | 10 | 10 | 10+ |
|
||||
| **Presence messages per second** | 20 | 50 | 1,000 | 1,000 | 1,000+ |
|
||||
| **Presence calls per client, per 30 seconds** | 5 | 5 | 5 | 5 | 5 |
|
||||
| **Broadcast payload size** | 256 KB | 3,000 KB | 3,000 KB | 3,000 KB | 3,000+ KB |
|
||||
| **Postgres change payload size ([**read more**](#postgres-changes-payload-limit))** | 1,024 KB | 1,024 KB | 1,024 KB | 1,024 KB | 1,024+ KB |
|
||||
|
||||
|
||||
@@ -214,15 +214,17 @@ When new API keys have not been added yet, the `kong-entrypoint.sh` script remov
|
||||
|
||||
To assist with the authorization flows a specialized configuration in `kong.yml` substitutes internal, gateway-level-only pre-signed JWTs for `sb_publishable` and `sb_secret` API keys. These pre-signed JWTs are also auto-configured in `.env` but **should not** be used in any application code.
|
||||
|
||||
| Route | Service | API key required | Header substitution |
|
||||
| -------------------- | -------------------- | ---------------- | ------------------- |
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
| Route | Service | API key required | Header substitution |
|
||||
| -------------------------- | -------------------- | ---------------- | ------------------- |
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/tenants` | Realtime (REST) | Denied (blocked) | - |
|
||||
| `/realtime/v1/api/openapi` | Realtime (REST) | Denied (blocked) | - |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
|
||||
### Request flows
|
||||
|
||||
|
||||
@@ -60,13 +60,14 @@ HTTP filter chain
|
||||
├─ CORS
|
||||
├─ Basic Auth (dashboard only)
|
||||
├─ Lua: copy ?apikey query to header
|
||||
├─ Lua: 401 for missing/invalid API key
|
||||
├─ Lua: translate opaque keys in query
|
||||
├─ Lua: translate opaque keys in header
|
||||
├─ Lua: mirror apikey to x-api-key (Realtime WS)
|
||||
├─ Lua: synthesize Authorization header
|
||||
├─ Lua: 401 for missing/invalid API key
|
||||
├─ RBAC (global: service_role → /pg/, apikey → other API routes;
|
||||
│ per-route DENY override on /mcp)
|
||||
│ per-route DENY override on /mcp and Realtime
|
||||
│ /api/tenants, /api/openapi)
|
||||
└─ Router
|
||||
│
|
||||
▼
|
||||
@@ -140,7 +141,9 @@ Routes are matched in the order declared. The first matching prefix wins. Protec
|
||||
| `/auth/v1/` | auth | `/` | API key | Protected Auth endpoints |
|
||||
| `/rest/v1/` | rest | `/` | API key | PostgREST |
|
||||
| `/graphql/v1` | rest | `/rpc/graphql` | API key | pg_graphql (adds `Content-Profile: graphql_public`) |
|
||||
| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API |
|
||||
| `/realtime/v1/api/tenants` | realtime | - | Denied | Realtime management API (blocked by default) |
|
||||
| `/realtime/v1/api/openapi` | realtime | - | Denied | Realtime OpenAPI spec (blocked by default) |
|
||||
| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API (broadcast, channels, ping) |
|
||||
| `/realtime/v1/` | realtime | `/socket/` | API key | Realtime WebSocket |
|
||||
| `/pg/` | meta | `/` | Service role only | postgres-meta - used by Studio for database access |
|
||||
| `/api/mcp` | studio | - | Denied | MCP endpoint (blocked by default via RBAC DENY) |
|
||||
@@ -308,6 +311,7 @@ The access log format is a standard combined log with the request method, origin
|
||||
- **`401 Unauthorized` on a protected route.** The `apikey` header is missing or does not match any configured key. Verify that the header value exactly matches one of `ANON_KEY`, `SERVICE_ROLE_KEY`, `SUPABASE_PUBLISHABLE_KEY`, or `SUPABASE_SECRET_KEY` in your `.env` file. Note that `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` are only accepted when the new key configuration is fully set up - see [New API Keys and Asymmetric Authentication](/docs/guides/self-hosting/self-hosted-auth-keys).
|
||||
- **`403 Forbidden` on `/pg/`.** The `/pg/` route requires a service_role key (`SUPABASE_SECRET_KEY` or legacy `SERVICE_ROLE_KEY`). Anon and publishable keys are rejected.
|
||||
- **`403 Forbidden` on `/api/mcp` or `/mcp`.** These routes are blocked by default. See [Enabling MCP Server Access](/docs/guides/self-hosting/enable-mcp).
|
||||
- **`403 Forbidden` on `/realtime/v1/api/tenants` or `/realtime/v1/api/openapi`.** These Realtime management endpoints are blocked at the gateway by design and are not reachable by external clients, even with a valid key.
|
||||
- **`SignatureDoesNotMatch` on S3 requests to Storage.** Verify that the Storage service configuration in `docker-compose.yml` contains `REQUEST_ALLOW_X_FORWARDED_PATH=true` and `STORAGE_PUBLIC_URL`. Storage uses the `X-Forwarded-Prefix` header the gateway sends to reconstruct the original request path for SigV4 verification.
|
||||
- **`400 Bad Request` with underscore headers.** `headers_with_underscores_action: REJECT_REQUEST` is enabled. Some clients send headers like `X_Forwarded_For` with underscores; these are rejected. Use hyphens in header names.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Analytics buckets use [Apache Iceberg](https://iceberg.apache.org/), an open-tab
|
||||
|
||||
<Admonition type="note" title="About replication">
|
||||
|
||||
Analytics Buckets are still available, but managed replication into Analytics Buckets through Supabase ETL is no longer supported. If you need managed replication today, use [external replication](/docs/guides/database/replication/external-replication-setup) with **BigQuery**. If you want to use Analytics Buckets, bring your own ingestion pipeline.
|
||||
Analytics Buckets are still available, but managed replication into Analytics Buckets through Supabase ETL is no longer supported. If you need managed replication today, use [external replication (ETL)](/docs/guides/database/replication/external-replication-setup) with **BigQuery**. If you want to use Analytics Buckets, bring your own ingestion pipeline.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ PyIceberg is a Python client for Apache Iceberg that enables programmatic intera
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install pyiceberg pyarrow
|
||||
pip install "supabase[iceberg]"
|
||||
```
|
||||
|
||||
## Basic setup
|
||||
|
||||
@@ -9,7 +9,7 @@ This is made possible by the [Iceberg Foreign Data Wrapper](/docs/guides/databas
|
||||
|
||||
<Admonition type="note" title="About ingestion">
|
||||
|
||||
Managed replication into Analytics Buckets through Supabase ETL is no longer supported. This guide assumes your Analytics Bucket is being populated by your own ingestion pipeline.
|
||||
Managed replication into Analytics Buckets through Supabase ETL is no longer supported. This guide assumes your Analytics Bucket is being populated by your own ingestion pipeline. If you need managed replication today, use [external replication (ETL)](/docs/guides/database/replication/external-replication-setup) with **BigQuery**.
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'Learn how Supabase Storage caches objects with a CDN.'
|
||||
sidebar_label: 'CDN'
|
||||
---
|
||||
|
||||
Cache hits can be determined via the `metadata.response.headers.cf_cache_status` key in our [Logs Explorer](/docs/guides/platform/logs#logs-explorer). Any value that corresponds to either `HIT`, `STALE`, `REVALIDATED`, or `UPDATING` is categorized as a cache hit.
|
||||
Cache hits can be determined via the `metadata.response.headers.cf_cache_status` key in our [Logs Explorer](/docs/guides/telemetry/logs#logs-explorer). Any value that corresponds to either `HIT`, `STALE`, `REVALIDATED`, or `UPDATING` is categorized as a cache hit.
|
||||
The following example query will show the top cache misses from the `edge_logs`:
|
||||
|
||||
```sql
|
||||
|
||||
@@ -94,7 +94,7 @@ on storage.objects
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
(storage.folder(name))[1] = (select auth.uid())
|
||||
(storage.foldername(name))[1] = (select auth.uid())
|
||||
);
|
||||
|
||||
create policy "User can update their own objects (in any buckets)"
|
||||
|
||||
@@ -683,7 +683,13 @@ response = supabase.storage.from_('bucket').download(
|
||||
|
||||
## Self hosting
|
||||
|
||||
Our solution to image resizing and optimization can be self-hosted as with any other Supabase product. Under the hood we use [imgproxy](https://imgproxy.net/)
|
||||
Our solution to image resizing and optimization can be self-hosted as with any other Supabase product. Under the hood we use [imgproxy](https://imgproxy.net/).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you run the official self-hosted stack from the [`supabase/supabase`](https://github.com/supabase/supabase/tree/master/docker) Docker Compose setup, an `imgproxy` service is **already included** and wired up to the `storage` service for you (the `storage` service sets `ENABLE_IMAGE_TRANSFORMATION: "true"` and `IMGPROXY_URL: http://imgproxy:5001`). You only need the steps below if you run `storage-api` outside of that Compose stack and have to provide your own imgproxy container.
|
||||
|
||||
</Admonition>
|
||||
|
||||
#### imgproxy configuration:
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: 'Vector Bucket Local Development'
|
||||
subtitle: 'Develop and test vector bucket integrations in your local environment with the Supabase CLI.'
|
||||
---
|
||||
|
||||
<Admonition type="caution" title="This feature is in alpha">
|
||||
|
||||
Expect rapid changes, limited features, and possible breaking updates. [Share feedback](https://github.com/orgs/supabase/discussions/40116) as we refine the experience and expand access.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can now develop and test Vector Bucket integrations in your local environment using the Supabase CLI.
|
||||
|
||||
This allows you to build and iterate on your vector search applications without needing to deploy to a live environment.
|
||||
Make sure you have the latest version of the Supabase CLI installed to access this feature.
|
||||
|
||||
<Admonition type="note" title="Local driver">
|
||||
|
||||
In local development, vector buckets uses pg_vector as the underlying storage engine under the hood.
|
||||
The Hosted version uses S3Vectors as the Storage engine for vectors, which is optimized for large-scale vector storage and similarity search. This means that while you can develop and test your vector bucket integrations locally, there may be differences in performance and behavior compared to the cloud environment.
|
||||
|
||||
The API remain consistent between local and hosted environments, so you can build your application logic against the local pg_vector implementation and expect it to work with the S3Vectors engine in production.
|
||||
|
||||
</Admonition>
|
||||
## Setting up local vector buckets
|
||||
|
||||
Make sure you have the feature enabled in your `config.toml` file:
|
||||
|
||||
```toml
|
||||
|
||||
# Store vector embeddings in S3 for large and durable datasets
|
||||
[storage.vector]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
### Declarative configuration
|
||||
|
||||
You can define your vector buckets in the `config.toml` file using the following syntax:
|
||||
|
||||
```toml
|
||||
[storage.vector.buckets.documents-openai]
|
||||
[storage.vector.buckets.images]
|
||||
```
|
||||
|
||||
Then use `supabase seed buckets` to create the buckets in your local environment or linked project.
|
||||
@@ -184,7 +184,7 @@ regexp_contains(event_message, '^connection')
|
||||
|
||||
```sql
|
||||
-- find only messages that ends with port=12345
|
||||
regexp_contains(event_message, '$port=12345')
|
||||
regexp_contains(event_message, 'port=12345$')
|
||||
```
|
||||
|
||||
### Ignore case sensitivity:
|
||||
|
||||
@@ -119,14 +119,18 @@ height={650}
|
||||
| **Used** | RAM actively used by Postgres and the operating system |
|
||||
| **Cache + buffers** | Memory used for page cache and OS buffers |
|
||||
| **Free** | Available unallocated memory |
|
||||
| **Swap** | Disk overflow used when physical RAM is exhausted |
|
||||
|
||||
The **Swap** series only appears when the system is swapping. Swap is disk space the operating system uses as an overflow when physical RAM is full. Because disk is much slower than RAM, sustained swap activity indicates memory pressure and can significantly degrade database performance.
|
||||
|
||||
How it helps debug issues:
|
||||
|
||||
| Issue | Description |
|
||||
| ------------------------------ | ------------------------------------------------ |
|
||||
| Memory pressure detection | Identify when free memory is consistently low |
|
||||
| Cache effectiveness monitoring | Monitor cache performance for query optimization |
|
||||
| Memory leak detection | Detect inefficient memory usage patterns |
|
||||
| Issue | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------- |
|
||||
| Memory pressure detection | Identify when free memory is consistently low |
|
||||
| Cache effectiveness monitoring | Monitor cache performance for query optimization |
|
||||
| Memory leak detection | Detect inefficient memory usage patterns |
|
||||
| Swap activity monitoring | Sustained swap usage signals RAM is exhausted and paging to disk is degrading performance |
|
||||
|
||||
Actions you can take:
|
||||
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title = "FDW Wrappers error: 'component verification failed'"
|
||||
date_created = "2026-06-09T16:04:36+00:00"
|
||||
topics = [ "platform" ]
|
||||
keywords = []
|
||||
[[errors]]
|
||||
code = "HV000"
|
||||
message = "guest fdw error: component verification failed"
|
||||
|
||||
---
|
||||
|
||||
If you are observing an `ERROR: HV000: guest fdw error: component verification failed` when querying a wrapper foreign tables, it indicates that the configured Wasm package metadata does not match the downloaded component. As a security measure, the Wrappers extension blocks the module when this mismatch occurs.
|
||||
|
||||
To resolve this, update your foreign server configuration to use the correct package version and checksum. Details depend on the exact Wrapper and can be found here: https://fdw.dev/catalog/wasm/
|
||||
|
||||
**How to fix the configuration:**
|
||||
Run the following SQL via the [SQL Editor](/dashboard/project/_/sql/new):
|
||||
|
||||
```sql
|
||||
ALTER SERVER example_server OPTIONS (
|
||||
SET fdw_package_url '[THE_CORRECT_URL]',
|
||||
SET fdw_package_version '[THE_CORRECT_VERSION]',
|
||||
SET fdw_package_checksum '[INTERNAL_ID]'
|
||||
);
|
||||
```
|
||||
|
||||
**Verify the update:**
|
||||
You can confirm the update by inspecting the server options:
|
||||
|
||||
```sql
|
||||
select srvname, unnest(srvoptions) as option
|
||||
from pg_foreign_server
|
||||
where srvname = 'example_server';
|
||||
```
|
||||
|
||||
Test the connection by querying your foreign table:
|
||||
|
||||
```sql
|
||||
select * from example_table limit 1;
|
||||
```
|
||||
@@ -7,22 +7,88 @@ keywords = [ "users" ]
|
||||
database_id = "5d1c44ed-b2f6-4509-9312-1eeb8838e701"
|
||||
|
||||
[[errors]]
|
||||
http_status_code = 500
|
||||
code = "unexpected_failure"
|
||||
message = "Database error saving new user"
|
||||
|
||||
[[errors]]
|
||||
http_status_code = 500
|
||||
code = "unexpected_failure"
|
||||
message = "Database error creating new user"
|
||||
---
|
||||
|
||||
You generally get this error when trying to invite a new user from the dashboard or when trying to insert a user into a table using the table editor in the Supabase dashboard.
|
||||
You generally get this error when trying to invite a new user from the dashboard or when trying to insert a user into a table using the table editor in the Supabase dashboard. You may also see this error in logs in connection to failed signups.
|
||||
|
||||
This error is normally associated with a side effect of a database transaction.
|
||||
|
||||
**Common causes of this error:**
|
||||
|
||||
- You have a trigger/trigger function setup on the `auth.users` table
|
||||
- You have a trigger/trigger function setup on the `auth.users` table that has an error
|
||||
- You have added a constraint on the `auth.users` table which isn't being met
|
||||
- You are using Prisma and it has broken all the permissions on the `auth.users` table
|
||||
|
||||
**Debugging this error:**
|
||||
## Debugging this error:
|
||||
|
||||
- You can use the [Auth logs explorer](https://app.supabase.com/project/_/logs/auth-logs) to find the issue with more information
|
||||
- You can use the [Postgres logs explorer](https://app.supabase.com/project/_/logs/postgres-logs)
|
||||
**Step 1: Check the Auth logs**
|
||||
|
||||
https://user-images.githubusercontent.com/79497/225517698-b6e3ccaf-cd70-4acd-8124-ffbcee310d63.mp4
|
||||
Start in the [Auth logs explorer](/dashboard/project/_/logs/auth-logs) in your project dashboard. The logs surface the specific error message and give you the most direct signal about what went wrong.
|
||||
|
||||
**Step 2: Check the Postgres logs**
|
||||
|
||||
If the Auth logs point to a database-level issue, open the [Postgres logs explorer](/dashboard/project/_/logs/postgres-logs) to look for corresponding errors.
|
||||
|
||||
---
|
||||
|
||||
## Common errors
|
||||
|
||||
### NULL value in auth schema column
|
||||
|
||||
**Example Auth log error:**
|
||||
|
||||
```
|
||||
500: Database error querying schema
|
||||
error finding user: sql: Scan error on column index 8, name "confirmation_token": converting NULL to string is unsupported
|
||||
```
|
||||
|
||||
**Cause:**
|
||||
|
||||
The `auth` schema is managed by Supabase and expects specific column formats. This error typically happens when users are inserted directly via SQL or using an AI tool that uses a direct SQL `INSERT`, rather than through the [Auth API](/docs/reference/javascript/auth-api). A direct insert can leave required columns as `NULL` when the Auth service expects an empty string.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Run the following in the [SQL editor](/dashboard/project/_/sql), replacing `confirmation_token` with whichever column appears in your error message:
|
||||
|
||||
```sql
|
||||
update auth.users
|
||||
set confirmation_token = ''
|
||||
where confirmation_token is null;
|
||||
```
|
||||
|
||||
This sets all `NULL` values in that column to an empty string, which is what the Auth service expects.
|
||||
|
||||
---
|
||||
|
||||
### Relation does not exist
|
||||
|
||||
**Example Auth log error:**
|
||||
|
||||
```
|
||||
failed to close prepared statement: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQLSTATE 25P02): ERROR: relation "profiles" does not exist (SQLSTATE 42P01)
|
||||
```
|
||||
|
||||
**Example Postgres log error:**
|
||||
|
||||
```
|
||||
event_message: "relation "profiles" does not exist"
|
||||
context: "PL/pgSQL function public.handle_new_user() line 3 at SQL statement"
|
||||
```
|
||||
|
||||
**Cause:**
|
||||
|
||||
A trigger on `auth.users` is calling a function (in this case `handle_new_user`) that tries to insert into a table that does not exist. This is common when a `profiles` table is referenced in a trigger function but was never created, or was accidentally dropped.
|
||||
|
||||
**Fix:**
|
||||
|
||||
1. Check whether the missing table should exist in your `public` schema. If it should, create the table for the trigger to succeed and the error should resolve.
|
||||
|
||||
2. If the table is not needed, review the function definition and update or remove the reference. You can view and edit your database functions from the [Database Functions](/dashboard/project/_/database/functions) page in the dashboard.
|
||||
@@ -25,7 +25,7 @@ Running out of Disk IO Budget means that your instance is using more disk than i
|
||||
|
||||
To check your Disk IO Budget on the Supabase Platform, head over to [Database Health in the Observability section](/dashboard/project/_/observability/database).
|
||||
|
||||
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to pinpoint potential causes and see more fine-grained metrics like how much of your RAM is used for caching and your Swap usage. Read the [Metrics Guide](/docs/guides/platform/metrics) to learn more.
|
||||
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to pinpoint potential causes and see more fine-grained metrics like how much of your RAM is used for caching and your Swap usage. Read the [Metrics Guide](/docs/guides/telemetry/metrics) to learn more.
|
||||
|
||||
## Common reasons for high disk IO usage
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ High RAM usage could come with a range of issues:
|
||||
|
||||
To check your RAM usage on the Supabase Platform, head over to [Database Health in the Observability section](/dashboard/project/_/observability/database).
|
||||
|
||||
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](/docs/guides/platform/metrics) to learn more.
|
||||
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](/docs/guides/telemetry/metrics) to learn more.
|
||||
|
||||
## Common reasons for high RAM usage
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ High Swap usage can affect your database performance. For example, you might see
|
||||
|
||||
## Monitor your swap
|
||||
|
||||
You can monitor your resources and set up alerts using Prometheus/Grafana. See the [metrics guide](/docs/guides/platform/metrics) for more information.
|
||||
You can monitor your resources and set up alerts using Prometheus/Grafana. See the [metrics guide](/docs/guides/telemetry/metrics) for more information.
|
||||
|
||||
An [example repository](https://github.com/supabase/supabase-grafana) to ingest metrics and visualize them with Grafana is provided in the linked guide, where we maintain a [list of the exported metrics](https://github.com/supabase/supabase-grafana/blob/main/docs/metrics.md).
|
||||
|
||||
|
||||
@@ -42,9 +42,10 @@ It is happening because some Google Suite requires the explicit request of email
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google'
|
||||
provider: 'google',
|
||||
options: {
|
||||
scopes: 'https://www.googleapis.com/auth/userinfo.email'
|
||||
scopes: 'https://www.googleapis.com/auth/userinfo.email',
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
Loaded 100 of 449 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user