Merge branch 'master' into jgoux/feat-gitlab-integration-step-1

This commit is contained in:
Julien Goux committed 2025-02-19 14:54:55 +01:00
commit fc71e26c6a
893 files changed
+91591 -58206

No files matched your search

-1
View File
@@ -36,7 +36,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
-1
View File
@@ -26,7 +26,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- uses: actions/setup-node@v4
-1
View File
@@ -29,7 +29,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Setup node
+3 -3
View File
@@ -38,10 +38,10 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: 6435a4cd1eeea7c2bbd343731de7e8a5127cb2d1
key: 0be447eba738c3fd56698eff8ad01fa56c2391d7
- name: install linter
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 6435a4cd1eeea7c2bbd343731de7e8a5127cb2d1
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 0be447eba738c3fd56698eff8ad01fa56c2391d7
- name: install reviewdog
uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0
with:
@@ -54,7 +54,7 @@ jobs:
run: |
set -o pipefail
git diff --name-only origin/$BASE_REF HEAD \
| { grep -E "^apps/docs/content/guides/" || test $? = 1; } \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -r supa-mdx-lint --format rdf \
| reviewdog -f=rdjsonl -reporter=github-pr-review
- name: run linter on push or workflow dispatch
@@ -20,7 +20,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
-1
View File
@@ -25,7 +25,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
-1
View File
@@ -26,7 +26,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
-1
View File
@@ -28,7 +28,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
-3
View File
@@ -21,7 +21,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Setup node
uses: actions/setup-node@v4
@@ -47,7 +46,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Setup node
uses: actions/setup-node@v4
@@ -73,7 +71,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Setup node
uses: actions/setup-node@v4
-1
View File
@@ -41,7 +41,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Setup node
-1
View File
@@ -34,7 +34,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
-1
View File
@@ -22,7 +22,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
-1
View File
@@ -24,7 +24,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
-1
View File
@@ -24,7 +24,6 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.10.0
run_install: false
- name: Use Node.js
+2 -1
View File
@@ -1 +1,2 @@
engine-strict=true
engine-strict=true
update-notifier=false
+11
View File
@@ -1919,6 +1919,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"multi-select-combobox-creatable": {
name: "multi-select-combobox-creatable",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/multi-select-combobox-creatable")),
source: "",
files: ["registry/default/example/multi-select-combobox-creatable.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"multi-select-deletable-badge": {
name: "multi-select-deletable-badge",
type: "components:example",
+5
View File
@@ -349,6 +349,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/slider',
items: [],
},
{
title: 'Sidebar',
href: '/docs/components/sidebar',
items: [],
},
{
title: 'Sonner',
href: '/docs/components/sonner',
File diff suppressed because it is too large. Load diff
@@ -47,6 +47,13 @@ Use `MultiSelectorInput`to add a search input.
<ComponentPreview name="multi-select-combobox" />
### Badge Combo Box with creatable selector
creatable: `boolean`
`creatable` prop on the `MultiSelectorList` component can be used to add a new option if no items match
<ComponentPreview name="multi-select-combobox-creatable" />
### Badge Limit
badgeLimit: `number` | `"wrap"`.
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Calendar, MoreHorizontal, Tags, Trash, User } from 'lucide-react'
import { Button, CommandList_Shadcn_ } from 'ui'
import { Button, CommandList_Shadcn_, ScrollArea } from 'ui'
import {
Command_Shadcn_,
CommandEmpty_Shadcn_,
@@ -75,23 +75,25 @@ export default function ComboboxDropdownMenu() {
<DropdownMenuSubContent className="p-0">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Filter label..." autoFocus={true} />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No label found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{labels.map((label) => (
<CommandItem_Shadcn_
key={label}
value={label}
onSelect={(value) => {
setLabel(value)
setOpen(false)
}}
>
{label}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
<ScrollArea className="h-20">
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No label found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{labels.map((label) => (
<CommandItem_Shadcn_
key={label}
value={label}
onSelect={(value) => {
setLabel(value)
setOpen(false)
}}
>
{label}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</ScrollArea>
</Command_Shadcn_>
</DropdownMenuSubContent>
</DropdownMenuSub>
@@ -0,0 +1,34 @@
import { useState } from 'react'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
export default function MultiSelectDemo() {
const [selectedValues, setSelectedValues] = useState<string[]>([])
return (
<MultiSelector values={selectedValues} onValuesChange={setSelectedValues}>
<MultiSelectorTrigger className="w-72" label="Select fruits" badgeLimit="wrap" />
<MultiSelectorContent>
<MultiSelectorInput placeholder="Search fruits" showResetIcon />
<MultiSelectorList creatable>
<MultiSelectorItem value="Mango">Mango</MultiSelectorItem>
<MultiSelectorItem value="Date">Date</MultiSelectorItem>
<MultiSelectorItem value="Apple">Apple</MultiSelectorItem>
<MultiSelectorItem value="Elderberrie">Elderberrie</MultiSelectorItem>
<MultiSelectorItem value="Fig">Fig</MultiSelectorItem>
<MultiSelectorItem value="Grape">Grape</MultiSelectorItem>
<MultiSelectorItem value="Banana">Banana</MultiSelectorItem>
<MultiSelectorItem value="Kiwi">Kiwi</MultiSelectorItem>
<MultiSelectorItem value="Strawberry">Strawberry</MultiSelectorItem>
<MultiSelectorItem value="Cherry">Cherry</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)
}
+5
View File
@@ -1091,6 +1091,11 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/multi-select-combobox.tsx'],
},
{
name: 'multi-select-combobox-creatable',
type: 'components:example',
files: ['example/multi-select-combobox-creatable.tsx'],
},
{
name: 'multi-select-deletable-badge',
type: 'components:example',
+2 -1
View File
@@ -8,7 +8,8 @@ module.exports = config({
// purge styles from grid library
//
'./../../packages/ui/src/**/*.{tsx,ts,js}',
'./../../packages/ui-patterns/!(node_modules)/**/*.{tsx,ts,js}',
'./../../packages/ui-patterns/**/*.{tsx,ts,js}',
'!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule
],
theme: {
extend: {
@@ -1,3 +1,4 @@
import matter from 'gray-matter'
import type { Code, Heading, Root } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
@@ -11,7 +12,8 @@ import { EXAMPLES_DIRECTORY } from '~/lib/docs'
const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')
function parseMarkdown(markdown: string) {
const mdast = fromMarkdown(markdown)
const { content: withoutFrontmatter } = matter(markdown)
const mdast = fromMarkdown(withoutFrontmatter)
let heading = ''
visit(mdast, 'heading', (node: Heading) => {
+2 -1
View File
@@ -12,6 +12,8 @@ import {
redirectNonexistentReferenceSection,
} from '~/features/docs/Reference.utils'
export const dynamicParams = false
export default async function ReferencePage({
params: { slug },
}: {
@@ -39,7 +41,6 @@ export default async function ReferencePage({
return <ClientSdkReferencePage sdkId={sdkId} libVersion={version} />
} else if (isCliReference) {
console.log('Returning CLI reference page: %o', parsedPath)
return <CliReferencePage />
} else if (isApiReference) {
return <ApiReferencePage />
+15 -14
View File
@@ -109,15 +109,15 @@ const BreadcrumbsInternal = ({
</DrawerTrigger>
<DrawerContent showHandle={false}>
<div className="grid gap-1 px-4">
{breadcrumbs
.slice(1, -2)
.map((crumb) =>
crumb.url ? (
<Link href={crumb.url}>{crumb.title || crumb.name}</Link>
) : (
crumb.title || crumb.name
)
)}
{breadcrumbs.slice(1, -2).map((crumb, index) =>
crumb.url ? (
<Link key={index} href={crumb.url}>
{crumb.title || crumb.name}
</Link>
) : (
crumb.title || crumb.name
)
)}
</div>
<DrawerFooter className="pt-4">
<DrawerClose asChild>
@@ -131,12 +131,12 @@ const BreadcrumbsInternal = ({
<BreadcrumbSeparator />
</>
)}
{appendedBreadcrumbs?.map((crumb, i) => (
<Fragment key={crumb.url}>
{appendedBreadcrumbs?.map((crumb, index) => (
<Fragment key={index}>
<BreadcrumbItem
className={cn(
'flex items-center overflow-hidden',
i === appendedBreadcrumbs.length - 1 && 'md:text-foreground-light'
index === appendedBreadcrumbs.length - 1 && 'md:text-foreground-light'
)}
>
{crumb.url ? (
@@ -148,7 +148,7 @@ const BreadcrumbsInternal = ({
)}
</BreadcrumbItem>
<BreadcrumbSeparator
className={cn(i === appendedBreadcrumbs.length - 1 && 'md:hidden')}
className={cn(index === appendedBreadcrumbs.length - 1 && 'md:hidden')}
/>
</Fragment>
))}
@@ -170,7 +170,8 @@ function useBreadcrumbs() {
if (isAiPromptsPage) {
const breadcrumbs = [
{ name: 'Getting started', url: '/guides/getting-started' },
{ name: 'AI Prompts', url: '/guides/getting-started/ai-prompts' },
{ name: 'AI Tools' },
{ name: 'Prompts', url: '/guides/getting-started/ai-prompts' },
]
return breadcrumbs
}
+1 -1
View File
@@ -74,7 +74,7 @@ const HomePageCover = (props) => {
href: '/guides/getting-started/quickstarts/vue',
},
{
tooltip: 'NuxtJS',
tooltip: 'Nuxt',
icon: '/docs/img/icons/nuxt-icon',
href: '/guides/getting-started/quickstarts/nuxtjs',
},
@@ -0,0 +1 @@
There is no fixed fee for a Preview branch. You only pay for the usage it incurs. With Compute costs of $0.01344 per hour, a branch running on Micro Compute size starts at $0.32 per day.
@@ -0,0 +1,8 @@
$2 per 1 million invocations. You are only charged for usage exceeding your subscription plan's quota.
| Plan | Quota | Over-Usage |
| ---------- | --------- | ---------------------------- |
| Free | 500,000 | - |
| Pro | 2 million | $2 per 1 million invocations |
| Team | 2 million | $2 per 1 million invocations |
| Enterprise | Custom | Custom |
@@ -0,0 +1 @@
$0.00325 per MAU. You are only charged for usage exceeding your subscription plan's quota.
@@ -0,0 +1 @@
$0.015 per SSO MAU. You are only charged for usage exceeding your subscription plan's quota.
@@ -0,0 +1 @@
$0.00325 per Third-Party MAU. You are only charged for usage exceeding your subscription plan's quota.
@@ -0,0 +1,7 @@
$0.1027 per hour ($75 per month) for the first project. $0.0137 per hour ($10 per month) for every additional project.
| Plan | Project 1 per month | Project 2 per month | Project 3 per month |
| ---------- | ------------------- | ------------------- | ------------------- |
| Pro | $75 | $10 | $10 |
| Team | $75 | $10 | $10 |
| Enterprise | Custom | Custom | Custom |
@@ -0,0 +1,7 @@
Pricing depends on the recovery retention period, which determines how many days back you can restore data to any chosen point of up to seconds in granularity.
| Recovery Retention Period in Days | Hourly Price USD | Monthly Price USD |
| --------------------------------- | ---------------- | ----------------- |
| 7 | $0.137 | $100 |
| 14 | $0.274 | $200 |
| 28 | $0.55 | $400 |
@@ -0,0 +1,8 @@
$10 per 1,000 peak connections. You are only charged for usage exceeding your subscription plan's quota.
| Plan | Quota | Over-Usage |
| ---------- | ------ | ------------------------------ |
| Free | 200 | - |
| Pro | 500 | $10 per 1,000 peak connections |
| Team | 500 | $10 per 1,000 peak connections |
| Enterprise | Custom | Custom |
@@ -0,0 +1,8 @@
$2.50 per 1 million messages. You are only charged for usage exceeding your subscription plan's quota.
| Plan | Quota | Over-Usage |
| ---------- | --------- | ---------------------------- |
| Free | 2 million | - |
| Pro | 5 million | $2.50 per 1 million messages |
| Team | 5 million | $2.50 per 1 million messages |
| Enterprise | Custom | Custom |
@@ -0,0 +1,13 @@
$5 per 1,000 origin images. You are only charged for usage exceeding your subscription plan's quota.
<Admonition type="note">
The count resets at the start of each billing cycle.
</Admonition>
| Plan | Quota | Over-Usage |
| ---------- | ------ | -------------------------- |
| Pro | 100 | $5 per 1,000 origin images |
| Team | 100 | $5 per 1,000 origin images |
| Enterprise | Custom | Custom |
@@ -0,0 +1,8 @@
$0.00002919 per GB-Hr ($0.021 per GB per month). You are only charged for usage exceeding your subscription plan's quota.
| Plan | Quota in GB | Over-Usage per GB | Quota in GB-Hrs | Over-Usage per GB-Hrs |
| ---------- | ----------- | ----------------- | --------------- | --------------------- |
| Free | 1 | - | 744 | - |
| Pro | 100 | $0.021 | 74,400 | $0.00002919 |
| Team | 100 | $0.021 | 74,400 | $0.00002919 |
| Enterprise | Custom | Custom | Custom | Custom |
+22
View File
@@ -15,6 +15,17 @@ import QuickstartIntro from './quickstart_intro.mdx'
import SocialProviderSettingsSupabase from './social_provider_settings_supabase.mdx'
import SocialProviderSetup from './social_provider_setup.mdx'
import PostgresInstallation from './postgres_installation.mdx'
import PricingMfaPhone from './billing/pricing/pricing_mfa_phone.mdx'
import PricingBranching from './billing/pricing/pricing_branching.mdx'
import PricingEdgeFunctions from './billing/pricing/pricing_edge_functions.mdx'
import PricingMau from './billing/pricing/pricing_mau.mdx'
import PricingMauSso from './billing/pricing/pricing_mau_sso.mdx'
import PricingMauThirdParty from './billing/pricing/pricing_mau_third_party.mdx'
import PricingPitr from './billing/pricing/pricing_pitr.mdx'
import PricingRealtimeMessages from './billing/pricing/pricing_realtime_messages.mdx'
import PricingRealtimeConnections from './billing/pricing/pricing_realtime_connections.mdx'
import PricingStorageImageTransformations from './billing/pricing/pricing_storage_image_transformations.mdx'
import PricingStorageSize from './billing/pricing/pricing_storage_size.mdx'
export {
AuthErrorCodesTable,
@@ -32,4 +43,15 @@ export {
SocialProviderSettingsSupabase,
SocialProviderSetup,
PostgresInstallation,
PricingMfaPhone,
PricingBranching,
PricingEdgeFunctions,
PricingMau,
PricingMauSso,
PricingMauThirdParty,
PricingPitr,
PricingRealtimeMessages,
PricingRealtimeConnections,
PricingStorageImageTransformations,
PricingStorageSize,
}
@@ -258,7 +258,7 @@ export const gettingstarted: NavMenuConstant = {
items: [
{ name: 'Next.js', url: '/guides/getting-started/quickstarts/nextjs' },
{ name: 'React', url: '/guides/getting-started/quickstarts/reactjs' },
{ name: 'NuxtJS', url: '/guides/getting-started/quickstarts/nuxtjs' },
{ name: 'Nuxt', url: '/guides/getting-started/quickstarts/nuxtjs' },
{ name: 'Vue', url: '/guides/getting-started/quickstarts/vue' },
{ name: 'Hono', url: '/guides/getting-started/quickstarts/hono' },
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' },
@@ -366,13 +366,17 @@ export const gettingstarted: NavMenuConstant = {
],
},
{
name: 'AI Prompts',
name: 'AI Tools',
url: undefined,
items: [
{
name: 'Overview',
name: 'Prompts',
url: '/guides/getting-started/ai-prompts',
},
{
name: 'Model context protocol (MCP)',
url: '/guides/getting-started/mcp',
},
],
},
],
@@ -1009,10 +1013,6 @@ export const database: NavMenuConstant = {
name: 'pg_plan_filter: Restrict Total Cost',
url: '/guides/database/extensions/pg_plan_filter',
},
{
name: 'pg_stat_monitor: Extended Query Performance Monitoring',
url: '/guides/database/extensions/pg_stat_monitor',
},
{
name: 'postgres_fdw: query data from an external Postgres server',
url: '/guides/database/extensions/postgres_fdw',
@@ -1415,6 +1415,10 @@ export const functions: NavMenuConstant = {
name: 'Limits',
url: '/guides/functions/limits',
},
{
name: 'Pricing',
url: '/guides/functions/pricing',
},
],
},
{
@@ -1583,6 +1587,7 @@ export const realtime: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Quotas', url: '/guides/realtime/quotas' },
{ name: 'Pricing', url: '/guides/realtime/pricing' },
{ name: 'Architecture', url: '/guides/realtime/architecture' },
{ name: 'Message Protocol', url: '/guides/realtime/protocol', items: [] },
{ name: 'Benchmarks', url: '/guides/realtime/benchmarks' },
@@ -1668,6 +1673,7 @@ export const storage: NavMenuConstant = {
items: [
{ name: 'Copy / Move Objects', url: '/guides/storage/management/copy-move-objects' },
{ name: 'Delete Objects', url: '/guides/storage/management/delete-objects' },
{ name: 'Pricing', url: '/guides/storage/management/pricing' },
],
},
{
@@ -1747,6 +1753,10 @@ export const ai = {
url: '/guides/ai/vector-indexes',
items: vectorIndexItems,
},
{
name: 'Automatic embeddings',
url: '/guides/ai/automatic-embeddings',
},
{
name: 'Engineering for scale',
url: '/guides/ai/engineering-for-scale',
@@ -1920,6 +1930,22 @@ export const local_development: NavMenuConstant = {
},
],
},
{
name: 'Testing',
url: undefined,
items: [
{ name: 'Getting started', url: '/guides/local-development/testing/overview' },
{
name: 'pgTAP advanced guide',
url: '/guides/local-development/testing/pgtap-extended',
},
{ name: 'Database testing', url: '/guides/database/testing' },
{
name: 'RLS policies testing',
url: '/guides/database/extensions/pgtap#testing-rls-policies',
},
],
},
],
}
@@ -2083,12 +2109,114 @@ export const platform: NavMenuConstant = {
url: undefined,
items: [
{
name: 'How billing works',
url: '/guides/platform/org-based-billing',
name: 'About billing on Supabase',
url: '/guides/platform/billing-on-supabase',
},
{
name: 'Spend caps and cost-controls',
url: '/guides/platform/spend-cap',
name: 'Get set up for billing',
url: '/guides/platform/get-set-up-for-billing',
},
{
name: 'Manage your subscription',
url: '/guides/platform/manage-your-subscription',
},
{
name: 'Manage your usage',
url: '/guides/platform/manage-your-usage',
items: [
{
name: 'Compute',
url: '/guides/platform/manage-your-usage/compute',
},
{
name: 'Egress',
url: '/guides/platform/manage-your-usage/egress',
},
{
name: 'Disk Size',
url: '/guides/platform/manage-your-usage/disk-size',
},
{
name: 'Disk Throughput',
url: '/guides/platform/manage-your-usage/disk-throughput',
},
{
name: 'Disk IOPS',
url: '/guides/platform/manage-your-usage/disk-iops',
},
{
name: 'Monthly Active Users',
url: '/guides/platform/manage-your-usage/monthly-active-users',
},
{
name: 'Monthly Active Third-Party Users',
url: '/guides/platform/manage-your-usage/monthly-active-users-third-party',
},
{
name: 'Monthly Active SSO Users',
url: '/guides/platform/manage-your-usage/monthly-active-users-sso',
},
{
name: 'Storage Size',
url: '/guides/platform/manage-your-usage/storage-size',
},
{
name: 'Storage Image Transformations',
url: '/guides/platform/manage-your-usage/storage-image-transformations',
},
{
name: 'Edge Function Invocations',
url: '/guides/platform/manage-your-usage/edge-function-invocations',
},
{
name: 'Realtime Messages',
url: '/guides/platform/manage-your-usage/realtime-messages',
},
{
name: 'Realtime Peak Connections',
url: '/guides/platform/manage-your-usage/realtime-peak-connections',
},
{
name: 'Custom Domains',
url: '/guides/platform/manage-your-usage/custom-domains',
},
{
name: 'Point-in-Time Recovery',
url: '/guides/platform/manage-your-usage/point-in-time-recovery',
},
{
name: 'IPv4',
url: '/guides/platform/manage-your-usage/ipv4',
},
{
name: 'MFA Phone',
url: '/guides/platform/manage-your-usage/advanced-mfa-phone',
},
{
name: 'Read Replicas',
url: '/guides/platform/manage-your-usage/read-replicas',
},
{
name: 'Branching',
url: '/guides/platform/manage-your-usage/branching',
},
{
name: 'Log Drains',
url: '/guides/platform/manage-your-usage/log-drains',
},
],
},
{
name: 'Your monthly invoice',
url: '/guides/platform/your-monthly-invoice',
},
{
name: 'Control your costs',
url: '/guides/platform/cost-control',
},
{
name: 'Credits',
url: '/guides/platform/credits',
},
{
name: 'Billing FAQ',
@@ -39,19 +39,36 @@ const NavigationMenuGuideList = ({
}
}
// Inject federated prompts into the 'AI Tools > Prompts' section
if (id === MenuId.GettingStarted && additionalNavItems?.prompts) {
const promptsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Prompts')
if (promptsSectionIndex !== -1) {
menu = {
...menu,
items: [
...menu.items.slice(0, promptsSectionIndex),
{
...menu.items[promptsSectionIndex],
items: [...menu.items[promptsSectionIndex].items, ...additionalNavItems.prompts],
},
...menu.items.slice(promptsSectionIndex + 1),
],
const aiToolsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Tools')
if (aiToolsSectionIndex !== -1) {
const beforeAITools = menu.items.slice(0, aiToolsSectionIndex)
const afterAITools = menu.items.slice(aiToolsSectionIndex + 1)
const aiToolsSection = menu.items[aiToolsSectionIndex]
const promptsSectionIndex = aiToolsSection.items.findIndex((item) => item.name === 'Prompts')
if (promptsSectionIndex !== -1) {
const beforePrompts = aiToolsSection.items.slice(0, promptsSectionIndex)
const afterPrompts = aiToolsSection.items.slice(promptsSectionIndex + 1)
const promptsSection = aiToolsSection.items[promptsSectionIndex]
const modifiedPromptsSection = {
...promptsSection,
items: additionalNavItems.prompts,
}
const modifiedAIToolsSection = {
...aiToolsSection,
items: [...beforePrompts, modifiedPromptsSection, ...afterPrompts],
}
menu = {
...menu,
items: [...beforeAITools, modifiedAIToolsSection, ...afterAITools],
}
}
}
}
@@ -0,0 +1,817 @@
---
id: 'ai-automatic-embeddings'
title: 'Automatic embeddings'
description: 'Automate embedding generation and updates in Postgres'
sidebar_label: 'Automatic embeddings'
---
Vector embeddings enable powerful [semantic search](/docs/guides/ai/semantic-search) capabilities in Postgres, but managing them alongside your content has traditionally been complex. This guide demonstrates how to automate embedding generation and updates using Supabase [Edge Functions](/docs/guides/functions), [pgmq](/docs/guides/database/extensions/pgmq), [pg_net](/docs/guides/database/extensions/pg_net), and [pg_cron](/docs/guides/cron).
## Understanding the challenge
When implementing semantic search with pgvector, developers typically need to:
1. Generate embeddings via an external API (like OpenAI)
2. Store these embeddings alongside the content
3. Keep embeddings in sync when content changes
4. Handle failures and retries in the embedding generation process
While Postgres [full-text search](/docs/guides/database/full-text-search) can handle this internally through synchronous calls to `to_tsvector` and [triggers](https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS), semantic search requires asynchronous API calls to a provider like OpenAI to generate vector embeddings. This guide demonstrates how to use triggers, queues, and Supabase Edge Functions to bridge this gap.
## Understanding the architecture
We'll leverage the following Postgres and Supabase features to create the automated embedding system:
1. [pgvector](/docs/guides/database/extensions/pgvector): Stores and queries vector embeddings
1. [pgmq](/docs/guides/queues): Queues embedding generation requests for processing and retries
1. [pg_net](/docs/guides/database/extensions/pg_net): Handles asynchronous HTTP requests to Edge Functions directly from Postgres
1. [pg_cron](/docs/guides/cron): Automatically processes and retries embedding generations
1. [Triggers](/docs/guides/database/postgres/triggers): Detects content changes and enqueues embedding generation requests
1. [Edge Functions](/docs/guides/functions): Generates embeddings via an API like OpenAI (customizable)
We'll design the system to:
1. Be generic, so that it can be used with any table and content. This allows you to configure embeddings in multiple places, each with the ability to customize the input used for embedding generation. These will all use the same queue infrastructure and Edge Function to generate the embeddings.
1. Handle failures gracefully, by retrying failed jobs and providing detailed information about the status of each job.
## Implementation
We'll start by setting up the infrastructure needed to queue and process embedding generation requests. Then we'll create an example table with triggers to enqueue these embedding requests whenever content is inserted or updated.
### Step 1: Enable extensions
First, let's enable the required extensions:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="sql"
queryGroup="database-method"
>
<TabPanel id="sql" label="SQL">
```sql
-- For vector operations
create extension if not exists vector
with
schema extensions;
-- For queueing and processing jobs
-- (pgmq will create its own schema)
create extension if not exists pgmq;
-- For async HTTP requests
create extension if not exists pg_net
with
schema extensions;
-- For scheduled processing and retries
-- (pg_cron will create its own schema)
create extension if not exists pg_cron;
-- For clearing embeddings during updates
create extension if not exists hstore
with
schema extensions;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension, call `drop extension`.
</TabPanel>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Extensions](/dashboard/project/_/database/extensions) page in the Dashboard.
2. Search for and enable the following extensions:
- `vector`
- `pgmq`
- `pg_net`
- `pg_cron`
- `hstore`
</TabPanel>
</Tabs>
### Step 2: Create utility functions
Before we set up our embedding logic, we need to create some utility functions:
```sql
-- Schema for utility functions
create schema util;
-- Utility function to get the Supabase project URL (required for Edge Functions)
create function util.project_url()
returns text
language plpgsql
security definer
as $$
declare
secret_value text;
begin
-- Retrieve the project URL from Vault
select decrypted_secret into secret_value from vault.decrypted_secrets where name = 'project_url';
return secret_value;
end;
$$;
-- Generic function to invoke any Edge Function
create or replace function util.invoke_edge_function(
name text,
body jsonb,
timeout_milliseconds int = 5 * 60 * 1000 -- default 5 minute timeout
)
returns void
language plpgsql
as $$
declare
headers_raw text;
auth_header text;
begin
-- If we're in a PostgREST session, reuse the request headers for authorization
headers_raw := current_setting('request.headers', true);
-- Only try to parse if headers are present
auth_header := case
when headers_raw is not null then
(headers_raw::json->>'authorization')
else
null
end;
-- Perform async HTTP request to the edge function
perform net.http_post(
url => util.project_url() || '/functions/v1/' || name,
headers => jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', auth_header
),
body => body,
timeout_milliseconds => timeout_milliseconds
);
end;
$$;
-- Generic trigger function to clear a column on update
create or replace function util.clear_column()
returns trigger
language plpgsql as $$
declare
clear_column text := TG_ARGV[0];
begin
NEW := NEW #= hstore(clear_column, NULL);
return NEW;
end;
$$;
```
Here we create:
- A schema `util` to store utility functions.
- A function to retrieve the Supabase project URL from [Vault](/docs/guides/database/vault). We'll add this secret next.
- A generic function to invoke any Edge Function with a given name and request body.
- A generic trigger function to clear a column on update. This function accepts the column name as an argument and sets it to `NULL` in the `NEW` record. We'll explain how to use this function later.
Every project has a unique API URL that is required to invoke Edge Functions. Let's go ahead and add the project URL secret to Vault depending on your environment.
When working with a local Supabase stack, add the following to your `supabase/seed.sql` file:
```sql
select
vault.create_secret('http://api.supabase.internal:8000', 'project_url');
```
When deploying to the cloud platform, open the [SQL editor](/dashboard/project/_/sql/new) and run the following, replacing `<project-url>` with your [project's API URL](/dashboard/project/_/settings/api):
```sql
select
vault.create_secret('<project-url>', 'project_url');
```
### Step 3: Create queue and triggers
Our goal is to automatically generate embeddings whenever content is inserted or updated within a table. We can use triggers and queues to achieve this. Our approach is to automatically queue embedding jobs whenever records are inserted or updated in a table, then process them asynchronously using a cron job. If a job fails, it will remain in the queue and be retried in the next scheduled task.
First we create a `pgmq` queue for processing embedding requests:
```sql
-- Queue for processing embedding jobs
select pgmq.create('embedding_jobs');
```
Next we create a trigger function to queue embedding jobs. We'll use this function to handle both insert and update events:
```sql
-- Generic trigger function to queue embedding jobs
create or replace function util.queue_embeddings()
returns trigger
language plpgsql
as $$
declare
content_function text = TG_ARGV[0];
embedding_column text = TG_ARGV[1];
begin
perform pgmq.send(
queue_name => 'embedding_jobs',
msg => jsonb_build_object(
'id', NEW.id,
'schema', TG_TABLE_SCHEMA,
'table', TG_TABLE_NAME,
'contentFunction', content_function,
'embeddingColumn', embedding_column
)
);
return NEW;
end;
$$;
```
Our `util.queue_embeddings` trigger function is generic and can be used with any table and content function. It accepts two arguments:
1. `content_function`: The name of a function that returns the text content to be embedded. The function should accept a single row as input and return text (see the `embedding_input` example).
This allows you to customize the text input passed to the embedding model - for example, you could concatenate multiple columns together like `title` and `content` and use the result as input.
2. `embedding_column`: The name of the destination column where the embedding will be stored.
Note that the `util.queue_embeddings` trigger function requires a `for each row` clause to work correctly. See [Usage](#usage) for an example of how to use this trigger function with your table.
Next we'll create a function to process the embedding jobs. This function will read jobs from the queue, group them into batches, and invoke the Edge Function to generate embeddings. We'll use `pg_cron` to schedule this function to run every 10 seconds.
```sql
-- Function to process embedding jobs from the queue
create or replace function util.process_embeddings(
batch_size int = 10,
max_requests int = 10,
timeout_milliseconds int = 5 * 60 * 1000 -- default 5 minute timeout
)
returns void
language plpgsql
as $$
declare
job_batches jsonb[];
batch jsonb;
begin
with
-- First get jobs and assign batch numbers
numbered_jobs as (
select
message || jsonb_build_object('jobId', msg_id) as job_info,
(row_number() over (order by 1) - 1) / batch_size as batch_num
from pgmq.read(
queue_name => 'embedding_jobs',
vt => timeout_milliseconds / 1000,
qty => max_requests * batch_size
)
),
-- Then group jobs into batches
batched_jobs as (
select
jsonb_agg(job_info) as batch_array,
batch_num
from numbered_jobs
group by batch_num
)
-- Finally aggregate all batches into array
select array_agg(batch_array)
from batched_jobs
into job_batches;
-- Invoke the embed edge function for each batch
foreach batch in array job_batches loop
perform util.invoke_edge_function(
name => 'embed',
body => batch,
timeout_milliseconds => timeout_milliseconds
);
end loop;
end;
$$;
-- Schedule the embedding processing
select
cron.schedule(
'process-embeddings',
'10 seconds',
$$
select util.process_embeddings();
$$
);
```
Let's discuss some common questions about this approach:
#### Why not generate all embeddings in a single Edge Function request?
While this is possible, it can lead to long processing times and potential timeouts. Batching allows us to process multiple embeddings concurrently and handle failures more effectively.
#### Why not one request per row?
This approach can lead to API rate limiting and performance issues. Batching provides a balance between efficiency and reliability.
#### Why queue requests instead of processing them immediately?
Queuing allows us to handle failures gracefully, retry requests, and manage concurrency more effectively. Specifically we are using `pgmq`'s visibility timeouts to ensure that failed requests are retried.
#### How do visibility timeouts work?
Every time we read a message from the queue, we set a visibility timeout which tells `pgmq` to hide the message from other readers for a certain period. If the Edge Function fails to process the message within this period, the message becomes visible again and will be retried by the next scheduled task.
#### How do we handle retries?
We use `pg_cron` to schedule a task that reads messages from the queue and processes them. If the Edge Function fails to process a message, it becomes visible again after a timeout and can be retried by the next scheduled task.
#### Is 10 seconds a good interval for processing?
This interval is a good starting point, but you may need to adjust it based on your workload and the time it takes to generate embeddings. You can adjust the `batch_size`, `max_requests`, and `timeout_milliseconds` parameters to optimize performance.
### Step 4: Create the Edge Function
Finally we'll create the Edge Function to generate embeddings. We'll use OpenAI's API in this example, but you can replace it with any other embedding generation service.
Use the Supabase CLI to create a new Edge Function:
```bash
supabase functions new embed
```
This will create a new directory `supabase/functions/embed` with an `index.ts` file. Replace the contents of this file with the following:
_supabase/functions/embed/index.ts_:
```typescript
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
// We'll use the OpenAI API to generate embeddings
import OpenAI from 'jsr:@openai/openai'
import { z } from 'npm:zod'
// We'll make a direct Postgres connection to update the document
import postgres from 'https://deno.land/x/postgresjs@v3.4.5/mod.js'
// Initialize OpenAI client
const openai = new OpenAI({
// We'll need to manually set the `OPENAI_API_KEY` environment variable
apiKey: Deno.env.get('OPENAI_API_KEY'),
})
// Initialize Postgres client
const sql = postgres(
// `SUPABASE_DB_URL` is a built-in environment variable
Deno.env.get('SUPABASE_DB_URL')!
)
const jobSchema = z.object({
jobId: z.number(),
id: z.number(),
schema: z.string(),
table: z.string(),
contentFunction: z.string(),
embeddingColumn: z.string(),
})
const failedJobSchema = jobSchema.extend({
error: z.string(),
})
type Job = z.infer<typeof jobSchema>
type FailedJob = z.infer<typeof failedJobSchema>
type Row = {
id: string
content: unknown
}
const QUEUE_NAME = 'embedding_jobs'
// Listen for HTTP requests
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return new Response('expected POST request', { status: 405 })
}
if (req.headers.get('content-type') !== 'application/json') {
return new Response('expected json body', { status: 400 })
}
// Use Zod to parse and validate the request body
const parseResult = z.array(jobSchema).safeParse(await req.json())
if (parseResult.error) {
return new Response(`invalid request body: ${parseResult.error.message}`, {
status: 400,
})
}
const pendingJobs = parseResult.data
// Track jobs that completed successfully
const completedJobs: Job[] = []
// Track jobs that failed due to an error
const failedJobs: FailedJob[] = []
async function processJobs() {
let currentJob: Job | undefined
while ((currentJob = pendingJobs.shift()) !== undefined) {
try {
await processJob(currentJob)
completedJobs.push(currentJob)
} catch (error) {
failedJobs.push({
...currentJob,
error: error instanceof Error ? error.message : JSON.stringify(error),
})
}
}
}
try {
// Process jobs while listening for worker termination
await Promise.race([processJobs(), catchUnload()])
} catch (error) {
// If the worker is terminating (e.g. wall clock limit reached),
// add pending jobs to fail list with termination reason
failedJobs.push(
...pendingJobs.map((job) => ({
...job,
error: error instanceof Error ? error.message : JSON.stringify(error),
}))
)
}
// Log completed and failed jobs for traceability
console.log('finished processing jobs:', {
completedJobs: completedJobs.length,
failedJobs: failedJobs.length,
})
return new Response(
JSON.stringify({
completedJobs,
failedJobs,
}),
{
// 200 OK response
status: 200,
// Custom headers to report job status
headers: {
'Content-Type': 'application/json',
'X-Completed-Jobs': completedJobs.length.toString(),
'X-Failed-Jobs': failedJobs.length.toString(),
},
}
)
})
/**
* Generates an embedding for the given text.
*/
async function generateEmbedding(text: string) {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
const [data] = response.data
if (!data) {
throw new Error('failed to generate embedding')
}
return data.embedding
}
/**
* Processes an embedding job.
*/
async function processJob(job: Job) {
const { jobId, id, schema, table, contentFunction, embeddingColumn } = job
// Fetch content for the schema/table/row combination
const [row]: [Row] = await sql`
select
id,
${sql(contentFunction)}(t) as content
from
${sql(schema)}.${sql(table)} t
where
id = ${id}
`
if (!row) {
throw new Error(`row not found: ${schema}.${table}/${id}`)
}
if (typeof row.content !== 'string') {
throw new Error(`invalid content - expected string: ${schema}.${table}/${id}`)
}
const embedding = await generateEmbedding(row.content)
await sql`
update
${sql(schema)}.${sql(table)}
set
${sql(embeddingColumn)} = ${JSON.stringify(embedding)}
where
id = ${id}
`
await sql`
select pgmq.delete(${QUEUE_NAME}, ${jobId}::bigint)
`
}
/**
* Returns a promise that rejects if the worker is terminating.
*/
function catchUnload() {
return new Promise((reject) => {
addEventListener('beforeunload', (ev: any) => {
reject(new Error(ev.detail?.reason))
})
})
}
```
The Edge Function listens for incoming HTTP requests from `pg_net` and processes each embedding job. It is a generic worker that can handle embedding jobs for any table and column. It uses OpenAI's API to generate embeddings and updates the corresponding row in the database. It also deletes the job from the queue once it has been processed.
The function is designed to process multiple jobs independently. If one job fails, it will not affect the processing of other jobs. The function returns a `200 OK` response with a list of completed and failed jobs. We can use this information to diagnose failed jobs. See [Troubleshooting](#troubleshooting) for more details.
You will need to set the `OPENAI_API_KEY` environment variable to authenticate with OpenAI. When running the Edge Function locally, you can add it to a `.env` file:
_.env_:
```
OPENAI_API_KEY=your-api-key
```
When you're ready to deploy the Edge Function, set can set the environment variable using the Supabase CLI:
```shell
supabase secrets set --env-file .env
```
or
```shell
supabase secrets set OPENAI_API_KEY=<your-api-key>
```
Alternatively, you can replace the `generateEmbedding` function with your own embedding generation logic.
See [Deploy to Production](/docs/guides/functions/deploy) for more information on how to deploy the Edge Function.
## Usage
Now that the infrastructure is in place, let's go through an example of how to use this system to automatically generate embeddings for a table of documents. You can use this approach with multiple tables and customize the input for each embedding generation as needed.
### 1. Create table to store documents with embeddings
We'll set up a new `documents` table that will store our content and embeddings:
```sql
-- Table to store documents with embeddings
create table documents (
id integer primary key generated always as identity,
title text not null,
content text not null,
embedding halfvec,
created_at timestamp with time zone default now()
);
-- Index for vector search over document embeddings
create index on documents using hnsw (embedding halfvec_cosine_ops);
```
Our `documents` table stores the title and content of each document along with its vector embedding. We use a `halfvec` column to store the embeddings, which is a `pgvector` data type that stores float values in half precision (16 bits) to save space.
We use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes) on the vector column. Note that we are choosing `halfvec_cosine_ops` as the index method, which means our future queries will need to use cosine distance (`<=>`) to find similar embeddings. Also note that HNSW indexes support a maximum of 4000 dimensions for `halfvec` vectors, so keep this in mind when choosing an embedding model. If your model generates embeddings with more than 4000 dimensions, you will need to reduce the dimensionality before indexing them. See [Matryoshka embeddings](/blog/matryoshka-embeddings) for a potential solution to shortening dimensions.
Also note that the table must have a primary key column named `id` for our triggers to work correctly with the `util.queue_embeddings` function and for our Edge Function to update the correct row.
### 2. Create triggers to enqueue embedding jobs
Now we'll set up the triggers to enqueue embedding jobs whenever content is inserted or updated:
```sql
-- Customize the input for embedding generation
-- e.g. Concatenate title and content with a markdown header
create or replace function embedding_input(doc documents)
returns text
language plpgsql
immutable
as $$
begin
return '# ' || doc.title || E'\n\n' || doc.content;
end;
$$;
-- Trigger for insert events
create trigger embed_documents_on_insert
after insert
on documents
for each row
execute function util.queue_embeddings('embedding_input', 'embedding');
-- Trigger for update events
create trigger embed_documents_on_update
after update of title, content -- must match the columns in embedding_input()
on documents
for each row
execute function util.queue_embeddings('embedding_input', 'embedding');
```
We create 2 triggers:
1. `embed_documents_on_insert`: Enqueues embedding jobs whenever new rows are inserted into the `documents` table.
2. `embed_documents_on_update`: Enqueues embedding jobs whenever the `title` or `content` columns are updated in the `documents` table.
Both of these triggers use the same `util.queue_embeddings` function that will queue the embedding jobs for processing. They accept 2 arguments:
1. `embedding_input`: The name of the function that generates the input for embedding generation. This function allows you to customize the text input passed to the embedding model (e.g. concatenating the title and content). The function should accept a single row as input and return text.
2. `embedding`: The name of the destination column where the embedding will be stored.
Note that the update trigger only fires when the `title` or `content` columns are updated. This is to avoid unnecessary updates to the embedding column when other columns are updated. Make sure that these columns match the columns used in the `embedding_input` function.
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### (Optional) Clearing embeddings on update
Note that our trigger will enqueue new embedding jobs when content is updated, but it will not clear any existing embeddings. This means that an embedding can be temporarily out of sync with the content until the new embedding is generated and updated.
If it is more important to have _accurate_ embeddings than _any_ embedding, you can add another trigger to clear the existing embedding until the new one is generated:
```sql
-- Trigger to clear the embedding column on update
create trigger clear_document_embedding_on_update
before update of title, content -- must match the columns in embedding_input()
on documents
for each row
execute function util.clear_column('embedding');
```
`util.clear_column` is a generic trigger function we created earlier that can be used to clear any column in a table.
- It accepts the column name as an argument. This column must be nullable.
- It requires a `before` trigger with a `for each row` clause.
- It requires the `hstore` extension we created earlier.
This example will clear the `embedding` column whenever the `title` or `content` columns are updated (note the `of title, content` clause). This ensures that the embedding is always in sync with the title and content, but it will result in temporary gaps in search results until the new embedding is generated.
We intentionally use a `before` trigger because it allows us to modify the record before it's written to disk, avoiding an extra `update` statement that would be needed with an `after` trigger.
### 3. Insert and update documents
Let's insert a new document and update its content to see the embedding generation in action:
```sql
-- Insert a new document
insert into documents (title, content)
values
('Understanding Vector Databases', 'Vector databases are specialized...');
-- Immediately check the embedding column
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```
You should observe that the `embedding` column is initially `null` after inserting the document. This is because the embedding generation is asynchronous and will be processed by the Edge Function in the next scheduled task.
Wait up to 10 seconds for the next task to run, then check the `embedding` column again:
```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```
You should see the generated embedding for the document.
Next let's update the content of the document:
```sql
-- Update the content of the document
update documents
set content = 'Vector databases allow you to query...'
where title = 'Understanding Vector Databases';
-- Immediately check the embedding column
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```
You should observe that the `embedding` column is reset to `null` after updating the content. This is because of the trigger we added to clear existing embeddings whenever the content is updated. The embedding will be regenerated by the Edge Function in the next scheduled task.
Wait up to 10 seconds for the next task to run, then check the `embedding` column again:
```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```
You should see the updated embedding for the document.
Finally we'll update the title of the document:
```sql
-- Update the title of the document
update documents
set title = 'Understanding Vector Databases with Supabase'
where title = 'Understanding Vector Databases';
```
You should observe that the `embedding` column is once again reset to `null` after updating the title. This is because the trigger we added to clear existing embeddings fires when either the `content` or `title` columns are updated. The embedding will be regenerated by the Edge Function in the next scheduled task.
Wait up to 10 seconds for the next task to run, then check the `embedding` column again:
```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases with Supabase';
```
You should see the updated embedding for the document.
## Troubleshooting
The `embed` Edge Function processes a batch of embedding jobs and returns a `200 OK` response with a list of completed and failed jobs in the body. For example:
```json
{
"completedJobs": [
{
"jobId": "1",
"id": "1",
"schema": "public",
"table": "documents",
"contentFunction": "embedding_input",
"embeddingColumn": "embedding"
}
],
"failedJobs": [
{
"jobId": "2",
"id": "2",
"schema": "public",
"table": "documents",
"contentFunction": "embedding_input",
"embeddingColumn": "embedding",
"error": "error connecting to openai api"
}
]
}
```
It also returns the number of completed and failed jobs in the response headers. For example:
```
X-Completed-Jobs: 1
X-Failed-Jobs: 1
```
You can also use the `X-Deno-Execution-Id` header to trace the execution of the Edge Function within the [dashboard](/dashboard/project/_/functions) logs.
Each failed job includes an `error` field with a description of the failure. Reasons for a job failing could include:
- An error generating the embedding via external API
- An error connecting to the database
- The edge function being terminated (e.g. due to a wall clock limit)
- Any other error thrown during processing
`pg_net` stores HTTP responses in the `net._http_response` table, which can be queried to diagnose issues with the embedding generation process.
```sql
select
*
from
net._http_response
where
(headers->>'X-Failed-Jobs')::int > 0;
```
## Conclusion
Automating embedding generation and updates in Postgres allow you to build powerful semantic search capabilities without the complexity of managing embeddings manually.
By combining Postgres features like triggers, queues, and other extensions with Supabase Edge Functions, we can create a robust system that handles embedding generation asynchronously and retries failed jobs automatically.
This system can be customized to work with any content and embedding generation service, providing a flexible and scalable solution for semantic search in Postgres.
## See also
- [What are embeddings?](/docs/guides/ai/concepts)
- [Semantic search](/docs/guides/ai/semantic-search)
- [Vector indexes](/docs/guides/ai/vector-indexes)
- [Supabase Edge Functions](/docs/guides/functions)
@@ -277,7 +277,7 @@ There are various ways to improve your pgvector performance. Here are some tips:
It's useful to execute a few thousand “warm-up” queries before going into production. This helps help with RAM utilization. This can also help to determine that you've selected the right compute size for your workload.
### Finetune index parameters
### Fine-tune index parameters
You can increase the Requests per Second by increasing `m` and `ef_construction` or `lists`. This also has an important caveat: building the index takes longer with higher values for these parameters.
@@ -301,7 +301,7 @@ You can increase the Requests per Second by increasing `m` and `ef_construction`
</TabPanel>
<TabPanel id="ivfflat" label="IVFFlat">
<Image
alt="multi database"
src={{
@@ -80,9 +80,9 @@ To start quicker you may use Supabase CLI to spin everything up locally as it al
supabase start
```
This will pull all docker images and run supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
This will pull all docker images and run Supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
Using `supabase-cli` is not required and you can use any other docker image or hosted version of PostgresDB that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
Using `supabase-cli` is not required and you can use any other docker image or hosted version of Postgres that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
### Step 5: Obtain OpenAI API key
@@ -6,7 +6,7 @@ breadcrumb: 'AI Examples'
Supabase provides a [Headless Search Toolkit](https://github.com/supabase/headless-vector-search) for adding "Generative Q&A" to your documentation. The toolkit is "headless", so that you can integrate it into your existing website and style it to match your website theme.
You can see how this works with the Supabase docs. Just hit `cmd+k` and "ask" for something like "what are the features of supabase?". You will see that the response is streamed back, using the information provided in the docs:
You can see how this works with the Supabase docs. Just hit `cmd+k` and "ask" for something like "what are the features of Supabase?". You will see that the response is streamed back, using the information provided in the docs:
![headless search](/docs/img/ai/headless-search/headless.png)
@@ -8,9 +8,9 @@ subtitle: 'Implement image search with the OpenAI CLIP Model and Supabase Vector
The [OpenAI CLIP Model](https://github.com/openai/CLIP) was trained on a variety of (image, text)-pairs. You can use the CLIP model for:
- Text-to-Image / Image-To-Text / Image-to-Image / Text-to-Text Search
- You can fine-tune it on your own image and text data with the regular SentenceTransformers training code.
- You can fine-tune it on your own image and text data with the regular `SentenceTransformers` training code.
[SentenceTransformers](https://www.sbert.net/examples/applications/image-search/README.html) provides models that allow you to embed images and text into the same vector space. You can use this to find similar images as well as to implement image search.
[`SentenceTransformers`](https://www.sbert.net/examples/applications/image-search/README.html) provides models that allow you to embed images and text into the same vector space. You can use this to find similar images as well as to implement image search.
You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search#image-search-with-supabase-vector).
@@ -72,7 +72,7 @@ DB_CONNECTION = "postgresql://postgres:postgres@localhost:54322/postgres"
## Create embeddings for your images
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search/images) or you can find license free images on [unsplash](https://unsplash.com).
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search/images) or you can find license free images on [Unsplash](https://unsplash.com).
Next, create a `seed` method, which will create a new Supabase Vector Collection, generate embeddings for your images, and upsert the embeddings into your database:
@@ -130,7 +130,7 @@ seed = "image_search.main:seed"
search = "image_search.main:search"
```
After activating the virtual environtment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor), selecting the `vecs` schema, and the `image_vectors` database.
After activating the virtual environment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor), selecting the `vecs` schema, and the `image_vectors` database.
## Perform an image search from a text query
@@ -297,7 +297,7 @@ With our database set up, we need to process and store all `.mdx` files in the `
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Run script at build time">
Include the script in your `package.json` script commands to enable Vercel to automaticall run it at build time.
Include the script in your `package.json` script commands to enable Vercel to automatically run it at build time.
</StepHikeCompact.Details>
@@ -91,7 +91,7 @@ You should see a GPT response come back from OpenAI!
## Deploy
Deploy your function to the cloud by runnning:
Deploy your function to the cloud by running:
```bash
supabase functions deploy --no-verify-jwt openai
@@ -84,7 +84,7 @@ bedrock_client = boto3.client(
## Create embeddings for your images
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search/images) or you can find license free images on [unsplash](https://unsplash.com).
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search/images) or you can find license free images on [Unsplash](https://unsplash.com).
To send images to the Amazon Bedrock API we need to need to encode them as `base64` strings. Create the following helper methods:
@@ -188,7 +188,7 @@ seed = "image_search.main:seed"
search = "image_search.main:search"
```
After activating the virtual environtment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your Supabase Dashboard by visiting the [Table Editor](https://supabase.com/dashboard/project/_/editor), selecting the `vecs` schema, and the `image_vectors` table.
After activating the virtual environment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your Supabase Dashboard by visiting the [Table Editor](https://supabase.com/dashboard/project/_/editor), selecting the `vecs` schema, and the `image_vectors` table.
## Perform an image search from a text query
@@ -29,7 +29,7 @@ On the other hand, if you need to scale your application, you will need to [crea
`pgvector` supports two types of indexes: HNSW and IVFFlat. We recommend using [HNSW](/docs/guides/ai/vector-indexes/hnsw-indexes) because of its [performance](https://supabase.com/blog/increase-performance-pgvector-hnsw#hnsw-performance-1536-dimensions) and [robustness against changing data](/docs/guides/ai/vector-indexes/hnsw-indexes#when-should-you-create-hnsw-indexes).
<Image
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute addon"
alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute add-on"
src={{
light: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--light.png',
dark: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--dark.png',
@@ -83,12 +83,12 @@ You can find more examples of how `lists` and `probes` constants affect accuracy
First, a few generic tips which you can pick and choose from:
1. The Supabase managed platform will automatically optimize Postgres configs for you based on your compute addon. But if you self-host, consider **adjusting your Postgres config** based on RAM & CPU cores. See [example optimizations](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08) for more details.
1. The Supabase managed platform will automatically optimize Postgres configs for you based on your compute add-on. But if you self-host, consider **adjusting your Postgres config** based on RAM & CPU cores. See [example optimizations](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08) for more details.
2. Prefer `inner-product` to `L2` or `Cosine` distances if your vectors are normalized (like `text-embedding-ada-002`). If embeddings are not normalized, `Cosine` distance should give the best results with an index.
3. **Pre-warm your database.** Implement the warm-up technique before transitioning to production or running benchmarks.
- Use [pg_prewarm](https://www.postgresql.org/docs/current/pgprewarm.html) to load the index into RAM `select pg_prewarm('vecs.docs_vec_idx');`. This will help to avoid cold cache issues.
- Execute 10,000 to 50,000 "warm-up" queries before each benchmark/prod. This will help to utilize cache and buffers more efficiently.
4. **Establish your workload.** Finetune `m` and `ef_construction` or `lists` constants for the pgvector index to accelerate your queries (at the expense of a slower build times). For instance, for benchmarks with 1,000,000 OpenAI embeddings, we set `m` and `ef_construction` to 32 and 80, and it resulted in 35% higher QPS than 24 and 56 values respectively.
4. **Establish your workload.** Fine-tune `m` and `ef_construction` or `lists` constants for the pgvector index to accelerate your queries (at the expense of a slower build times). For instance, for benchmarks with 1,000,000 OpenAI embeddings, we set `m` and `ef_construction` to 32 and 80, and it resulted in 35% higher QPS than 24 and 56 values respectively.
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the index build parameters, balancing accuracy with queries per second (QPS).
## Going into production
@@ -5,7 +5,7 @@ subtitle: 'Learn how to integrate Supabase with LlamaIndex, a data framework for
breadcrumb: 'AI Integrations'
---
This guide will walk you through a basic example using the LlamaIndex [SupabaseVectorStore](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb).
This guide will walk you through a basic example using the LlamaIndex [`SupabaseVectorStore`](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb).
<DatabaseSetup />
@@ -66,5 +66,5 @@ You can view the inserted items in the [Table Editor](https://supabase.com/dashb
## Resources
- Visit the LlamaIndex + SupabaseVectorStore [docs](https://gpt-index.readthedocs.io/en/latest/examples/vector_stores/SupabaseVectorIndexDemo.html)
- Visit the LlamaIndex + `SupabaseVectorStore` [docs](https://gpt-index.readthedocs.io/en/latest/examples/vector_stores/SupabaseVectorIndexDemo.html)
- Visit the official LlamaIndex [repo](https://github.com/jerryjliu/llama_index/)
+1 -1
View File
@@ -147,7 +147,7 @@ export const run = async () => {
### Advanced metadata filtering
You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](https://supabase.com/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [Postgrest API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).
You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](https://supabase.com/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [PostgREST API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).
```js
import { SupabaseFilterRPCCall, SupabaseVectorStore } from 'langchain/vectorstores/supabase'
@@ -109,7 +109,7 @@ Let's build an Edge Function that will accept an input string and generate an em
Note the two options we pass to `session.run()`:
- `mean_pool`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings.
- `mean_pool`: The first option sets `pooling` to `mean`. Pooling refers to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings.
- `normalize`: The second option tells to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
</StepHikeCompact.Details>
@@ -50,7 +50,7 @@ on document_sections for select to authenticated using (
);
```
<Admonition>
<Admonition type="tip">
In this example, the current user is determined using the built-in `auth.uid()` function when the query is executed through your project's auto-generated [REST API](/docs/guides/api). If you are connecting to your Supabase database through a direct Postgres connection, see [Direct Postgres Connection](#direct-postgres-connection) below for directions on how to achieve the same access control.
@@ -117,7 +117,7 @@ RLS is latency-sensitive, so extra caution should be taken before implementing t
</Admonition>
<Admonition>
<Admonition type="tip">
For data sources other than Postgres, see [Foreign Data Wrappers](/docs/guides/database/extensions/wrappers/overview) for a list of external sources supported today. If your data lives in a source not provided in the list, please contact [support](https://supabase.com/dashboard/support/new) and we'll be happy to discuss your use case.
@@ -161,7 +161,7 @@ import foreign schema public limit to (users, documents)
from server foreign_server into external;
```
<Admonition>
<Admonition type="tip">
This example maps the `authenticated` role in Supabase to the `postgres` user in the external DB. In production, it's best to create a custom user on the external DB that has the minimum permissions necessary to access the information you need.
@@ -224,7 +224,7 @@ where document_sections.embedding <#> embedding < -match_threshold
order by document_sections.embedding <#> embedding;
```
<Admonition>
<Admonition type="caution">
You might be tempted to discard RLS completely and simply filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future.
@@ -61,7 +61,7 @@ create table documents (
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 384 dimensions. Change this to the number of dimensions produced by your embedding model. For example, if you are [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, you would set this number to 384 since that model produces 384 dimensions.
<Admonition>
<Admonition type="tip">
In general, embeddings with fewer dimensions perform best. See our [analysis on fewer dimensions in pgvector](https://supabase.com/blog/fewer-dimensions-are-better-pgvector).
@@ -155,7 +155,7 @@ const { data: documents } = await supabaseClient.rpc('match_documents', {
In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself, then pass it into the above `rpc()` function to match.
<Admonition>
<Admonition type="tip">
Be sure to use embeddings produced from the same embedding model when calculating distance. Comparing embeddings from two different models will produce no meaningful result.
@@ -0,0 +1,118 @@
---
id: automatic-retries-in-supabase-js
title: 'How to do automatic retries with `supabase-js`'
subtitle: 'Learn how to add automatic retries to your Supabase API requests using `fetch-retry`.'
---
<Admonition type="danger" title="Important">
You should only enable retries if your requests fail with network errors (e.g. 520 status from Cloudflare). A high number of retries have the potential to exhaust the Data API connection pool, which could result in lower throughput and failed requests.
</Admonition>
The `fetch-retry` package allows you to add retry logic to `fetch` requests, making it a useful tool for enhancing the resilience of API calls in your Supabase applications. Here's a step-by-step guide on how to integrate `fetch-retry` with the `supabase-js` library.
## 1. Install dependencies
To get started, ensure you have both `supabase-js` and `fetch-retry` installed in your project:
```bash
npm install @supabase/supabase-js fetch-retry
```
## 2. Wrap the fetch function
The `fetch-retry` package works by wrapping the native `fetch` function. You can create a custom fetch instance with retry logic and pass it to the `supabase-js` client.
```javascript
import { createClient } from '@supabase/supabase-js'
import fetchRetry from 'fetch-retry'
// Wrap the global fetch with fetch-retry
const fetchWithRetry = fetchRetry(fetch)
// Create a Supabase client instance with the custom fetch
const supabase = createClient('https://your-supabase-url.supabase.co', 'your-anon-key', {
global: {
fetch: fetchWithRetry,
},
})
```
## 3. Configure retry options
You can configure `fetch-retry` options to control retry behavior, such as the number of retries, retry delay, and which errors should trigger a retry.
Here is an example with custom retry options:
```javascript
const fetchWithRetry = fetchRetry(fetch, {
retries: 3, // Number of retry attempts
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff
retryOn: [520], // Retry only on Cloudflare errors
})
```
In this example, the `retryDelay` function implements an exponential backoff strategy, and retries are triggered only for specific HTTP status codes.
## 4. Using the Supabase client
With `fetch-retry` integrated, you can use the Supabase client as usual. The retry logic will automatically apply to all network requests made by `supabase-js`.
```javascript
async function fetchData() {
const { data, error } = await supabase.from('your_table').select('*')
if (error) {
console.error('Error fetching data:', error)
} else {
console.log('Fetched data:', data)
}
}
fetchData()
```
## 5. Fine-Tuning retries for specific requests
If you need different retry logic for certain requests, you can use the `retryOn` with a custom function to inspect the URL or response and decide whether to retry the request.
```javascript
const fetchWithRetry = fetchRetry(fetch, {
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
retryOn: (attempt, error, response) => {
const shouldRetry
= (attempt: number, error: Error | null, response: Response | null) =>
attempt < 3
&& response
&& response.status == 520 // Cloudflare errors
&& response.url.includes('rpc/your_stored_procedure')
if (shouldRetry(attempt, error, response)) {
console.log(`Retrying request... Attempt #${attempt}`, response)
return true
}
return false
}
})
async function yourStoredProcedure() {
const { data, error } = await supabase
.rpc('your_stored_procedure', { param1: 'value1' });
if (error) {
console.log('Error executing RPC:', error);
} else {
console.log('Response:', data);
}
}
yourStoredProcedure();
```
By using `retryOn` with a custom function, you can define specific conditions for retrying requests. In this example, the retry logic is applied only to requests targeting a specific stored procedure.
## Conclusion
Integrating `fetch-retry` with `supabase-js` is a straightforward way to add robustness to your Supabase API requests. By handling transient errors and implementing retry strategies, you can improve the reliability of your application while maintaining a seamless user experience.
@@ -105,9 +105,9 @@ curl '<SUPABASE_URL>/rest/v1/todos' \
</TabPanel>
</Tabs>
JS Reference: [select()](/docs/reference/javascript/select),
[insert()](/docs/reference/javascript/insert),
[update()](/docs/reference/javascript/update),
[upsert()](/docs/reference/javascript/upsert),
[delete()](/docs/reference/javascript/delete),
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
JS Reference: [`select()`](/docs/reference/javascript/select),
[`insert()`](/docs/reference/javascript/insert),
[`update()`](/docs/reference/javascript/update),
[`upsert()`](/docs/reference/javascript/upsert),
[`delete()`](/docs/reference/javascript/delete),
[`rpc()`](/docs/reference/javascript/rpc) (call Postgres functions).
@@ -258,7 +258,7 @@ jobs:
branch: ${{ github.ref }}
```
Alternatively, you can use a community-supported GitHub action: [generate-supabase-db-types-github-action](https://github.com/lyqht/generate-supabase-db-types-github-action).
Alternatively, you can use a community-supported GitHub action: [`generate-supabase-db-types-github-action`](https://github.com/lyqht/generate-supabase-db-types-github-action).
## Resources
+1 -1
View File
@@ -66,7 +66,7 @@ const { data, error } = await supabase
## Resources
- [Supabase - Get started for free](https://supabase.com)
- [Postgrest Operators](https://postgrest.org/en/stable/api.html#operators)
- [PostgREST Operators](https://postgrest.org/en/stable/api.html#operators)
- [Supabase API: JavaScript select](/docs/reference/javascript/select)
- [Supabase API: JavaScript modifiers](/docs/reference/javascript/using-modifiers)
- [Supabase API: JavaScript filters](/docs/reference/javascript/using-filters)
+1 -1
View File
@@ -7,7 +7,7 @@ description: 'Translate SQL queries to HTTP requests and Supabase client code'
Sometimes it's challenging to translate SQL queries to the equivalent [PostgREST](https://postgrest.org/) request or Supabase client code. Use this tool to help with this translation.
<Admonition type="info">
<Admonition type="note">
PostgREST supports a subset of SQL, so not all SQL queries will translate.
+9
View File
@@ -38,3 +38,12 @@ Supabase Auth works with many popular Auth methods, including Social and Phone A
### Phone Auth
<AuthProviders type="phone" />
## Pricing
Charges apply to Monthly Active Users (MAU), Monthly Active Third-Party Users (Third-Party MAU), and Monthly Active SSO Users (SSO MAU) and Advanced MFA Add-ons. For a detailed breakdown of how these charges are calculated, refer to the following pages:
- [Pricing MAU](/docs/guides/platform/manage-your-usage/monthly-active-users)
- [Pricing Third-Party MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party)
- [Pricing SSO MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-sso)
- [Advanced MFA - Phone](/docs/guides/platform/manage-your-usage/advanced-mfa-phone)
@@ -189,7 +189,7 @@ response = supabase.auth.update_user({
>
<TabPanel id="js" label="JavaScript">
You can use the [`linkIdentity()`](/docs/reference/javascript/auth-linkidentity) method to link an oauth identity to the anonymous user.
You can use the [`linkIdentity()`](/docs/reference/javascript/auth-linkidentity) method to link an OAuth identity to the anonymous user.
```js
const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })
@@ -216,7 +216,7 @@ try await supabase.auth.linkIdentity(provider: .google)
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
You can use the [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity) method to link an oauth identity to the anonymous user.
You can use the [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity) method to link an OAuth identity to the anonymous user.
```kotlin
supabase.auth.linkIdentity(Google)
@@ -225,7 +225,7 @@ supabase.auth.linkIdentity(Google)
</TabPanel>
<TabPanel id="python" label="Python">
You can use the [`link_identity()`](/docs/reference/python/auth-linkidentity) method to link an oauth identity to the anonymous user.
You can use the [`link_identity()`](/docs/reference/python/auth-linkidentity) method to link an OAuth identity to the anonymous user.
```python
response = supabase.auth.link_identity({'provider': 'google'})
@@ -250,7 +250,7 @@ to authenticated
using ( true );
```
<Admonition variant="note" label="Use restrictive policies">
<Admonition type="note" label="Use restrictive policies">
RLS policies are permissive by default, which means that they are combined using an "OR" operator when multiple policies are applied. It is important to construct restrictive policies to ensure that the checks for an anonymous user are always enforced when combined with other policies.
@@ -313,7 +313,7 @@ async function resolveDataConflicts(anonymousUserId, existingUserId) {
## Abuse prevention and rate limits
Since anonymous users are stored in your database, bad actors can abuse the endpoint to increase your database size drastically. It is strongly recommended to [enable invisible Captcha or Cloudflare Turnstile](/docs/guides/auth/auth-captcha) to prevent abuse for anonymous sign-ins. An IP-based rate limit is enforced at 30 requests per hour which can be modified in your [dashboard](/dashboard/project/_/auth/rate-limits). You can refer to the full list of rate limits [here](/docs/guides/platform/going-into-prod#rate-limiting-resource-allocation--abuse-prevention).
Since anonymous users are stored in your database, bad actors can abuse the endpoint to increase your database size drastically. It is strongly recommended to [enable invisible CAPTCHA or Cloudflare Turnstile](/docs/guides/auth/auth-captcha) to prevent abuse for anonymous sign-ins. An IP-based rate limit is enforced at 30 requests per hour which can be modified in your [dashboard](/dashboard/project/_/auth/rate-limits). You can refer to the full list of rate limits [here](/docs/guides/platform/going-into-prod#rate-limiting-resource-allocation--abuse-prevention).
## Automatic cleanup
+11 -11
View File
@@ -41,13 +41,13 @@ In the Settings page, look for the **Sitekey** section and copy the key.
## Enable CAPTCHA protection for your Supabase project
Navigate to the **[Auth](https://supabase.com/dashboard/project/_/settings/auth)** section of your Project Settings in the Supabase Dashboard and find the **Enable Captcha protection** toggle under Settings > Authentication > Bot and Abuse Protection > Enable Captcha protection.
Navigate to the **[Auth](https://supabase.com/dashboard/project/_/settings/auth)** section of your Project Settings in the Supabase Dashboard and find the **Enable CAPTCHA protection** toggle under Settings > Authentication > Bot and Abuse Protection > Enable CAPTCHA protection.
Select your CAPTCHA provider from the dropdown, enter your Captcha **Secret key**, and click **Save**.
Select your CAPTCHA provider from the dropdown, enter your CAPTCHA **Secret key**, and click **Save**.
## Add the CAPTCHA frontend component
The frontend requires some changes to provide the captcha on-screen for the user. This example uses React and the corresponding Captcha React component, but both Captcha providers can be used with any JavaScript framework.
The frontend requires some changes to provide the CAPTCHA on-screen for the user. This example uses React and the corresponding CAPTCHA React component, but both CAPTCHA providers can be used with any JavaScript framework.
<Tabs
scrollable
@@ -77,13 +77,13 @@ Let's create a empty state to store our `captchaToken`
const [captchaToken, setCaptchaToken] = useState()
```
Now lets add the HCaptcha component to the JSX section of our code
Now lets add the `HCaptcha` component to the JSX section of our code
```jsx
<HCaptcha />
```
We will pass it the sitekey we copied from the hCaptcha website as a property along with a onVerify property which takes a callback function. This callback function will have a token as one of its properties. Let's set the token in the state using `setCaptchaToken`
We will pass it the sitekey we copied from the hCaptcha website as a property along with a `onVerify` property which takes a callback function. This callback function will have a token as one of its properties. Let's set the token in the state using `setCaptchaToken`
```jsx
<HCaptcha
@@ -94,7 +94,7 @@ We will pass it the sitekey we copied from the hCaptcha website as a property al
/>
```
Now lets use the captcha token we receive in our Supabase signUp function.
Now lets use the CAPTCHA token we receive in our Supabase signUp function.
```jsx
await supabase.auth.signUp({
@@ -104,9 +104,9 @@ await supabase.auth.signUp({
})
```
We will also need to reset the captcha challenge after we have made a call to the function above.
We will also need to reset the CAPTCHA challenge after we have made a call to the function above.
Create a ref to use on our HCaptcha component.
Create a ref to use on our `HCaptcha` component.
```jsx
const captcha = useRef()
@@ -136,7 +136,7 @@ In order to test that this works locally we will need to use something like [ngr
<TabPanel id="turnstile-2" label="Turnstile">
The frontend requires some changes to provide the captcha on-screen for the user. Turnstile can be used with any JavaScript framework but we'll use React and the Turnstile React component for this example.
The frontend requires some changes to provide the CAPTCHA on-screen for the user. Turnstile can be used with any JavaScript framework but we'll use React and the Turnstile React component for this example.
Install @marsidev/react-turnstile in your project as a dependency.
@@ -183,9 +183,9 @@ await supabase.auth.signUp({
})
```
To test locally, you will need to add localhost to the domain whitelist as per the [Cloudflare docs](https://developers.cloudflare.com/turnstile/reference/testing/)
To test locally, you will need to add localhost to the domain allowlist as per the [Cloudflare docs](https://developers.cloudflare.com/turnstile/reference/testing/)
</TabPanel>
</Tabs>
Run the application and you should now be provided with a captcha challenge.
Run the application and you should now be provided with a CAPTCHA challenge.
@@ -5,7 +5,7 @@ description: 'A prebuilt, customizable React component for authenticating users.
sitemapPriority: 0.5
---
<Admonition type="warning">
<Admonition type="caution">
As of 7th Feb 2024, [this repository](https://github.com/supabase-community/auth-ui) is no longer maintained by the Supabase Team. At the moment, the team does not have capacity to give the expected level of care to this repository. We may revisit Auth UI in the future but regrettably have to leave it on hold for now as we focus on other priorities such as improving the Server-Side Rendering (SSR) package and advanced Auth primitives.
@@ -444,7 +444,7 @@ A full list of the available variables is below:
| `phone_input_label` | Phone number |
| `phone_input_placeholder` | Your phone number |
| `token_input_label` | Token |
| `token_input_placeholder` | Your Otp token |
| `token_input_placeholder` | Your OTP token |
| `button_label` | Verify token |
| `loading_button_label` | Signing in ... |
@@ -36,9 +36,9 @@ void main() async {
### Email Auth
Use a SupaEmailAuth widget to create an email and password signin and signup form. It also contains a button to toggle to display a forgot password form.
Use a `SupaEmailAuth` widget to create an email and password signin and signup form. It also contains a button to toggle to display a forgot password form.
You can pass metadataFields to add additional fields to the form to pass as metadata to Supabase.
You can pass `metadataFields` to add additional fields to the form to pass as metadata to Supabase.
```dart
SupaEmailAuth(
@@ -63,7 +63,7 @@ SupaEmailAuth(
### Magic link Auth
Use SupaMagicAuth widget to create a magic link signIn form.
Use `SupaMagicAuth` widget to create a magic link signIn form.
```dart
SupaMagicAuth(
@@ -75,7 +75,7 @@ SupaMagicAuth(
### Reset password
Use SupaResetPassword to create a password reset form.
Use `SupaResetPassword` to create a password reset form.
```dart
SupaResetPassword(
@@ -87,7 +87,7 @@ SupaResetPassword(
### Phone Auth
Use SupaPhoneAuth to create a phone authentication form.
Use `SupaPhoneAuth` to create a phone authentication form.
```dart
SupaPhoneAuth(
@@ -100,7 +100,7 @@ SupaPhoneAuth(
The package supports login with [official social providers](../../auth#providers).
Use SupaSocialsAuth to create list of social login buttons.
Use `SupaSocialsAuth` to create list of social login buttons.
```dart
SupaSocialsAuth(
@@ -20,7 +20,7 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
size="medium"
className="text-foreground-light border-b mt-8 pb-2"
>
<AccordionItem
header="See legacy docs"
id="legacy-docs"
@@ -196,7 +196,7 @@ You can pass types that were [generated with the Supabase CLI](/docs/reference/j
### Browser client
Creating a new supabase client object:
Creating a new `supabase` client object:
```tsx
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
@@ -205,7 +205,7 @@ import { Database } from '../database.types'
const supabaseClient = createPagesBrowserClient<Database>()
```
Retrieving a supabase client object from the SessionContext:
Retrieving a `supabase` client object from the `SessionContext`:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
@@ -286,7 +286,7 @@ export default LoginPage
## Server-side rendering (SSR)
Create a server supabase client to retrieve the logged in user's session:
Create a server Supabase client to retrieve the logged in user's session:
```jsx pages/profile.js
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
@@ -321,7 +321,7 @@ export const getServerSideProps = async (ctx) => {
## Server-side data fetching with RLS
You can use the server supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
You can use the server Supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
<Tabs
scrollable
@@ -543,7 +543,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
## Protecting API routes
Create a server supabase client to retrieve the logged in user's session:
Create a server Supabase client to retrieve the logged in user's session:
<Tabs
scrollable
@@ -674,7 +674,7 @@ supabase.auth.signUp({
#### Deprecated functions
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The `createBrowserSupabaseClient` and `createServerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for `createClient` functions. The `createBrowserSupabaseClient` and `createServerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
- `createBrowserSupabaseClient` has been replaced with `createPagesBrowserClient`
- `createServerSupabaseClient` has been replaced with `createPagesServerClient`
@@ -917,7 +917,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
- The `useUser` hook now returns the `user` object or `null`.
- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
Creating a new supabase client object:
Creating a new `supabase` client object:
```tsx
import { Database } from '../database.types'
@@ -925,7 +925,7 @@ import { Database } from '../database.types'
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
```
Retrieving a supabase client object from the SessionContext:
Retrieving a `supabase` client object from the `SessionContext`:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
@@ -1304,7 +1304,7 @@ supabase.auth.signUp({
#### Deprecated functions
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The `createMiddlewareSupabaseClient`, `createBrowserSupabaseClient`, `createServerComponentSupabaseClient` and `createRouteHandlerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for `createClient` functions. The `createMiddlewareSupabaseClient`, `createBrowserSupabaseClient`, `createServerComponentSupabaseClient` and `createRouteHandlerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
- `createMiddlewareSupabaseClient` has been replaced with `createMiddlewareClient`
- `createBrowserSupabaseClient` has been replaced with `createClientComponentClient`
@@ -20,7 +20,7 @@ We generally recommend using the new `@supabase/ssr` package instead of `auth-he
size="medium"
className="text-foreground-light border-b mt-8 pb-2"
>
<AccordionItem
header="See legacy docs"
id="legacy-docs"
@@ -155,7 +155,7 @@ export const handle: Handle = async ({ event, resolve }) => {
<Admonition type="note">
Note that we are specifying filterSerializedResponseHeaders here. We need to tell SvelteKit that supabase needs the `content-range` and `x-supabase-api-version` headers.
Note that we are specifying `filterSerializedResponseHeaders` here. We need to tell SvelteKit that Supabase needs the `content-range` and `x-supabase-api-version` headers.
</Admonition>
@@ -213,7 +213,7 @@ export const GET = async ({ url, locals: { supabase } }) => {
### Generate types from your database
In order to get the most out of TypeScript and it's intellisense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](https://supabase.com/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI.
In order to get the most out of TypeScript and its IntelliSense, you should import the generated Database types into the `app.d.ts` type definition file that comes with your SvelteKit project, where `import('./DatabaseDefinitions')` points to the generated types file outlined in [v2 docs here](https://supabase.com/docs/reference/javascript/release-notes#typescript-support) after you have logged in, linked, and generated types through the Supabase CLI.
```ts src/app.d.ts
// src/app.d.ts
@@ -377,13 +377,13 @@ TypeScript types can be [generated with the Supabase CLI](https://supabase.com/d
Access the client inside pages by `$page.data.supabase` or `data.supabase` when using `export let data`.
The usage of `depends` tells sveltekit that this load function should be executed whenever `invalidate` is called to keep the page store in sync.
The usage of `depends` tells SvelteKit that this load function should be executed whenever `invalidate` is called to keep the page store in sync.
`createSupabaseLoadClient` caches the client when running in a browser environment and therefore does not create a new client for every time the load function runs.
#### Setting up the event listener on the client side
We need to create an event listener in the root `+layout.svelte` file in order to catch supabase events being triggered.
We need to create an event listener in the root `+layout.svelte` file in order to catch Supabase events being triggered.
```svelte src/routes/+layout.svelte
<!-- src/routes/+layout.svelte -->
@@ -416,7 +416,7 @@ The usage of `invalidate` tells SvelteKit that the root `+layout.ts` load functi
#### Sign in / sign up / sign out
We can access the supabase instance in our `+page.svelte` file through the data object.
We can access the Supabase instance in our `+page.svelte` file through the data object.
```svelte src/routes/auth/+page.svelte
<!-- // src/routes/auth/+page.svelte -->
@@ -634,7 +634,7 @@ If you try to submit a form with the action `?/createPost` without a valid sessi
To avoid writing the same auth logic in every single route you can also use the handle hook to
protect multiple routes at once. For this to work with your Supabase session, you need to use
Sveltekit's [sequence helper](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) function.
SvelteKit's [sequence helper](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) function.
Edit your `/src/hooks.server.js` with the below:
<Tabs
@@ -1081,7 +1081,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
Since version 0.9 relies on `hooks.server.ts` to setup our client, we no longer need the `hooks.client.ts` in our project for Supabase related code.
#### Typings [#migration-typings]
#### Types [#migration-typings]
<Tabs
scrollable
@@ -1431,7 +1431,7 @@ import '$lib/db'
</TabPanel>
</Tabs>
#### Typings [#migration-typings-0-8]
#### Types [#migration-typings-0-8]
<Tabs
scrollable
@@ -1784,7 +1784,7 @@ export const handle = sequence(auth(), yourHandler)
</TabPanel>
</Tabs>
#### Typings [#migration-typings-0-7]
#### Types [#migration-typings-0-7]
<Tabs
scrollable
+22 -16
View File
@@ -36,7 +36,7 @@ Supabase supports 2 ways to [configure a hook](/dashboard/project/_/auth/hooks)
>
<TabPanel id="postgres-function" label="Postgres Function">
A [postgres function](/docs/guides/database/functions) can be configured as a hook. The function should take in a single argument -- the event of type JSONB -- and return a JSONB object. Since the postgres function runs on your database, the request does not leave your project's instance.
A [Postgres function](/docs/guides/database/functions) can be configured as a hook. The function should take in a single argument -- the event of type JSONB -- and return a JSONB object. Since the Postgres function runs on your database, the request does not leave your project's instance.
</TabPanel>
<TabPanel id="http" label="HTTP Endpoint">
@@ -59,9 +59,9 @@ Sign the payload and grant permissions selectively in order to guard the integri
>
<TabPanel id="sql" label="SQL">
When you configure a postgres function as a hook, Supabase will automatically apply the following grants to the function for these reasons:
When you configure a Postgres function as a hook, Supabase will automatically apply the following grants to the function for these reasons:
- Allow the `supabase_auth_admin` role to execute the function. The `supabase_auth_admin` role is the postgres role that is used by Supabase Auth to make requests to your database.
- Allow the `supabase_auth_admin` role to execute the function. The `supabase_auth_admin` role is the Postgres role that is used by Supabase Auth to make requests to your database.
- Revoke permissions from other roles (e.g. `anon`, `authenticated`, `public`) to ensure the function is not accessible by Supabase Data APIs.
```sql
@@ -81,7 +81,7 @@ revoke execute
You will need to alter your row-level security (RLS) policies to allow the `supabase_auth_admin` role to access tables that you have RLS policies on. You can read more about RLS policies [here](/docs/guides/database/postgres/row-level-security).
Alternatively, you can create your postgres function via the dashboard with the `security definer` tag. The `security definer` tag specifies that the function is to be executed with the privileges of the user that owns it.
Alternatively, you can create your Postgres function via the dashboard with the `security definer` tag. The `security definer` tag specifies that the function is to be executed with the privileges of the user that owns it.
Currently, functions created via the dashboard take on the `postgres` role. Read more about the `security definer` tag [in our database guide](/docs/guides/database/functions#security-definer-vs-invoker)
@@ -96,16 +96,16 @@ HTTP Hooks in Supabase follow the [Standard Webhooks Specification](https://www.
When the request is made to the HTTP hook, you should use the [Standard Webhooks libraries](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) to verify these headers.
When a HTTP hook is created, the secret generated should be of the following format: `v1,whsec_<standard-base64-secret>`:
When a HTTP hook is created, the secret generated should be of the `v1,whsec_<base64-secret>` format:
- `v1` denotes the version of the hook
- `whsec_` signifies that the secret is symmetric
- `<standard-base64-secret>` implies a Standard Base64 encoded secret which can contain the characters `+`, `/` and `=`
- `<base64-secret>` implies a Standard Base64 encoded secret which can contain the characters `+`, `/` and `=`
The secret is used to verify the payload received in your hook. Create an entry in your `.env.local` file to store the secret for each hook that you have. For example:
The secret is used to verify the payload received in your hook. Create an entry in your `.env.local` file to store the `<standard-base64-secret>` portion of the secret for each hook that you have. For example:
```bash
HOOK_SECRETS=<standard-base64-secret>
```ini
SEND_SMS_HOOK_SECRETS=v1,whsec_<base64-secret>
```
There field is expressed in plural rather than singular as there are plans to allow for asymmetric signing and multiple hook secrets for ease of secret rotation. For instance: `<standard-base-64-secret>|<another-standard-base-64-secret>`.
@@ -117,7 +117,7 @@ import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
Deno.serve(async (req) => {
const payload = await req.text()
const hookSecret = Deno.env.get('HOOK_SECRETS')
const hookSecret = Deno.env.get('SEND_SMS_HOOK_SECRETS').replace('v1,whsec_', '')
// Extract headers and security specific fields
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(hookSecret)
@@ -229,19 +229,25 @@ You can visit `SQL Editor > Templates` for hook templates.
</TabPanel>
<TabPanel id="http" label="HTTP">
Modify the `auth.hook.<hook_name>` field and set `uri` to a valid HTTP URI. For example, the `send_sms` hook would take the following fields:
```
```toml
[auth.hook.send_sms]
enabled = true
uri = "http://host.docker.internal:54321/functions/v1/send_sms"
secrets = "env(SEND_SMS_HOOK_SECRET)"
# Comma separated list of secrets
secrets = "env(SEND_SMS_HOOK_SECRETS)"
```
`host.docker.internal` is a special DNS name used in Docker to allow a container to access the host machine's network. This allows the Auth container to reach the Functions container
<Admonition type="info">
`host.docker.internal` is a special DNS name used in Docker to allow a container to access the host machine's network. This allows the Auth container to reach your HTTP function, no matter if it's a Supabase Edge Function or a custom endpoint.
</Admonition>
Fill in the Hook Secret in `supabase/functions/.env`
```bash
SEND_SMS_HOOK_SECRET='v1,whsec_<base64_standard_secret>'
```ini
SEND_SMS_HOOK_SECRETS='v1,whsec_<base64-secret>'
```
Start the function locally:
@@ -344,7 +350,7 @@ Outside of runtime errors, both HTTP Hooks and Postgres Hooks return timeout err
## Available Hooks
Each Hook description contains an example JSONSchema which you can use in conjunction with [JSONSchema Faker](https://json-schema-faker.js.org/) in order to generate a mock payload. For HTTP Hooks, you can also use [the Standard Webhooks Testing Tool](https://www.standardwebhooks.com/simulate) to simulate a request.
Each Hook description contains an example JSON Schema which you can use in conjunction with [JSON Schema Faker](https://json-schema-faker.js.org/) in order to generate a mock payload. For HTTP Hooks, you can also use [the Standard Webhooks Testing Tool](https://www.standardwebhooks.com/simulate) to simulate a request.
<div className="grid md:grid-cols-12 gap-4 not-prose">
{[
@@ -431,7 +431,7 @@ import * as base64 from 'https://denopkg.com/chiefbiiko/base64/mod.ts'
Deno.serve(async (req) => {
const payload = await req.text()
const base64_secret = Deno.env.get('AUTH_CUSTOM_ACCESS_TOKEN_SECRET')
const base64_secret = Deno.env.get('CUSTOM_ACCESS_TOKEN_SECRET').replace('v1,whsec_', '')
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(base64_secret)
try {
@@ -477,7 +477,7 @@ import * as base64 from 'https://denopkg.com/chiefbiiko/base64/mod.ts'
Deno.serve(async (req) => {
const payload = await req.text()
const base64_secret = Deno.env.get('AUTH_CUSTOM_ACCESS_TOKEN_SECRET')
const base64_secret = Deno.env.get('CUSTOM_ACCESS_TOKEN_SECRET').replace('v1,whsec_', '')
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(base64_secret)
try {
@@ -282,7 +282,7 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
},
"email_action_type": {
"type": "string",
"enum": ["signup", "login", "reset_password"]
"enum": ["signup", "invite", "magiclink", "recovery", "email_change", "email"]
},
"site_url": {
"type": "string",
@@ -472,14 +472,14 @@ If you want to send emails through the Supabase Resend integration, which uses R
Create a `.env` file with the following environment variables:
```bash
RESEND_API_KEY=your_resend_api_key
SEND_EMAIL_HOOK_SECRET=<base64_secret>
```ini
RESEND_API_KEY="your_resend_api_key"
SEND_EMAIL_HOOK_SECRET="v1,whsec_<base64_secret>"
```
<Admonition type="info">
<Admonition type="note">
You can generate the secret in the [Auth Hooks](/dashboard/project/_/auth/hooks) section of the Supabase dashboard. Make sure to remove the `v1,whsec_` prefix!
You can generate the secret in the [Auth Hooks](/dashboard/project/_/auth/hooks) section of the Supabase dashboard.
</Admonition>
@@ -502,7 +502,7 @@ import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
import { Resend } from "npm:resend";
const resend = new Resend(Deno.env.get("RESEND_API_KEY") as string);
const hookSecret = Deno.env.get("SEND_EMAIL_HOOK_SECRET") as string;
const hookSecret = (Deno.env.get("SEND_EMAIL_HOOK_SECRET") as string).replace("v1,whsec_", "");
Deno.serve(async (req) => {
if (req.method !== "POST") {
@@ -569,7 +569,7 @@ supabase functions deploy send-email --no-verify-jwt
</TabPanel>
<TabPanel id="http-internationalization-for-emails" label="Add Internationalization for Email Templates">
Your company is expanding to France and Spain. As part of expansion efforts, the company would like to deliver internationalized email templates to best support local users in their native language. Ensure that you have configured `POSTMARK_SERVER_TOKEN` and `AUTH_SEND_EMAIL_HOOK_SECRET` in your `.env` file.
Your company is expanding to France and Spain. As part of expansion efforts, the company would like to deliver internationalized email templates to best support local users in their native language. Ensure that you have configured `POSTMARK_SERVER_TOKEN` and `SEND_EMAIL_HOOK_SECRET` in your `.env` file.
```javascript
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
@@ -650,7 +650,7 @@ Deno.serve(async (req) => {
const payload = await req.text()
const serverToken = Deno.env.get('POSTMARK_SERVER_TOKEN')
const headers = Object.fromEntries(req.headers)
const base64_secret = Deno.env.get('AUTH_SEND_EMAIL_HOOK_SECRET')
const base64_secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET').replace('v1,whsec_', '')
const wh = new Webhook(base64_secret)
const { user, email_data } = wh.verify(payload, headers)
@@ -717,7 +717,7 @@ Deno.serve(async (req) => {
</TabPanel>
{/* <TabPanel id="http-backup-email-provider" label="Add Backup Email Provider">
Your company is rapidly growing and depends heavily on email signups. You'd like to configure a backup email provider in case the email provider runs out of credits during your new product launch. Postmark and Sengrid are used as examples but in practice you can use any email provider.
Ensure that you have configured `POSTMARK_SERVER_TOKEN`, `SENDGRID_API_KEY` and `AUTH_SEND_EMAIL_HOOK_SECRET` in your `.env` file.
Ensure that you have configured `POSTMARK_SERVER_TOKEN`, `SENDGRID_API_KEY` and `SEND_EMAIL_HOOK_SECRET` in your `.env` file.
```javascript
import {
@@ -829,7 +829,7 @@ Deno.serve(async (req) => {
const postmarkServerToken = Deno.env.get("POSTMARK_SERVER_TOKEN");
const sendGridApiKey = Deno.env.get("SENDGRID_API_KEY");
const headers = Object.fromEntries(req.headers);
const base64_secret = Deno.env.get('AUTH_SEND_EMAIL_HOOK_SECRET');
const base64_secret = Deno.env.get('SEND_EMAIL_HOOK_SECRET').replace('v1,whsec_', '');
const wh = new Webhook(base64_secret);
const {
user,
@@ -8,7 +8,7 @@ Runs before a message is sent. Use the hook to:
- Use a regional SMS Provider
- Use alternate messaging channels such as WhatsApp
- Adjust the message body to include platform specific fields such as the [AppHash](https://developers.google.com/identity/sms-retriever/overview)
- Adjust the message body to include platform specific fields such as the [`AppHash`](https://developers.google.com/identity/sms-retriever/overview)
**Inputs**
@@ -464,7 +464,7 @@ const sendTextMessage = async (
Deno.serve(async (req) => {
const payload = await req.text()
const base64_secret = Deno.env.get('SEND_SMS_HOOK_SECRET')
const base64_secret = Deno.env.get('SEND_SMS_HOOK_SECRET').replace('v1,whsec_', '')
const headers = Object.fromEntries(req.headers)
const wh = new Webhook(base64_secret)
try {
@@ -527,12 +527,8 @@ Deno.serve(async (req) => {
Your company is expanding into Latin America and would like to use WhatsApp for higher deliverability. Write a hook to send WhatsApp messages to requests from the continent and SMS messages to all other numbers.
```javascript
import {
Webhook
} from "https://esm.sh/standardwebhooks@1.0.0";
import {
readAll
} from "https://deno.land/std/io/read_all.ts";
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
import { readAll } from "https://deno.land/std/io/read_all.ts";
import * as base64 from "https://denopkg.com/chiefbiiko/base64/mod.ts";
const accountSid: string | undefined = Deno.env.get("TWILIO_ACCOUNT_SID");
@@ -580,7 +576,7 @@ const sendMessage = async (
Deno.serve(async (req) => {
const payload = await req.text();
const base64_secret = Deno.env.get("SEND_SMS_HOOK_SECRET");
const base64_secret = Deno.env.get("SEND_SMS_HOOK_SECRET").replace('v1,whsec_', '');
const headers = Object.fromEntries(req.headers);
const wh = new Webhook(base64_secret);
try {
+1 -1
View File
@@ -111,7 +111,7 @@ Below is an example that creates a new `UnenrollMFA` component that illustrates
- When the component appears on screen, the `supabase.auth.mfa.listFactors()` endpoint
fetches all existing factors together with their details.
- The existing factors for a user are displayed in a table.
- Once the user has selected a factor to unenroll, they can type in the factorId and click **Unenroll**
- Once the user has selected a factor to unenroll, they can type in the `factorId` and click **Unenroll**
which creates a confirmation modal.
<Admonition type="note">
@@ -21,8 +21,6 @@ Below is a flow chart illustrating how the Enrollment and Verify APIs work in th
containerClassName="max-w-[700px]"
/>
Phone MFA is part of the Auth Advanced MFA Add-on and costs an additional $75 per month for the first project in the organization and an additional $10 per month for additional projects.
### Add enrollment flow
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
@@ -311,3 +309,9 @@ function AuthMFA() {
Each code is valid for up to 5 minutes, after which a new one can be sent. Successive codes remain valid until expiry. When possible choose the longest code length acceptable to your use case, at a minimum of 6. This can be configured in the [Authentication Settings](/dashboard/project/_/settings/auth).
Please be aware that Phone MFA is vulnerable to SIM swap attacks where an attacker will call a mobile provider and ask to port the target's phone number to a new SIM card and then use the said SIM card to intercept an MFA code. Please evaluate the your application's tolerance for such an attack. You can read more about SIM swapping attacks [here](https://en.wikipedia.org/wiki/SIM_swap_scam)
## Pricing
<PricingMfaPhone />
For a detailed breakdown of how charges are calculated, refer to [Manage Advanced MFA Phone usage](/docs/guides/platform/manage-your-usage/advanced-mfa-phone).
+1 -1
View File
@@ -63,7 +63,7 @@ They use lists of known email addresses to sign up users to your project with pr
Usually the goal for this behavior is:
- To negatively affect your email sending reputation, after which they might ask for a ransom promising to stop the behavior.
- To cause a short-term or even long-term Denial of Service attack on your service, by preventing new account creation, sign ins with magic links or one-time passwords, or to severely impact important security flows in your application (such as reset password or forgot password).
- To cause a short-term or even long-term Denial of Service attack on your service, by preventing new account creation, signins with magic links or one-time passwords, or to severely impact important security flows in your application (such as reset password or forgot password).
- To force you to reduce the security posture of your project, such as by disabling email confirmations. At that point, they may target specific or a broad number of users by creating an account in their name. Then they can use social engineering techniques to trick them to use your application in such a way that both attacker and victim have access to the same account.
Mitigation strategies:
@@ -5,7 +5,7 @@ description: 'Use Single Sign-On (SSO) authentication on your project with SAML
video: 'https://www.youtube.com/v/em1cpOAXknM'
---
<Admonition>
<Admonition type="tip">
Looking for guides on how to use Single Sign-On with the Supabase dashboard? Head on over to [Enable SSO for Your Organization](/docs/guides/platform/sso).
@@ -13,7 +13,7 @@ Looking for guides on how to use Single Sign-On with the Supabase dashboard? Hea
Supabase Auth supports enterprise-level Single Sign-On (SSO) for any identity providers compatible with the SAML 2.0 protocol. This is a non-exclusive list of supported identity providers:
- Google Workspaces (formerly known as GSuite)
- Google Workspaces (formerly known as G Suite)
- Okta, Auth0
- Microsoft Active Directory, Azure Active Directory, Microsoft Entra
- PingIdentity
@@ -36,14 +36,14 @@ Please note that SAML 2.0 support is offered on plans Pro and above. Check the [
The number of SAML and SSO acronyms can often be overwhelming. Here's a glossary which you can refer back to at any time:
- **Identity Provider**, **IdP**, or **IDP**
An identity provider is a service that manages user accounts at a company or organization. It can verify the identity of a user and exchange that information with your Supabase project and other applications. It acts as a single source of truth for user identities and access rights. Commonly used identity providers are: Microsoft Active Directory (Azure AD, Microsoft Entra), Okta, Google Workspaces (GSuite), PingIdentity, OneLogin, and many others. There are also self-hosted and on-prem versions of identity providers, and sometimes they are accessible only by having access to a company VPN or being in a specific building.
An identity provider is a service that manages user accounts at a company or organization. It can verify the identity of a user and exchange that information with your Supabase project and other applications. It acts as a single source of truth for user identities and access rights. Commonly used identity providers are: Microsoft Active Directory (Azure AD, Microsoft Entra), Okta, Google Workspaces (G Suite), PingIdentity, OneLogin, and many others. There are also self-hosted and on-prem versions of identity providers, and sometimes they are accessible only by having access to a company VPN or being in a specific building.
- **Service Provider**, **SP**
This is the software that is asking for user information from an identity provider. In Supabase, this is your project's Auth server.
- **Assertion**
An assertion is a statement issued by an identity provider that contains information about a user.
- **EntityID**
- **`EntityID`**
A globally unique ID (usually a URL) that identifies an Identity Provider or Service Provider across the world.
- **NameID**
- **`NameID`**
A unique ID (usually an email address) that identifies a user at an Identity Provider.
- **Metadata**
An XML document that describes the features and configuration of an Identity Provider or Service Provider. It can be as a standalone document or as a URL. Usually (but not always) the `EntityID` is the URL at which you can access the Metadata.
@@ -53,7 +53,7 @@ The number of SAML and SSO acronyms can often be overwhelming. Here's a glossary
This is one of the most important SAML URLs. It is the URL where Supabase Auth will accept assertions from an identity provider. Basically, once the identity provider verifies the user's identity it will redirect to this URL and the redirect request will contain the assertion.
- **Binding (Redirect, POST, or Artifact)**
This is a description of the way an identity provider communicates with Supabase Auth. When using the Redirect binding, the communication occurs using HTTP 301 redirects. When it's `POST`, it's using `POST` requests sent with `<form>` elements on a page. When using Artifact, it's using a more secure exchange over a Redirect or `POST`.
- **RelayState**
- **`RelayState`**
State used by Supabase Auth to hold information about a request to verify the identity of a user.
## Important SAML 2.0 information
@@ -62,12 +62,12 @@ Below is information about your project's SAML 2.0 configuration which you can s
| Name | Value |
| --------------------------- | ----------------------------------------------------------------------- |
| EntityID | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
| `EntityID` | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
| Metadata URL | `https://<project>.supabase.co/auth/v1/sso/saml/metadata` |
| Metadata URL<br/>(download) | `https://<project>.supabase.co/auth/v1/sso/saml/metadata?download=true` |
| ACS URL | `https://<project>.supabase.co/auth/v1/sso/saml/acs` |
| SLO URL | `https://<project>.supabase.co/auth/v1/sso/slo` |
| NameID | Required `emailAddress` or `persistent` |
| `NameID` | Required `emailAddress` or `persistent` |
Note that SLO (Single Logout) is not supported at this time with Supabase Auth as it is a rarely supported feature by identity providers. However, the URL is registered and advertised for when this does become available.
@@ -155,7 +155,7 @@ Commonly used SAML 2.0 Identity Providers that support Metadata URLs:
Commonly used SAML 2.0 Identity Providers that only support Metadata XML files:
- Google Workspaces (GSuite)
- Google Workspaces (G Suite)
- Any self-hosted or on-prem identity provider behind a VPN
Once you've obtained the SAML 2.0 Metadata XML file or URL you can [establish a connection](/docs/reference/cli/supabase-sso-add) with your project's Supabase Auth server by running:
@@ -323,7 +323,7 @@ Most SAML 2.0 identity providers use Lightweight Directory Access Protocol (LDAP
**Accessing the stored attributes**
The stored attributes, once mapped, show up in the access token (a JWT) of the user. If you need to look these values up in the database, you can find them in the `auth.identities` table under the `identity_data` JSON column. Identities created for SSO providers have `sso:<uuid-of-provider>` in the `provider` column, while `id` contains the unique NameID of the user account.
The stored attributes, once mapped, show up in the access token (a JWT) of the user. If you need to look these values up in the database, you can find them in the `auth.identities` table under the `identity_data` JSON column. Identities created for SSO providers have `sso:<uuid-of-provider>` in the `provider` column, while `id` contains the unique `NameID` of the user account.
Furthermore, you can find the same identity data under `raw_app_meta_data` inside `auth.users`.
@@ -382,6 +382,12 @@ supabase sso show <provider-id> --project-ref <your-project>
You can use the `-o json` flag to output the information as JSON, should you need to. Other formats may be supported, please use `--help` to see all available options.
## Pricing
<PricingMauSso />
For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active SSO Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-sso).
## Frequently asked questions
### How do I publish my application to an identity provider's marketplace?
+1 -1
View File
@@ -41,7 +41,7 @@ The JSON object starts out looking something like this:
Just note that the more data you store in your token, the longer the encoded string will be.
When we want to send the JWT to the user, we first encode the data using an algorithm such as `HS256`. There are many libraries (and several different algorithms) that can be used to do this encoding/decoding, such as [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken). The signing is as simple as:
When we want to send the JWT to the user, we first encode the data using an algorithm such as `HS256`. There are many libraries (and several different algorithms) that can be used to do this encoding/decoding, such as [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken). The signing is as simple as:
```js
// from https://replit.com/@awalias/jsonwebtokens#index.js
@@ -113,14 +113,14 @@ With Deep Linking, you can configure this redirect to open a specific page. This
</TabPanel>
<TabPanel id="flutter" label="Flutter">
// Currently supabase_flutter supports deep links on Android, iOS, Web, MacOS and Windows.
// Currently supabase_flutter supports deep links on Android, iOS, Web, macOS and Windows.
### Deep link config
- Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
- You need to enter your app redirect callback on `Additional Redirect URLs` field.
The redirect callback url should have this format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.flutterquickstart://login-callback` is just an example, you can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
The redirect callback URL should have this format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.flutterquickstart://login-callback` is just an example, you can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
![Supabase console deep link setting](/docs/img/deeplink-setting.png)
@@ -269,11 +269,10 @@ With Deep Linking, you can configure this redirect to open a specific page. This
You can achieve it with [url_protocol](https://pub.dev/packages/url_protocol) inside you app.
The most relevant solution is to include those registry modifications into your installer to allow
for deregistration.
The most relevant solution is to include those registry modifications into your installer to allow for deregistration.
</TabPanel>
<TabPanel id="macos" label="MacOS">
<TabPanel id="macos" label="macOS">
Add this XML chapter in your `macos/Runner/Info.plist` inside `<plist version="1.0"><dict>` chapter:
@@ -311,7 +310,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
1. Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link.
The redirect callback url should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
The redirect callback URL should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
![Supabase console deep link setting](/docs/img/deeplink-setting.png)
@@ -349,7 +348,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
1. Go to your [auth settings](https://supabase.com/dashboard/project/_/auth/url-configuration) page.
2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link.
The redirect callback url should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
The redirect callback URL should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used.
Now, edit the Android manifest to make sure the app opens when the user clicks on the magic link.
@@ -394,7 +393,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
}
```
The user will now be authenticated when your app receives a valid deeplink!
The user will now be authenticated when your app receives a valid deep link!
</TabPanel>
</Tabs>
+3 -3
View File
@@ -465,7 +465,7 @@ data = supabase.auth.sign_up({
>
<TabPanel id="js" label="JavaScript">
When your user signs in, call [signInWithPassword()](/docs/reference/javascript/auth-signinwithpassword) with their email address and password:
When your user signs in, call [`signInWithPassword()`](/docs/reference/javascript/auth-signinwithpassword) with their email address and password:
```js
async function signInWithEmail() {
@@ -479,7 +479,7 @@ async function signInWithEmail() {
</TabPanel>
<TabPanel id="dart" label="Dart">
When your user signs in, call [signInWithPassword()](/docs/reference/dart/auth-signinwithpassword) with their email address and password:
When your user signs in, call [`signInWithPassword()`](/docs/reference/dart/auth-signinwithpassword) with their email address and password:
```dart
Future<void> signInWithEmail() async {
@@ -576,7 +576,7 @@ try await supabase.auth.resetPasswordForEmail(
<TabPanel id="kotlin" label="Kotlin">
```kotlin
supabase.gotrue.sendRecoveryEmail(
supabase.auth.resetPasswordForEmail(
email = "valid.email@supabase.io",
redirectUrl = "http://example.com/account/update-password"
)
@@ -5,7 +5,7 @@ description: 'Learn about logging in to your platform using SMS one-time passwor
Phone Login is a method of authentication that allows users to log in to a website or application without using a password. The user authenticates through a one-time password (OTP) sent via a channel (SMS or WhatsApp).
<Admonition type="info">
<Admonition type="note">
At this time, `WhatsApp` is only supported as a channel for the Twilio and Twilio Verify Providers.
@@ -72,7 +72,7 @@ hideToc: true
Start the development server, go to http://localhost:3000 in a browser, and you should see the contents of `app/page.tsx`.
To sign up a new user, navigate to http://localhost:3000/login, and click `Sign Up Now`.
To sign up a new user, navigate to http://localhost:3000/sign-up, and click `Sign up`. *NOTE: .env.example must be renamed to .env.local before this route becomes available*
</StepHikeCompact.Details>
@@ -376,7 +376,7 @@ export const handle: Handle = async ({ event, resolve }) => {
<TabPanel id="layout" label="Root Layout Load">
Page components can get access to the supabase client from the `data` object due to this load function.
Page components can get access to the Supabase client from the `data` object due to this load function.
```ts +layout.ts
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
@@ -163,12 +163,13 @@ To prevent TypeScript errors, add type definitions for the new `event.locals` pr
```ts src/app.d.ts
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'
import type { Database } from './database.types.ts' // import generated types
declare global {
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient
supabase: SupabaseClient<Database>
safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
session: Session | null
user: User | null
@@ -63,7 +63,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally please revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring meeting in your calendar!
7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers).
<Admonition>
<Admonition type="tip">
Use this tool to generate a new Apple client secret. No keys leave your browser! Be aware that this tool does not currently work in Safari, so please use Firefox or a Chrome-based browser instead.
@@ -280,7 +280,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally please revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring reminder in your calendar!
7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers).
<Admonition>
<Admonition type="tip">
Use this tool to generate a new Apple client secret. No keys leave your browser! Be aware that this tool does not currently work in Safari, so please use Firefox or a Chrome-based browser instead.
@@ -348,12 +348,12 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
</Admonition>
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
## Using native sign in with Apple in Kotlin
When using [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On iOS it uses Native Apple Login automatically and on other platforms it uses `gotrue.signInWith(Apple)`.
When using [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/), you can use the [compose-auth](https://supabase.com/docs/reference/kotlin/installing) plugin. On iOS it uses Native Apple Login automatically and on other platforms it uses `gotrue.signInWith(Apple)`.
**Initialize the Supabase Client**
@@ -126,7 +126,7 @@ Supabase Auth requires that Azure returns a valid email address. Therefore you m
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `azure` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `azure` as the `provider`:
```js
async function signInWithAzure() {
@@ -142,7 +142,7 @@ async function signInWithAzure() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `azure` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `azure` as the `provider`:
```dart
Future<void> signInWithAzure() async {
@@ -4,7 +4,7 @@ title: 'Login with Bitbucket'
description: 'Add Bitbucket OAuth to your Supabase project'
---
To enable Bitbucket Auth for your project, you need to set up a BitBucket OAuth application and add the application credentials to your Supabase Dashboard.
To enable Bitbucket Auth for your project, you need to set up a Bitbucket OAuth application and add the application credentials to your Supabase Dashboard.
## Overview
@@ -57,7 +57,7 @@ Setting up Bitbucket logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `bitbucket` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `bitbucket` as the `provider`:
```js
async function signInWithBitbucket() {
@@ -70,7 +70,7 @@ async function signInWithBitbucket() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `bitbucket` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `bitbucket` as the `provider`:
```dart
Future<void> signInWithBitbucket() async {
@@ -56,7 +56,7 @@ Setting up Discord logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `discord` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `discord` as the `provider`:
```js
async function signInWithDiscord() {
@@ -69,7 +69,7 @@ async function signInWithDiscord() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `discord` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `discord` as the `provider`:
```dart
Future<void> signInWithDiscord() async {
@@ -73,7 +73,7 @@ Under `Build Your App`, click on `Use Cases` screen. From there, do the followin
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `facebook` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `facebook` as the `provider`:
```js
async function signInWithFacebook() {
@@ -86,7 +86,7 @@ async function signInWithFacebook() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `facebook` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `facebook` as the `provider`:
```dart
Future<void> signInWithFacebook() async {
@@ -102,7 +102,7 @@ Future<void> signInWithFacebook() async {
</TabPanel>
<TabPanel id="swift" label="Swift">
When your user signs in, call [signInWithOAuth()](/docs/reference/swift/auth-signinwithoauth) with `facebook` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/swift/auth-signinwithoauth) with `facebook` as the `provider`:
```swift
func signInWithFacebook() async throws {
@@ -57,7 +57,7 @@ Setting up Figma logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `figma` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `figma` as the `provider`:
```js
async function signInWithFigma() {
@@ -70,7 +70,7 @@ async function signInWithFigma() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/flutter/auth-signinwithoauth) with `figma` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/flutter/auth-signinwithoauth) with `figma` as the `provider`:
```dart
Future<void> signInWithFigma() async {
@@ -51,7 +51,7 @@ Copy your new OAuth credentials
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `github` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `github` as the `provider`:
```js
async function signInWithGithub() {
@@ -64,7 +64,7 @@ async function signInWithGithub() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `github` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `github` as the `provider`:
```dart
Future<void> signInWithGithub() async {
@@ -80,7 +80,7 @@ Future<void> signInWithGithub() async {
</TabPanel>
<TabPanel id="swift" label="Swift">
When your user signs in, call [signInWithOAuth](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`:
When your user signs in, call [`signInWithOAuth`](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`:
```swift
func signInWithGithub() async throws {
@@ -54,7 +54,7 @@ Setting up GitLab logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `gitlab` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `gitlab` as the `provider`:
```js
async function signInWithGitLab() {
@@ -67,7 +67,7 @@ async function signInWithGitLab() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `gitlab` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `gitlab` as the `provider`:
```dart
Future<void> signInWithGitLab() async {
@@ -27,7 +27,7 @@ To support Sign In with Google, you need to configure the Google provider for yo
For web applications, you can set up your signin button two different ways:
- Use your own [application code](#application-code) for the button.
- Use [Google's pre-built sign-in or OneTap flows](#google-pre-built).
- Use [Google's pre-built sign-in or One Tap flows](#google-pre-built).
### Application code configuration
@@ -78,7 +78,7 @@ To use Google's pre-built signin buttons:
1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for.
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app.
- You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store.
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_.
@@ -94,7 +94,7 @@ To use Google's pre-built signin buttons:
- For both Android and iOS, you will need to create a Web client ID in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials).
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app.
- You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store.
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. If you need iOS support, enable the `Skip nonce check` option.
4. For iOS, add the `CFBundleURLTypes` attributes below into the `/ios/Runner/Info.plist` file.
@@ -129,7 +129,7 @@ To use Google's pre-built signin buttons:
<TabPanel id="swift" label="Swift">
Google sign-in with Supabase is done through the [GoogleSignIn-iOS](https://github.com/google/GoogleSignIn-iOS) package.
Google sign-in with Supabase is done through the [`GoogleSignIn-iOS`](https://github.com/google/GoogleSignIn-iOS) package.
When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth.
@@ -187,7 +187,7 @@ To use Google's pre-built signin buttons:
1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for.
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple App Store.
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_.
@@ -548,7 +548,7 @@ Future<void> _nativeGoogleSignIn() async {
### Web, macOS, Windows, and Linux
Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [signInWithOAuth](docs/reference/dart/auth-signinwithoauth) method.
Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [`signInWithOAuth`](docs/reference/dart/auth-signinwithoauth) method.
This method of signing in is web based, and will open a browser window to perform the sign in. For non-web platforms, the user is brought back to the app via [deep linking](/docs/guides/auth/native-mobile-deep-linking?platform=flutter).
@@ -595,7 +595,7 @@ implementation ("com.google.android.libraries.identity.googleid:googleid:<latest
implementation("androidx.credentials:credentials-play-services-auth:<latest version>")
```
Add the following proguard rules to your `proguard-rules.pro` file:
Add the following ProGuard rules to your `proguard-rules.pro` file:
```proguard
-if class androidx.credentials.CredentialManager
@@ -52,7 +52,7 @@ This will serve as the `client_id` when you make API calls to authenticate the u
- Go to `Product settings` > `Kakao Login` > `Security`.
- Click on the `Kakao Login` switch to enable Kakao Login.
- Click on `generate code` at the bottom to generate the `Client secret code` -- this will serve as a `client_secret` for your supabase project.
- Click on `generate code` at the bottom to generate the `Client secret code` -- this will serve as a `client_secret` for your Supabase project.
- Make sure you enabled `Client secret code` by selecting `enable` from the `Activation state` section.
## Additional configurations on Kakao Developers portal
@@ -79,7 +79,7 @@ This will serve as the `client_id` when you make API calls to authenticate the u
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `kakao` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `kakao` as the `provider`:
```js
async function signInWithKakao() {
@@ -92,7 +92,7 @@ async function signInWithKakao() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `kakao` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `kakao` as the `provider`:
```dart
Future<void> signInWithKakao() async {
@@ -10,16 +10,16 @@ To enable Keycloak Auth for your project, you need to set up an Keycloak OAuth a
To get started with Keycloak, you can run it in a docker container with: `docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev`
This guide will be assuming that you are running keycloak in a docker container as described in the command above.
This guide will be assuming that you are running Keycloak in a docker container as described in the command above.
Keycloak OAuth consists of five broad steps:
- Create a new client in your specified keycloak realm.
- Create a new client in your specified Keycloak realm.
- Obtain the `issuer` from the "OpenID Endpoint Configuration". This will be used as the `Keycloak URL`.
- Ensure that the new client has the "Client Protocol" set to "openid-connect" and the "Access Type" is set to "confidential".
- Ensure that the new client has the "Client Protocol" set to `openid-connect` and the "Access Type" is set to "confidential".
- The `Client ID` of the client created will be used as the `client id`.
- Obtain the `Secret` from the credentials tab which will be used as the `client secret`.
- Add the callback url of your application to your allowlist.
- Add the callback URL of your application to your allowlist.
## Access your Keycloak admin console
@@ -43,7 +43,7 @@ The "Client ID" of the created client will serve as the `client_id` when you mak
After you've created the client successfully, ensure that you set the following settings:
1. The "Client Protocol" should be set to "openid-connect".
1. The "Client Protocol" should be set to `openid-connect`.
2. The "Access Type" should be set to "confidential".
3. The "Valid Redirect URIs" should be set to: `https://<project-ref>.supabase.co/auth/v1/callback`.
@@ -72,7 +72,7 @@ Since Keycloak version 22, the `openid` scope must be passed. Add this to the [`
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `keycloak` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `keycloak` as the `provider`:
```js
async function signInWithKeycloak() {
@@ -88,7 +88,7 @@ async function signInWithKeycloak() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `keycloak` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `keycloak` as the `provider`:
```dart
Future<void> signInWithKeycloak() async {
@@ -163,5 +163,5 @@ suspend fun signOut() {
## Resources
- You can find the keycloak openid endpoint configuration under the realm settings.
- You can find the Keycloak OpenID endpoint configuration under the realm settings.
![Keycloak OpenID Endpoint Configuration](/docs/img/guides/auth-keycloak/keycloak-openid-endpoint-config.png)
@@ -59,7 +59,7 @@ Ensure that the appropriate scopes have been added under OAuth 2.0 Scopes at the
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `linkedin_oidc` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `linkedin_oidc` as the `provider`:
```js
async function signInWithLinkedIn() {
@@ -72,7 +72,7 @@ async function signInWithLinkedIn() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `linkedin_oidc` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `linkedin_oidc` as the `provider`:
```dart
Future<void> signInWithLinkedIn() async {
@@ -23,7 +23,7 @@ Setting up Notion logins for your application consists of 3 parts:
- Once logged in, go to [notion.so/my-integrations](https://notion.so/my-integrations) and create a new integration.
- When creating your integration, ensure that you select "Public integration" under "Integration type" and "Read user information including email addresses" under "Capabilities".
- You will need to add a redirect uri, see [Add the redirect uri](#add-the-redirect-uri)
- You will need to add a redirect URI, see [Add the redirect URI](#add-the-redirect-uri)
- Once you've filled in the necessary fields, click "Submit" to finish creating the integration.
![notion.so](/docs/img/guides/auth-notion/notion-developer.png)
@@ -57,7 +57,7 @@ Setting up Notion logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `notion` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `notion` as the `provider`:
```js
async function signInWithNotion() {
@@ -70,7 +70,7 @@ async function signInWithNotion() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `notion` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `notion` as the `provider`:
```dart
Future<void> signInWithNotion() async {
@@ -75,7 +75,7 @@ Under `Scopes`:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `slack_oidc` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `slack_oidc` as the `provider`:
```js
async function signInWithSlack() {
@@ -88,7 +88,7 @@ async function signInWithSlack() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `slack` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `slack` as the `provider`:
```dart
Future<void> signInWithSlack() async {
@@ -69,7 +69,7 @@ The following outlines the steps to sign in using Spotify with Supabase Auth.
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `spotify` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `spotify` as the `provider`:
```js
async function signInWithSpotify() {
@@ -82,7 +82,7 @@ async function signInWithSpotify() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `spotify` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `spotify` as the `provider`:
```dart
Future<void> signInWithSpotify() async {
@@ -41,7 +41,7 @@ Setting up Twitch logins for your application consists of 3 parts:
- Enter the name of your application.
- Type or paste your `OAuth Redirect URL` (the callback URL from the previous step.)
- Select a category for your app.
- Check the Captcha box and click `Create`.
- Check the CAPTCHA box and click `Create`.
## Retrieve your Twitch OAuth client ID and client secret
@@ -72,7 +72,7 @@ Setting up Twitch logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `twitch` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `twitch` as the `provider`:
```js
async function signInWithTwitch() {
@@ -85,7 +85,7 @@ async function signInWithTwitch() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `twitch` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `twitch` as the `provider`:
```dart
Future<void> signInWithTwitch() async {
@@ -63,7 +63,7 @@ Setting up Twitter logins for your application consists of 3 parts:
<CreateClientSnippet />
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `twitter` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/javascript/auth-signinwithoauth) with `twitter` as the `provider`:
```js
async function signInWithTwitter() {
@@ -76,7 +76,7 @@ async function signInWithTwitter() {
</TabPanel>
<TabPanel id="flutter" label="Flutter">
When your user signs in, call [signInWithOAuth()](/docs/reference/dart/auth-signinwithoauth) with `twitter` as the `provider`:
When your user signs in, call [`signInWithOAuth()`](/docs/reference/dart/auth-signinwithoauth) with `twitter` as the `provider`:
```dart
Future<void> signInWithTwitter() async {
Loaded 100 of 893 files, more files were not shown because too many files have changed in this diff. Show more