Merge branch 'master' into chore/use-completions-api

This commit is contained in:
Terry Sutton committed 2024-02-19 15:16:11 -03:30
commit c0ae021d32
1431 files changed
+51846 -22755

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
name: Autofix Linting Checks
on:
pull_request:
types:
- labeled
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
autofix:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
steps:
- name: Calculate number of commits
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
sparse-checkout: apps
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Install required dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run Prettier in fix mode
run: |
npx prettier --write 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
- name: Commit changes and push to existing branch
run: |
git checkout -- package.json
git checkout -- package-lock.json
git config --global user.name 'github-tidy-bot'
git config --global user.email 'github-tidy-bot@supabase.com'
if [[ `git status --porcelain` ]]; then
echo "[bot] Changes detected, committing."
echo "[bot] Running in non-squash mode."
git commit -am "ci: Autofix updates from GitHub workflow"
git push
else
echo "[bot] No changes detected, nothing to commit."
fi
+47
View File
@@ -0,0 +1,47 @@
name: Playwright Tests
on:
push:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
pull_request:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- uses: supabase/setup-cli@v1
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:playwright
# - uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 30
+5 -4
View File
@@ -27,7 +27,7 @@ jobs:
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
npx prettier -c 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -50,7 +50,7 @@ jobs:
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -63,13 +63,14 @@ jobs:
with:
sparse-checkout: |
apps/docs/pages
apps/docs/content
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
grep -lr '```sql' apps/docs/pages/**/*.mdx | xargs npx prettier -c
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs npx prettier -c
+5
View File
@@ -38,6 +38,7 @@ jobs:
with:
sparse-checkout: |
apps/docs
apps/www/.env.local.example
supabase
- name: Setup node
@@ -54,6 +55,10 @@ jobs:
- name: Run migrations
run: npx supabase db push
# Need the miscellaneous use API, which is available publicly (by design) in www
- name: Copy environment variables
run: cp apps/www/.env.local.example apps/docs/.env
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
+7 -1
View File
@@ -7,5 +7,11 @@
"endOfLine": "lf",
"sqlKeywordCase": "lower",
"pluginSearchDirs": false,
"plugins": ["prettier-plugin-sql-cst"]
"plugins": ["prettier-plugin-sql-cst"],
"overrides": [
{
"files": "**/*.json",
"options": { "parser": "json" }
}
]
}
+9 -1
View File
@@ -71,7 +71,13 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
npm install # install dependencies
```
2. After that you can run the apps simultaneously with the following.
2. Copy the example `.env.local.example` to `.env.local`
```sh
cp apps/www/.env.local.example apps/www/.env.local
```
3. After that you can run the apps simultaneously with the following.
```sh
npm run dev # start all the applications
```
@@ -92,6 +98,8 @@ You can run any of the sites individually by using the scope name. For example:
npm run dev:www
```
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
#### Shared components
The monorepo has a set of shared components under `/packages`:
+1 -2
View File
@@ -39,7 +39,6 @@ To see how to Contribute, visit [Getting Started](./DEVELOPERS.md)
- [Email Support](https://supabase.com/docs/support#business-support). Best for: problems with your database or infrastructure.
- [Discord](https://discord.supabase.com). Best for: sharing your applications and hanging out with the community.
## How it works
Supabase is a combination of open source tools. We’re building the features of Firebase using enterprise-grade, open source products. If the tools and communities exist, with an MIT, Apache 2, or equivalent open license, we will use and support that tool. If the tool doesn't exist, we build and open source it ourselves. Supabase is not a 1-to-1 mapping of Firebase. Our aim is to give developers a Firebase-like developer experience using open source tools.
@@ -49,7 +48,7 @@ Supabase is a combination of open source tools. We’re building the features of
Supabase is a [hosted platform](https://supabase.com/dashboard). You can sign up and start using Supabase without installing anything.
You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development).
![Architecture](https://github.com/supabase/supabase/blob/master/apps/docs/public/img/supabase-architecture.png)
![Architecture](apps/docs/public/img/supabase-architecture.svg)
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
+18 -9
View File
@@ -8,56 +8,64 @@
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Book.woff) format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BookItalic.woff) format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Medium.woff) format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-MediumItalic.woff) format('woff');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Bold.woff) format('woff');
font-weight: 700;
font-style: 600;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BoldItalic.woff) format('woff');
font-style: 600;
font-style: italic;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
url(/fonts/custom/CustomFont-Black.woff) format('woff');
font-weight: 800;
font-style: normal;
}
@font-face {
font-family: 'circular';
src: url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
src:
url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
url(/fonts/custom/CustomFont-BlackItalic.woff) format('woff');
font-weight: 800;
font-style: italic;
@@ -66,7 +74,8 @@
@font-face {
font-family: 'source code pro';
src: url('/fonts/source-code-pro/SourceCodePro-Regular.eot');
src: url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
src:
url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
url('/fonts/source-code-pro/SourceCodePro-Regular.woff') format('woff'),
url('/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'),
url('/fonts/source-code-pro/SourceCodePro-Regular.svg#SourceCodePro-Regular') format('svg');
@@ -1,5 +1,5 @@
'use client'
import ThemeToggle from '@ui/components/ThemeProvider/ThemeToggle'
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
export default function ThemeSwitcher() {
return <ThemeToggle />
+1 -1
View File
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['ui'],
transpilePackages: ['ui', 'ui-patterns'],
webpack: (config, { dev, isServer, webpack, nextRuntime }) => {
config.module.rules.push({
test: /\.node$/,
+2 -1
View File
@@ -19,7 +19,7 @@
"common": "*",
"config": "*",
"dayjs": "^1.11.10",
"eslint-config-next": "^14.0.3",
"eslint-config-next": "^14.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^14.0.3",
@@ -29,6 +29,7 @@
"react-flow": "^1.0.3",
"sql-formatter": "^13.1.0",
"ui": "*",
"ui-patterns": "*",
"valtio": "^1.12.0",
"zod": "^3.22.4"
},
+1 -1
View File
@@ -5,8 +5,8 @@ export default config({
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
'../../packages/ui/src/**/*.{tsx,ts,js}',
'../../packages/ui-patterns/**/*.{tsx,ts,js}',
],
theme: {
extend: {
-12
View File
@@ -1,12 +0,0 @@
declare module 'sse.js' {
export type SSEOptions = EventSourceInit & {
headers?: Record<string, string>
payload?: string
method?: string
}
export class SSE extends EventSource {
constructor(url: string | URL, sseOptions?: SSEOptions)
stream(): void
}
}
+58 -44
View File
@@ -1,4 +1,10 @@
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
@@ -36,18 +42,18 @@ export interface Database {
}
Relationships: [
{
foreignKeyName: 'messages_thread_id_fkey'
columns: ['thread_id']
foreignKeyName: "messages_thread_id_fkey"
columns: ["thread_id"]
isOneToOne: false
referencedRelation: 'threads'
referencedColumns: ['id']
referencedRelation: "threads"
referencedColumns: ["id"]
},
{
foreignKeyName: 'messages_user_id_fkey'
columns: ['user_id']
foreignKeyName: "messages_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
@@ -72,11 +78,11 @@ export interface Database {
}
Relationships: [
{
foreignKeyName: 'threads_user_id_fkey'
columns: ['user_id']
foreignKeyName: "threads_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
@@ -92,18 +98,18 @@ export interface Database {
}
Relationships: [
{
foreignKeyName: 'messages_thread_id_fkey'
columns: ['thread_id']
foreignKeyName: "messages_thread_id_fkey"
columns: ["thread_id"]
isOneToOne: false
referencedRelation: 'threads'
referencedColumns: ['id']
referencedRelation: "threads"
referencedColumns: ["id"]
},
{
foreignKeyName: 'messages_user_id_fkey'
columns: ['user_id']
foreignKeyName: "messages_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
@@ -122,22 +128,23 @@ export interface Database {
export type Tables<
PublicTableNameOrOptions extends
| keyof (Database['public']['Tables'] & Database['public']['Views'])
| keyof (Database["public"]["Tables"] & Database["public"]["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] &
Database[PublicTableNameOrOptions['schema']]['Views'])
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions['schema']]['Tables'] &
Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends {
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (Database['public']['Tables'] &
Database['public']['Views'])
? (Database['public']['Tables'] & Database['public']['Views'])[PublicTableNameOrOptions] extends {
: PublicTableNameOrOptions extends keyof (Database["public"]["Tables"] &
Database["public"]["Views"])
? (Database["public"]["Tables"] &
Database["public"]["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
@@ -145,18 +152,20 @@ export type Tables<
: never
export type TablesInsert<
PublicTableNameOrOptions extends keyof Database['public']['Tables'] | { schema: keyof Database },
PublicTableNameOrOptions extends
| keyof Database["public"]["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof Database['public']['Tables']
? Database['public']['Tables'][PublicTableNameOrOptions] extends {
: PublicTableNameOrOptions extends keyof Database["public"]["Tables"]
? Database["public"]["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
@@ -164,18 +173,20 @@ export type TablesInsert<
: never
export type TablesUpdate<
PublicTableNameOrOptions extends keyof Database['public']['Tables'] | { schema: keyof Database },
PublicTableNameOrOptions extends
| keyof Database["public"]["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof Database['public']['Tables']
? Database['public']['Tables'][PublicTableNameOrOptions] extends {
: PublicTableNameOrOptions extends keyof Database["public"]["Tables"]
? Database["public"]["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
@@ -183,12 +194,15 @@ export type TablesUpdate<
: never
export type Enums<
PublicEnumNameOrOptions extends keyof Database['public']['Enums'] | { schema: keyof Database },
PublicEnumNameOrOptions extends
| keyof Database["public"]["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions['schema']]['Enums']
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName]
: PublicEnumNameOrOptions extends keyof Database['public']['Enums']
? Database['public']['Enums'][PublicEnumNameOrOptions]
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof Database["public"]["Enums"]
? Database["public"]["Enums"][PublicEnumNameOrOptions]
: never
@@ -0,0 +1,11 @@
import { lazy, Suspense } from 'react'
const Generator = lazy(() => import('./AppleSecretGenerator'))
export function AppleSecretGenerator() {
return (
<Suspense>
<Generator />
</Suspense>
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { IconPanel } from 'ui-patterns/IconPanel'
import providers from '../data/authProviders'
import { IconPanel } from 'ui'
import Link from 'next/link'
export default function AuthProviders({ type }: { type: string }) {
@@ -46,8 +46,12 @@ const Heading: React.FC<React.PropsWithChildren<Props>> = ({ tag, customAnchor,
<HeadingTag id={anchor} ref={ref} className="group scroll-mt-24">
{removeAnchor(children)}
{anchor && (
<a href={link} className="ml-2 opacity-0 group-hover:opacity-100 transition">
#
<a
href={link}
aria-hidden="true"
className="ml-2 opacity-0 group-hover:opacity-100 transition"
>
<span aria-hidden="true">#</span>
</a>
)}
</HeadingTag>
+14
View File
@@ -0,0 +1,14 @@
export function DateFormat({
date,
options,
}: {
date: string
options?: Intl.DateTimeFormatOptions
}) {
const dateObject = new Date(date)
return (
<time dateTime={dateObject.toISOString()} suppressHydrationWarning>
{dateObject.toLocaleDateString(undefined, options)}
</time>
)
}
+3 -3
View File
@@ -1,7 +1,7 @@
import React from 'react'
import React, { type PropsWithChildren } from 'react'
import { LazyMotion, domAnimation, m } from 'framer-motion'
const DocsCoverLogo = () => {
const DocsCoverLogo = (props: PropsWithChildren) => {
const pathMotionConfig = {
initial: { pathLength: 0 },
animate: { pathLength: 1 },
@@ -22,7 +22,7 @@ const DocsCoverLogo = () => {
}
return (
<div className="w-[60px] md:w-[150px] [&_svg]">
<div className="w-[60px] md:w-[150px] [&_svg]" {...props}>
<LazyMotion features={domAnimation}>
<m.svg
width="100%"
@@ -1,7 +1,8 @@
import Link from 'next/link'
import React, { useState } from 'react'
import { extensions } from 'shared-data'
import { GlassPanel, IconX, Input } from 'ui'
import { IconX, Input } from 'ui'
import { GlassPanel } from 'ui-patterns/GlassPanel'
type Extension = {
name: string
@@ -116,28 +117,6 @@ export default function Extensions() {
</p>
</GlassPanel>
</Link>
// <div className="my-2 px-2 relative" key={extension.name}>
// <div className="border rounded-sm p-4">
// <h3 className="m-0">
// <code className="text-sm">{extension.name}</code>
// </h3>
// <p className=" mt-4">
// {extension.comment.charAt(0).toUpperCase() + extension.comment.slice(1)}
// </p>
// {extension.link && (
// <Link href={extension.link}>
// <a
// target="_blank"
// className="text-xs no-underline absolute top-2 right-4 bg-slate-200 hover:bg-slate-400 transition-colors p-2 rounded-md"
// >
// <span>
// <IconLink size={14} className="" />
// </span>
// </a>
// </Link>
// )}
// </div>
// </div>
))}
</div>
</div>
+11
View File
@@ -0,0 +1,11 @@
import { lazy, Suspense } from 'react'
const ExtensionsInternal = lazy(() => import('./Extensions'))
export function Extensions() {
return (
<Suspense>
<ExtensionsInternal />
</Suspense>
)
}
+5 -4
View File
@@ -1,7 +1,8 @@
import Link from 'next/link'
import { IconBackground, IconPanel, IconPlay } from 'ui'
import { IconBackground, IconPlay } from 'ui'
import { useBreakpoint } from 'common'
import DocsCoverLogo from './DocsCoverLogo'
import { IconPanel } from 'ui-patterns/IconPanel'
const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
@@ -74,9 +75,9 @@ const HomePageCover = (props) => {
<div className="md:max-w-xs shrink w-fit xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
<IconPlay className="text-brand w-4" strokeWidth={2} />
<IconPlay aria-hidden="true" className="text-brand-600 w-4" strokeWidth={2} />
</IconBackground>
<h3 className="text-2xl m-0 text-foreground">Getting Started</h3>
<h2 className="text-2xl m-0 text-foreground">Getting Started</h2>
</div>
<p className="text-foreground-light text-sm">
Discover how to set up a database to an app making queries in just a few minutes.
@@ -102,7 +103,7 @@ const HomePageCover = (props) => {
<div className="w-full bg-alternative border-b max-w-none mb-16 md:mb-12 xl:mb-0">
<div className="max-w-7xl px-5 mx-auto py-8 sm:pb-16 sm:pt-12 xl:pt-16 flex flex-col xl:flex-row justify-between gap-12 xl:gap-12">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-start sm:items-center w-full max-w-xl xl:max-w-[33rem]">
<DocsCoverLogo />
<DocsCoverLogo aria-hidden="true" />
<div className="flex flex-col">
<h1 className="m-0 mb-3 text-2xl sm:text-3xl text-foreground">{props.meta?.title}</h1>
<p className="m-0 text-foreground-light">
@@ -0,0 +1,45 @@
import Link from 'next/link'
import { memo } from 'react'
import { Button, IconExternalLink, cn } from 'ui'
export const IPv4DeprecationBanner = memo(() => {
return (
<div
role="region"
aria-label="Deprecation notice"
className="flex items-center justify-center gap-x-4 bg-surface-100 p-2 transition text-foreground box-border border-b border-default"
>
<p className="text-sm">
Prepare for the PgBouncer and IPv4 deprecations on 26th January 2024{' '}
<Link
href="https://github.com/orgs/supabase/discussions/17817"
target="_blank"
rel="noreferrer"
className={cn(
'sm:hidden',
'inline-flex items-center justify-center px-2.5 py-1',
'border border-transparent rounded-md',
'text-xs text-brand-600 hover:bg-brand-400',
'ease-out duration-200 transition-all'
)}
aria-hidden="true"
>
Learn more
</Link>
</p>
<div className="max-sm:sr-only sm:flex items-center gap-x-1">
<Button asChild type="link" iconRight={<IconExternalLink />}>
<a
href="https://github.com/orgs/supabase/discussions/17817"
target="_blank"
rel="noreferrer"
>
Learn more
</a>
</Button>
</div>
</div>
)
})
IPv4DeprecationBanner.displayName = 'IPv4DeprecationBanner'
@@ -1,5 +1,5 @@
import KJUR from 'jsrsasign'
import { useState } from 'react'
import { KJUR } from 'jsrsasign'
import { ChangeEvent, useState } from 'react'
import { Button, CodeBlock, Input, Select } from 'ui'
const JWT_HEADER = { alg: 'HS256', typ: 'JWT' }
@@ -10,8 +10,8 @@ const anonToken = `
{
"role": "anon",
"iss": "supabase",
"iat": ${Math.floor(today / 1000)},
"exp": ${Math.floor(fiveYears / 1000)}
"iat": ${Math.floor(today.valueOf() / 1000)},
"exp": ${Math.floor(fiveYears.valueOf() / 1000)}
}
`.trim()
@@ -19,8 +19,8 @@ const serviceToken = `
{
"role": "service_role",
"iss": "supabase",
"iat": ${Math.floor(today / 1000)},
"exp": ${Math.floor(fiveYears / 1000)}
"iat": ${Math.floor(today.valueOf() / 1000)},
"exp": ${Math.floor(fiveYears.valueOf() / 1000)}
}
`.trim()
@@ -31,7 +31,7 @@ export default function JwtGenerator({}) {
const [token, setToken] = useState(anonToken)
const [signedToken, setSignedToken] = useState('')
const handleKeySelection = (e) => {
const handleKeySelection = (e: ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value
if (val == 'service') setToken(serviceToken)
else setToken(anonToken)
@@ -55,7 +55,7 @@ export default function JwtGenerator({}) {
/>
</div>
<div className="grid mb-8">
<label for="service">Preconfigured Payload:</label>
<label htmlFor="service">Preconfigured Payload:</label>
<Select id="service" style={{ fontFamily: 'monospace' }} onChange={handleKeySelection}>
<Select.Option value="anon">ANON_KEY</Select.Option>
<Select.Option value="service">SERVICE_KEY</Select.Option>
@@ -67,7 +67,7 @@ export default function JwtGenerator({}) {
<Input.TextArea
id="token"
type="text"
rows="6"
rows={6}
placeholder="A valid JWT Token"
value={token}
style={{ fontFamily: 'monospace' }}
@@ -82,7 +82,7 @@ export default function JwtGenerator({}) {
{signedToken && (
<div className="mt-8">
<h4>Generated Token:</h4>
<CodeBlock language="bash" className="relative" style={{ fontFamily: 'monospace' }}>
<CodeBlock language="bash" className="relative font-mono">
{signedToken}
</CodeBlock>
</div>
@@ -0,0 +1,11 @@
import { lazy, Suspense } from 'react'
const Generator = lazy(() => import('./JwtGenerator'))
export function JwtGenerator() {
return (
<Suspense>
<Generator />
</Suspense>
)
}
+2
View File
@@ -0,0 +1,2 @@
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
export default Mermaid
+11
View File
@@ -0,0 +1,11 @@
import { Suspense, lazy } from 'react'
const MermaidHeavy = lazy(() => import('./Mermaid'))
export function Mermaid(props) {
return (
<Suspense>
<MermaidHeavy {...props} />
</Suspense>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { ReactNode } from 'react'
import { navDataForMdx } from './Navigation/NavigationMenu/NavigationMenu.constants'
export function NavData({
data,
children,
}: {
data: keyof typeof navDataForMdx
children: (navItems: (typeof navDataForMdx)[keyof typeof navDataForMdx]) => ReactNode
}) {
const navItems = navDataForMdx[data]
return children(navItems)
}
+145 -134
View File
@@ -5,11 +5,98 @@ import { LayoutMainContent } from '~/layouts/DefaultLayout'
const Footer = () => (
<LayoutMainContent className="pt-0">
<div className="mt-16">
<ul className="flex flex-col gap-2">
{primaryLinks.map(({ url, featherIcon: Icon, icon, text, ctaLabel }) => (
<li key={url} className="flex items-center gap-1 text-xs text-foreground-lighter">
{icon && (
<footer role="contentinfo" aria-label="footer">
<div className="mt-16">
<ul className="flex flex-col gap-2">
{primaryLinks.map(({ url, featherIcon: Icon, icon, text, ctaLabel }) => (
<li key={url} className="flex items-center gap-1 text-xs text-foreground-lighter">
{icon && (
<svg
aria-hidden="true"
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d={icon}
stroke="hsl(var(--foreground-muted))"
strokeWidth={0.7}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{Icon && <Icon aria-hidden="true" width={16} height={16} />}
<p>{text}</p>
<Link
href={url}
className="text-brand-link hover:underline"
target="_blank"
rel="noreferrer noopener"
>
{ctaLabel}
</Link>
</li>
))}
</ul>
</div>
<hr className="border-default my-6"></hr>
<div className="flex gap-4 items-center justify-between">
<div className="flex flex-col lg:flex-row gap-3 ">
<Link href="https://supabase.com/" className="text-xs text-foreground-lighter">
&copy; Supabase Inc
</Link>
<span className="text-xs text-foreground-lighter">—</span>
{secondaryLinks.map(({ component: Component, ...item }) =>
item.url ? (
<Link
href={item.url}
key={item.url}
className="text-xs text-foreground-lighter hover:underline"
>
{item.title}
</Link>
) : (
Component && (
<Component
key={item.title}
className="text-xs text-foreground-lighter hover:underline"
>
{item.title}
</Component>
)
)
)}
</div>
<div className="flex items-center gap-2">
<Button type="text" asChild>
<a
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
>
<span className="sr-only">GitHub</span>
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.66832 1.55469C4.98649 1.55469 2.00195 4.54402 2.00195 8.23269C2.00195 11.1827 3.91187 13.686 6.56108 14.5687C6.8944 14.63 7.01573 14.424 7.01573 14.2467C7.01573 14.0887 7.0104 13.668 7.00706 13.1114C5.15248 13.5147 4.76116 12.216 4.76116 12.216C4.45851 11.444 4.0212 11.2387 4.0212 11.2387C3.41589 10.8254 4.06719 10.8334 4.06719 10.8334C4.73583 10.88 5.08782 11.5214 5.08782 11.5214C5.68246 12.5414 6.64841 12.2467 7.02773 12.076C7.08906 11.6447 7.26105 11.3507 7.45171 11.184C5.97178 11.0154 4.41518 10.442 4.41518 7.88335C4.41518 7.15469 4.67517 6.55802 5.10115 6.09135C5.03248 5.92269 4.80383 5.24335 5.16648 4.32469C5.16648 4.32469 5.72645 4.14469 6.99973 5.00869C7.54355 4.86036 8.10464 4.78482 8.66832 4.78402C9.23496 4.78669 9.80494 4.86069 10.3376 5.00869C11.6102 4.14469 12.1688 4.32402 12.1688 4.32402C12.5328 5.24335 12.3035 5.92269 12.2355 6.09135C12.6621 6.55802 12.9208 7.15469 12.9208 7.88335C12.9208 10.4487 11.3615 11.0134 9.87694 11.1787C10.1163 11.3847 10.3289 11.792 10.3289 12.4154C10.3289 13.3074 10.3209 14.028 10.3209 14.2467C10.3209 14.4254 10.4409 14.6334 10.7796 14.568C12.107 14.1228 13.261 13.2716 14.0784 12.1347C14.8958 10.9979 15.3353 9.6329 15.3347 8.23269C15.3347 4.54402 12.3495 1.55469 8.66832 1.55469Z"
fill="hsl(var(--foreground-muted))"
/>
</svg>
</a>
</Button>
<Button type="text" asChild>
<span className="sr-only">YouTube</span>
<a href="https://youtube.com/c/supabase" target="_blank" rel="noreferrer noopener">
<svg
width="16"
height="17"
@@ -18,139 +105,63 @@ const Footer = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
d={icon}
stroke="hsl(var(--foreground-muted))"
strokeWidth={0.7}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{Icon && <Icon width={16} height={16} />}
<p>{text}</p>
<Link
href={url}
className="text-brand-link hover:underline"
target="_blank"
rel="noreferrer noopener"
>
{ctaLabel}
</Link>
</li>
))}
</ul>
</div>
<hr className="border-default my-6"></hr>
<div className="flex gap-4 items-center justify-between">
<div className="flex flex-col lg:flex-row gap-3 ">
<Link href="https://supabase.com/" className="text-xs text-foreground-lighter">
&copy; Supabase Inc
</Link>
<span className="text-xs text-foreground-lighter">—</span>
{secondaryLinks.map(({ component: Component, ...item }) =>
item.url ? (
<Link
href={item.url}
key={item.url}
className="text-xs text-foreground-lighter hover:underline"
>
{item.title}
</Link>
) : (
Component && (
<Component
key={item.title}
className="text-xs text-foreground-lighter hover:underline"
>
{item.title}
</Component>
)
)
)}
</div>
<div className="flex items-center gap-2">
<Button type="text" asChild>
<a href="https://github.com/supabase/supabase" target="_blank" rel="noreferrer noopener">
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.66832 1.55469C4.98649 1.55469 2.00195 4.54402 2.00195 8.23269C2.00195 11.1827 3.91187 13.686 6.56108 14.5687C6.8944 14.63 7.01573 14.424 7.01573 14.2467C7.01573 14.0887 7.0104 13.668 7.00706 13.1114C5.15248 13.5147 4.76116 12.216 4.76116 12.216C4.45851 11.444 4.0212 11.2387 4.0212 11.2387C3.41589 10.8254 4.06719 10.8334 4.06719 10.8334C4.73583 10.88 5.08782 11.5214 5.08782 11.5214C5.68246 12.5414 6.64841 12.2467 7.02773 12.076C7.08906 11.6447 7.26105 11.3507 7.45171 11.184C5.97178 11.0154 4.41518 10.442 4.41518 7.88335C4.41518 7.15469 4.67517 6.55802 5.10115 6.09135C5.03248 5.92269 4.80383 5.24335 5.16648 4.32469C5.16648 4.32469 5.72645 4.14469 6.99973 5.00869C7.54355 4.86036 8.10464 4.78482 8.66832 4.78402C9.23496 4.78669 9.80494 4.86069 10.3376 5.00869C11.6102 4.14469 12.1688 4.32402 12.1688 4.32402C12.5328 5.24335 12.3035 5.92269 12.2355 6.09135C12.6621 6.55802 12.9208 7.15469 12.9208 7.88335C12.9208 10.4487 11.3615 11.0134 9.87694 11.1787C10.1163 11.3847 10.3289 11.792 10.3289 12.4154C10.3289 13.3074 10.3209 14.028 10.3209 14.2467C10.3209 14.4254 10.4409 14.6334 10.7796 14.568C12.107 14.1228 13.261 13.2716 14.0784 12.1347C14.8958 10.9979 15.3353 9.6329 15.3347 8.23269C15.3347 4.54402 12.3495 1.55469 8.66832 1.55469Z"
fill="hsl(var(--foreground-muted))"
/>
</svg>
</a>
</Button>
<Button type="text" asChild>
<a href="https://youtube.com/c/supabase" target="_blank" rel="noreferrer noopener">
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.665 4.35595C15.481 3.66308 14.9388 3.11739 14.2505 2.93221C13.0028 2.5957 7.99964 2.5957 7.99964 2.5957C7.99964 2.5957 2.99655 2.5957 1.74883 2.93221C1.06047 3.11742 0.518326 3.66308 0.334321 4.35595C0 5.61181 0 8.23207 0 8.23207C0 8.23207 0 10.8523 0.334321 12.1082C0.518326 12.8011 1.06047 13.324 1.74883 13.5092C2.99655 13.8457 7.99964 13.8457 7.99964 13.8457C7.99964 13.8457 13.0027 13.8457 14.2505 13.5092C14.9388 13.324 15.481 12.8011 15.665 12.1082C15.9993 10.8523 15.9993 8.23207 15.9993 8.23207C15.9993 8.23207 15.9993 5.61181 15.665 4.35595ZM6.36334 10.6111V5.85307L10.545 8.23212L6.36334 10.6111Z"
fill="hsl(var(--foreground-muted))"
/>
</svg>
</a>
</Button>
<Button type="text" asChild>
<a href="https://twitter.com/supabase" target="_blank" rel="noreferrer noopener">
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
fill="hsl(var(--foreground-muted))"
/>
</svg>
</a>
</Button>
<Button type="text" asChild>
<a href="https://discord.supabase.com/" target="_blank" rel="noreferrer noopener">
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1182_99731)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.8781 3.13594C12.8583 2.66802 11.7648 2.32328 10.6215 2.12583C10.6006 2.12202 10.5798 2.13154 10.5691 2.15059C10.4285 2.40073 10.2727 2.72706 10.1636 2.98356C8.93387 2.79945 7.71044 2.79945 6.50592 2.98356C6.39681 2.72136 6.23538 2.40073 6.09411 2.15059C6.08339 2.13218 6.06259 2.12265 6.04176 2.12583C4.89905 2.32265 3.80554 2.66739 2.78517 3.13594C2.77633 3.13975 2.76876 3.14611 2.76374 3.15435C0.689563 6.25326 0.12136 9.276 0.400102 12.2613C0.401363 12.2759 0.409561 12.2898 0.420913 12.2987C1.78939 13.3037 3.115 13.9139 4.41599 14.3183C4.43681 14.3247 4.45887 14.317 4.47212 14.2999C4.77987 13.8796 5.0542 13.4364 5.28941 12.9704C5.3033 12.9431 5.29005 12.9107 5.26167 12.9C4.82654 12.7349 4.4122 12.5336 4.01364 12.3051C3.98212 12.2867 3.97959 12.2416 4.00859 12.22C4.09247 12.1571 4.17636 12.0917 4.25645 12.0257C4.27094 12.0136 4.29113 12.0111 4.30816 12.0187C6.92655 13.2142 9.76126 13.2142 12.3488 12.0187C12.3658 12.0105 12.386 12.013 12.4011 12.0251C12.4812 12.0911 12.5651 12.1571 12.6496 12.22C12.6786 12.2416 12.6767 12.2867 12.6452 12.3051C12.2466 12.5381 11.8323 12.7349 11.3965 12.8993C11.3681 12.9101 11.3555 12.9431 11.3694 12.9704C11.6097 13.4358 11.884 13.879 12.1861 14.2993C12.1987 14.317 12.2214 14.3247 12.2422 14.3183C13.5495 13.9139 14.8751 13.3037 16.2436 12.2987C16.2556 12.2898 16.2631 12.2765 16.2644 12.2619C16.598 8.8106 15.7056 5.81265 13.8989 3.15498C13.8944 3.14611 13.8869 3.13975 13.8781 3.13594ZM5.68043 10.4435C4.89211 10.4435 4.24257 9.71978 4.24257 8.83093C4.24257 7.94207 4.87952 7.21831 5.68043 7.21831C6.48763 7.21831 7.13089 7.94843 7.11827 8.83093C7.11827 9.71978 6.48132 10.4435 5.68043 10.4435ZM10.9967 10.4435C10.2084 10.4435 9.55884 9.71978 9.55884 8.83093C9.55884 7.94207 10.1958 7.21831 10.9967 7.21831C11.8039 7.21831 12.4471 7.94843 12.4345 8.83093C12.4345 9.71978 11.8039 10.4435 10.9967 10.4435Z"
d="M15.665 4.35595C15.481 3.66308 14.9388 3.11739 14.2505 2.93221C13.0028 2.5957 7.99964 2.5957 7.99964 2.5957C7.99964 2.5957 2.99655 2.5957 1.74883 2.93221C1.06047 3.11742 0.518326 3.66308 0.334321 4.35595C0 5.61181 0 8.23207 0 8.23207C0 8.23207 0 10.8523 0.334321 12.1082C0.518326 12.8011 1.06047 13.324 1.74883 13.5092C2.99655 13.8457 7.99964 13.8457 7.99964 13.8457C7.99964 13.8457 13.0027 13.8457 14.2505 13.5092C14.9388 13.324 15.481 12.8011 15.665 12.1082C15.9993 10.8523 15.9993 8.23207 15.9993 8.23207C15.9993 8.23207 15.9993 5.61181 15.665 4.35595ZM6.36334 10.6111V5.85307L10.545 8.23212L6.36334 10.6111Z"
fill="hsl(var(--foreground-muted))"
/>
</g>
<defs>
<clipPath id="clip0_1182_99731">
<rect
width="15.9993"
height="16"
fill="white"
transform="translate(0.333984 0.222656)"
</svg>
</a>
</Button>
<Button type="text" asChild>
<a href="https://twitter.com/supabase" target="_blank" rel="noreferrer noopener">
<span className="sr-only">Twitter</span>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
fill="hsl(var(--foreground-muted))"
/>
</svg>
</a>
</Button>
<Button type="text" asChild>
<a href="https://discord.supabase.com/" target="_blank" rel="noreferrer noopener">
<span className="sr-only">Discord</span>
<svg
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_1182_99731)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.8781 3.13594C12.8583 2.66802 11.7648 2.32328 10.6215 2.12583C10.6006 2.12202 10.5798 2.13154 10.5691 2.15059C10.4285 2.40073 10.2727 2.72706 10.1636 2.98356C8.93387 2.79945 7.71044 2.79945 6.50592 2.98356C6.39681 2.72136 6.23538 2.40073 6.09411 2.15059C6.08339 2.13218 6.06259 2.12265 6.04176 2.12583C4.89905 2.32265 3.80554 2.66739 2.78517 3.13594C2.77633 3.13975 2.76876 3.14611 2.76374 3.15435C0.689563 6.25326 0.12136 9.276 0.400102 12.2613C0.401363 12.2759 0.409561 12.2898 0.420913 12.2987C1.78939 13.3037 3.115 13.9139 4.41599 14.3183C4.43681 14.3247 4.45887 14.317 4.47212 14.2999C4.77987 13.8796 5.0542 13.4364 5.28941 12.9704C5.3033 12.9431 5.29005 12.9107 5.26167 12.9C4.82654 12.7349 4.4122 12.5336 4.01364 12.3051C3.98212 12.2867 3.97959 12.2416 4.00859 12.22C4.09247 12.1571 4.17636 12.0917 4.25645 12.0257C4.27094 12.0136 4.29113 12.0111 4.30816 12.0187C6.92655 13.2142 9.76126 13.2142 12.3488 12.0187C12.3658 12.0105 12.386 12.013 12.4011 12.0251C12.4812 12.0911 12.5651 12.1571 12.6496 12.22C12.6786 12.2416 12.6767 12.2867 12.6452 12.3051C12.2466 12.5381 11.8323 12.7349 11.3965 12.8993C11.3681 12.9101 11.3555 12.9431 11.3694 12.9704C11.6097 13.4358 11.884 13.879 12.1861 14.2993C12.1987 14.317 12.2214 14.3247 12.2422 14.3183C13.5495 13.9139 14.8751 13.3037 16.2436 12.2987C16.2556 12.2898 16.2631 12.2765 16.2644 12.2619C16.598 8.8106 15.7056 5.81265 13.8989 3.15498C13.8944 3.14611 13.8869 3.13975 13.8781 3.13594ZM5.68043 10.4435C4.89211 10.4435 4.24257 9.71978 4.24257 8.83093C4.24257 7.94207 4.87952 7.21831 5.68043 7.21831C6.48763 7.21831 7.13089 7.94843 7.11827 8.83093C7.11827 9.71978 6.48132 10.4435 5.68043 10.4435ZM10.9967 10.4435C10.2084 10.4435 9.55884 9.71978 9.55884 8.83093C9.55884 7.94207 10.1958 7.21831 10.9967 7.21831C11.8039 7.21831 12.4471 7.94843 12.4345 8.83093C12.4345 9.71978 11.8039 10.4435 10.9967 10.4435Z"
fill="hsl(var(--foreground-muted))"
/>
</clipPath>
</defs>
</svg>
</a>
</Button>
</g>
<defs>
<clipPath id="clip0_1182_99731">
<rect
width="15.9993"
height="16"
fill="white"
transform="translate(0.333984 0.222656)"
/>
</clipPath>
</defs>
</svg>
</a>
</Button>
</div>
</div>
</div>
</footer>
</LayoutMainContent>
)
@@ -19,7 +19,7 @@ const NavigationMenuHome = () => {
{section.map((link, i) => {
if (!link.href) {
return (
<div
<h2
key={link.label}
className={cn(
'font-mono uppercase text-xs text-foreground-lighter',
@@ -27,7 +27,7 @@ const NavigationMenuHome = () => {
)}
>
{link.label}
</div>
</h2>
)
} else {
return (
@@ -22,7 +22,7 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
{
label: 'Database',
icon: 'database',
href: '/guides/database',
href: '/guides/database/overview',
level: 'database',
},
{
@@ -239,12 +239,27 @@ export const gettingstarted: NavMenuConstant = {
{ name: 'NuxtJS', url: '/guides/getting-started/quickstarts/nuxtjs' },
{ name: 'Vue', url: '/guides/getting-started/quickstarts/vue' },
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' },
{ name: 'Android Kotlin', url: '/guides/getting-started/quickstarts/kotlin' },
{ name: 'SvelteKit', url: '/guides/getting-started/quickstarts/sveltekit' },
{ name: 'Laravel PHP', url: '/guides/getting-started/quickstarts/laravel' },
{ name: 'Ruby on Rails', url: '/guides/getting-started/quickstarts/ruby-on-rails' },
{
name: 'Android Kotlin',
url: '/guides/getting-started/quickstarts/kotlin',
},
{
name: 'SvelteKit',
url: '/guides/getting-started/quickstarts/sveltekit',
},
{
name: 'Laravel PHP',
url: '/guides/getting-started/quickstarts/laravel',
},
{
name: 'Ruby on Rails',
url: '/guides/getting-started/quickstarts/ruby-on-rails',
},
{ name: 'SolidJS', url: '/guides/getting-started/quickstarts/solidjs' },
{ name: 'RedwoodJS', url: '/guides/getting-started/quickstarts/redwoodjs' },
{
name: 'RedwoodJS',
url: '/guides/getting-started/quickstarts/redwoodjs',
},
{ name: 'refine', url: '/guides/getting-started/quickstarts/refine' },
],
},
@@ -496,7 +511,11 @@ export const auth = {
items: [
{ name: 'Next.js', url: '/guides/auth/quickstarts/nextjs', items: [] },
{ name: 'React', url: '/guides/auth/quickstarts/react', items: [] },
{ name: 'React Native', url: '/guides/auth/quickstarts/react-native', items: [] },
{
name: 'React Native',
url: '/guides/auth/quickstarts/react-native',
items: [],
},
],
},
{
@@ -508,8 +527,14 @@ export const auth = {
name: 'Passwordless Login',
url: '/guides/auth/passwordless-login',
items: [
{ name: 'Email Magic Link', url: '/guides/auth/passwordless-login/auth-magic-link' },
{ name: 'Email OTP', url: '/guides/auth/passwordless-login/auth-email-otp' },
{
name: 'Email Magic Link',
url: '/guides/auth/passwordless-login/auth-magic-link',
},
{
name: 'Email OTP',
url: '/guides/auth/passwordless-login/auth-email-otp',
},
{ name: 'Phone OTP', url: '/guides/auth/phone-login' },
],
},
@@ -578,7 +603,10 @@ export const auth = {
{ name: 'Managing User Data', url: '/guides/auth/managing-user-data' },
{ name: 'Multi-Factor Authentication', url: '/guides/auth/auth-mfa' },
{ name: 'Row Level Security', url: '/guides/auth/row-level-security' },
{ name: 'Column Level Security', url: '/guides/auth/column-level-security' },
{
name: 'Column Level Security',
url: '/guides/auth/column-level-security',
},
],
},
{
@@ -586,7 +614,14 @@ export const auth = {
url: undefined,
items: [
{ name: 'Overview', url: '/guides/auth/server-side/overview' },
{ name: 'Creating a client', url: '/guides/auth/server-side/creating-a-client' },
{
name: 'Next.js guide',
url: '/guides/auth/server-side/nextjs',
},
{
name: 'Creating a client',
url: '/guides/auth/server-side/creating-a-client',
},
{
name: 'Email Auth with PKCE flow',
url: '/guides/auth/server-side/email-based-auth-with-pkce-flow-for-ssr',
@@ -595,7 +630,10 @@ export const auth = {
name: 'OAuth with PKCE flow',
url: '/guides/auth/server-side/oauth-with-pkce-flow-for-ssr',
},
{ name: 'Server-side Rendering', url: '/guides/auth/server-side-rendering' },
{
name: 'Server-side Rendering',
url: '/guides/auth/server-side-rendering',
},
{
name: 'Migrating from Auth Helpers',
url: '/guides/auth/server-side/migrating-to-ssr-from-auth-helpers',
@@ -607,7 +645,10 @@ export const auth = {
url: undefined,
items: [
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui' },
{ name: 'Flutter Auth UI', url: '/guides/auth/auth-helpers/flutter-auth-ui' },
{
name: 'Flutter Auth UI',
url: '/guides/auth/auth-helpers/flutter-auth-ui',
},
],
},
{
@@ -616,17 +657,20 @@ export const auth = {
items: [
{
name: 'Part One: JWTs',
url: '/learn/auth-deep-dive/auth-deep-dive-jwts',
url: '/guides/auth/auth-deep-dive/auth-deep-dive-jwts',
},
{
name: 'Part Two: Row Level Security',
url: '/learn/auth-deep-dive/auth-row-level-security',
url: '/guides/auth/auth-deep-dive/auth-row-level-security',
},
{ name: 'Part Three: Policies', url: '/learn/auth-deep-dive/auth-policies' },
{ name: 'Part Four: GoTrue', url: '/learn/auth-deep-dive/auth-gotrue' },
{
name: 'Part Three: Policies',
url: '/guides/auth/auth-deep-dive/auth-policies',
},
{ name: 'Part Four: GoTrue', url: '/guides/auth/auth-deep-dive/auth-gotrue' },
{
name: 'Part Five: Google OAuth',
url: '/learn/auth-deep-dive/auth-google-oauth',
url: '/guides/auth/auth-deep-dive/auth-google-oauth',
},
],
},
@@ -636,14 +680,17 @@ export const auth = {
export const database: NavMenuConstant = {
icon: 'database',
title: 'Database',
url: '/guides/database',
url: '/guides/database/overview',
items: [
{ name: 'Overview', url: '/guides/database' },
{ name: 'Overview', url: '/guides/database/overview' },
{
name: 'Fundamentals',
url: undefined,
items: [
{ name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres' },
{
name: 'Connecting to your database',
url: '/guides/database/connecting-to-postgres',
},
{ name: 'Importing data', url: '/guides/database/import-data' },
{ name: 'Securing your data', url: '/guides/database/secure-data' },
],
@@ -652,7 +699,10 @@ export const database: NavMenuConstant = {
name: 'Working with your database (basics)',
url: undefined,
items: [
{ name: 'Managing tables, views, and data', url: '/guides/database/tables' },
{
name: 'Managing tables, views, and data',
url: '/guides/database/tables',
},
{ name: 'Managing indexes', url: '/guides/database/postgres/indexes' },
{
name: 'Querying joins and nested tables',
@@ -665,33 +715,73 @@ export const database: NavMenuConstant = {
name: 'Working with your database (intermediate)',
url: undefined,
items: [
{ name: 'Implementing cascade deletes', url: '/guides/database/postgres/cascade-deletes' },
{
name: 'Implementing cascade deletes',
url: '/guides/database/postgres/cascade-deletes',
},
{ name: 'Managing enums', url: '/guides/database/postgres/enums' },
{ name: 'Managing database functions', url: '/guides/database/functions' },
{ name: 'Managing database triggers', url: '/guides/database/postgres/triggers' },
{ name: 'Managing database webhooks', url: '/guides/database/webhooks' },
{ name: 'Using Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
{
name: 'Managing database functions',
url: '/guides/database/functions',
},
{
name: 'Managing database triggers',
url: '/guides/database/postgres/triggers',
},
{
name: 'Managing database webhooks',
url: '/guides/database/webhooks',
},
{
name: 'Using Full Text Search',
url: '/guides/database/full-text-search',
},
{
name: 'Partitioning your tables',
url: '/guides/database/partitions',
},
],
},
{
name: 'Access and security',
url: undefined,
items: [
{ name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
{ name: 'Managing Postgres Roles', url: '/guides/database/postgres/roles' },
{
name: 'Row Level Security',
url: '/guides/database/postgres/row-level-security',
},
{
name: 'Managing Postgres Roles',
url: '/guides/database/postgres/roles',
},
{ name: 'Managing secrets with Vault', url: '/guides/database/vault' },
{
name: 'Superuser Access and Unsupported Operations',
url: '/guides/database/postgres/roles-superuser',
},
],
},
{
name: 'Configuration, optimization, and testing',
url: undefined,
items: [
{ name: 'Database configuration', url: '/guides/database/postgres/configuration' },
{ name: 'Managing database replication', url: '/guides/database/replication' },
{ name: 'Query optimization', url: '/guides/database/query-optimization' },
{
name: 'Database configuration',
url: '/guides/database/postgres/configuration',
},
{
name: 'Managing database replication',
url: '/guides/database/replication',
},
{
name: 'Query optimization',
url: '/guides/database/query-optimization',
},
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
{ name: 'Debugging performance issues', url: '/guides/database/debugging-performance' },
{
name: 'Debugging performance issues',
url: '/guides/database/debugging-performance',
},
{ name: 'Testing your database', url: '/guides/database/testing' },
],
},
@@ -708,7 +798,10 @@ export const database: NavMenuConstant = {
name: 'plv8: Javascript Language',
url: '/guides/database/extensions/plv8',
},
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http' },
{
name: 'http: RESTful Client',
url: '/guides/database/extensions/http',
},
{
name: 'index_advisor: Query optimization',
url: '/guides/database/extensions/index_advisor',
@@ -747,7 +840,7 @@ export const database: NavMenuConstant = {
},
{
name: 'pg_net: Async Networking',
url: '/guides/database/extensions/pgnet',
url: '/guides/database/extensions/pg_net',
},
{
name: 'pg_plan_filter: Restrict Total Cost',
@@ -777,7 +870,10 @@ export const database: NavMenuConstant = {
name: 'pgsodium: Encryption Features',
url: '/guides/database/extensions/pgsodium',
},
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap' },
{
name: 'pgTAP: Unit Testing',
url: '/guides/database/extensions/pgtap',
},
{
name: 'plpgsql_check: PL/pgSQL Linter',
url: '/guides/database/extensions/plpgsql_check',
@@ -800,17 +896,38 @@ export const database: NavMenuConstant = {
name: 'Foreign Data Wrappers',
url: undefined,
items: [
{ name: 'Overview', url: '/guides/database/extensions/wrappers/overview' },
{ name: 'Connecting to Airtable', url: '/guides/database/extensions/wrappers/airtable' },
{ name: 'Connecting to AWS S3', url: '/guides/database/extensions/wrappers/s3' },
{ name: 'Connecting to BigQuery', url: '/guides/database/extensions/wrappers/bigquery' },
{
name: 'Overview',
url: '/guides/database/extensions/wrappers/overview',
},
{
name: 'Connecting to Airtable',
url: '/guides/database/extensions/wrappers/airtable',
},
{
name: 'Connecting to AWS S3',
url: '/guides/database/extensions/wrappers/s3',
},
{
name: 'Connecting to BigQuery',
url: '/guides/database/extensions/wrappers/bigquery',
},
{
name: 'Connecting to ClickHouse',
url: '/guides/database/extensions/wrappers/clickhouse',
},
{ name: 'Connecting to Firebase', url: '/guides/database/extensions/wrappers/firebase' },
{ name: 'Connecting to Logflare', url: '/guides/database/extensions/wrappers/logflare' },
{ name: 'Connecting to Stripe', url: '/guides/database/extensions/wrappers/stripe' },
{
name: 'Connecting to Firebase',
url: '/guides/database/extensions/wrappers/firebase',
},
{
name: 'Connecting to Logflare',
url: '/guides/database/extensions/wrappers/logflare',
},
{
name: 'Connecting to Stripe',
url: '/guides/database/extensions/wrappers/stripe',
},
],
},
{
@@ -829,6 +946,10 @@ export const database: NavMenuConstant = {
name: 'Print PostgreSQL Version',
url: '/guides/database/postgres/which-version-of-postgres',
},
{
name: 'Replicating from Supabase to External Postgres',
url: '/guides/database/postgres/setup-replication-external',
},
],
},
],
@@ -841,9 +962,21 @@ export const api: NavMenuConstant = {
items: [
{ name: 'Overview', url: '/guides/api', items: [] },
{ name: 'Quickstart', url: '/guides/api/quickstart', items: [] },
{ name: 'Client Libraries', url: '/guides/api/rest/client-libs', items: [] },
{ name: 'Auto-generated Docs', url: '/guides/api/rest/auto-generated-docs', items: [] },
{ name: 'Generating TypeScript Types', url: '/guides/api/rest/generating-types', items: [] },
{
name: 'Client Libraries',
url: '/guides/api/rest/client-libs',
items: [],
},
{
name: 'Auto-generated Docs',
url: '/guides/api/rest/auto-generated-docs',
items: [],
},
{
name: 'Generating TypeScript Types',
url: '/guides/api/rest/generating-types',
items: [],
},
{
name: 'Guides',
url: '/guides/api',
@@ -881,7 +1014,10 @@ export const api: NavMenuConstant = {
name: 'Debugging performance issues',
url: '/guides/api/data-apis#debugging-performance-issues',
},
{ name: 'Using custom schemas', url: '/guides/api/using-custom-schemas' },
{
name: 'Using custom schemas',
url: '/guides/api/using-custom-schemas',
},
],
},
],
@@ -896,7 +1032,11 @@ export const graphql: NavMenuConstant = {
{ name: 'API', url: '/guides/graphql/api', items: [] },
{ name: 'Views', url: '/guides/graphql/views', items: [] },
{ name: 'Functions', url: '/guides/graphql/functions', items: [] },
{ name: 'Configuration & Customization', url: '/guides/graphql/configuration', items: [] },
{
name: 'Configuration & Customization',
url: '/guides/graphql/configuration',
items: [],
},
{ name: 'Security', url: '/guides/graphql/security', items: [] },
{
name: 'Integrations',
@@ -929,7 +1069,10 @@ export const functions: NavMenuConstant = {
name: 'Deploy to Production',
url: '/guides/functions/deploy',
},
{ name: 'Setting up your editor', url: '/guides/functions/local-development' },
{
name: 'Setting up your editor',
url: '/guides/functions/local-development',
},
],
},
{
@@ -937,8 +1080,14 @@ export const functions: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Managing dependencies', url: '/guides/functions/import-maps' },
{ name: 'Managing environment variables', url: '/guides/functions/secrets' },
{ name: 'Integrating with Supabase Auth', url: '/guides/functions/auth' },
{
name: 'Managing environment variables',
url: '/guides/functions/secrets',
},
{
name: 'Integrating with Supabase Auth',
url: '/guides/functions/auth',
},
{
name: 'Integrating with Postgres',
url: '/guides/functions/connect-to-postgres',
@@ -947,37 +1096,74 @@ export const functions: NavMenuConstant = {
name: 'Integrating with Supabase Storage',
url: '/guides/functions/storage-caching',
},
{ name: 'Regional invocations', url: '/guides/functions/regional-invocation' },
{ name: 'Deploying with GitHub Actions', url: '/guides/functions/cicd-workflow' },
{
name: 'Handling Routing in Functions',
url: '/guides/functions/routing',
},
{
name: 'Regional invocations',
url: '/guides/functions/regional-invocation',
},
{
name: 'Deploying with CI / CD pipelines',
url: '/guides/functions/cicd-workflow',
},
],
},
{
name: 'Debugging',
url: undefined,
items: [
{ name: 'Debugging Edge Functions', url: '/guides/functions/debugging' },
{ name: 'Testing your Edge Functions', url: '/guides/functions/unit-test' },
{
name: 'Debugging Edge Functions',
url: '/guides/functions/debugging',
},
{
name: 'Testing your Edge Functions',
url: '/guides/functions/unit-test',
},
],
},
{
name: 'Examples',
url: '/guides/functions/examples',
items: [
{ name: 'CORS support for invoking from the browser', url: '/guides/functions/cors' },
{ name: 'Scheduling Functions', url: '/guides/functions/schedule-functions' },
{
name: 'CORS support for invoking from the browser',
url: '/guides/functions/cors',
},
{
name: 'Scheduling Functions',
url: '/guides/functions/schedule-functions',
},
{
name: 'Sending Push Notifications',
url: '/guides/functions/examples/push-notifications',
},
{ name: 'Generating OG images ', url: '/guides/functions/examples/og-image' },
{
name: 'Generating OG images ',
url: '/guides/functions/examples/og-image',
},
{
name: 'CAPTCHA support with Cloudflare Turnstile',
url: '/guides/functions/examples/cloudflare-turnstile',
},
{ name: 'Building a Discord Bot', url: '/guides/functions/examples/discord-bot' },
{ name: 'Building a Telegram Bot', url: '/guides/functions/examples/telegram-bot' },
{ name: 'Handling Stripe Webhooks ', url: '/guides/functions/examples/stripe-webhooks' },
{ name: 'Rate-limiting with Redis', url: '/guides/functions/examples/rate-limiting' },
{
name: 'Building a Discord Bot',
url: '/guides/functions/examples/discord-bot',
},
{
name: 'Building a Telegram Bot',
url: '/guides/functions/examples/telegram-bot',
},
{
name: 'Handling Stripe Webhooks ',
url: '/guides/functions/examples/stripe-webhooks',
},
{
name: 'Rate-limiting with Redis',
url: '/guides/functions/examples/rate-limiting',
},
{
name: 'Taking Screenshots with Puppeteer',
url: '/guides/functions/examples/screenshots',
@@ -993,12 +1179,27 @@ export const functions: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Dart Edge on Supabase', url: '/guides/functions/dart-edge' },
{ name: 'Browserless.io', url: '/guides/functions/examples/screenshots' },
{ name: 'Hugging Face', url: '/guides/ai/examples/huggingface-image-captioning' },
{
name: 'Browserless.io',
url: '/guides/functions/examples/screenshots',
},
{
name: 'Hugging Face',
url: '/guides/ai/examples/huggingface-image-captioning',
},
{ name: 'OpenAI API', url: '/guides/ai/examples/openai' },
{ name: 'Sending Emails with Resend', url: '/guides/functions/examples/send-emails' },
{ name: 'Upstash Redis', url: '/guides/functions/examples/upstash-redis' },
{ name: 'Type-Safe SQL with Kysely', url: '/guides/functions/kysely-postgres' },
{
name: 'Sending Emails with Resend',
url: '/guides/functions/examples/send-emails',
},
{
name: 'Upstash Redis',
url: '/guides/functions/examples/upstash-redis',
},
{
name: 'Type-Safe SQL with Kysely',
url: '/guides/functions/kysely-postgres',
},
],
},
],
@@ -1076,20 +1277,34 @@ export const storage: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Fundamentals', url: '/guides/storage/buckets/fundamentals' },
{ name: 'Creating Buckets', url: '/guides/storage/buckets/creating-buckets' },
{
name: 'Creating Buckets',
url: '/guides/storage/buckets/creating-buckets',
},
],
},
{
name: 'Security',
url: undefined,
items: [{ name: 'Access Control', url: '/guides/storage/security/access-control' }],
items: [
{
name: 'Access Control',
url: '/guides/storage/security/access-control',
},
],
},
{
name: 'Uploads',
url: undefined,
items: [
{ name: 'Standard Uploads', url: '/guides/storage/uploads/standard-uploads' },
{ name: 'Resumable Uploads', url: '/guides/storage/uploads/resumable-uploads' },
{
name: 'Standard Uploads',
url: '/guides/storage/uploads/standard-uploads',
},
{
name: 'Resumable Uploads',
url: '/guides/storage/uploads/resumable-uploads',
},
{ name: 'Limits', url: '/guides/storage/uploads/file-limits' },
],
},
@@ -1098,7 +1313,10 @@ export const storage: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Serving assets', url: '/guides/storage/serving/downloads' },
{ name: 'Image Transformations', url: '/guides/storage/serving/image-transformations' },
{
name: 'Image Transformations',
url: '/guides/storage/serving/image-transformations',
},
],
},
{
@@ -1123,7 +1341,10 @@ export const storage: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Database Design', url: '/guides/storage/schema/design' },
{ name: 'Helper Functions', url: '/guides/storage/schema/helper-functions' },
{
name: 'Helper Functions',
url: '/guides/storage/schema/helper-functions',
},
],
},
{
@@ -1161,11 +1382,24 @@ export const ai = {
url: undefined,
items: [
{ name: 'Vector columns', url: '/guides/ai/vector-columns' },
{ name: 'Vector indexes', url: '/guides/ai/vector-indexes', items: vectorIndexItems },
{ name: 'Engineering for scale', url: '/guides/ai/engineering-for-scale' },
{ name: 'Choosing Compute Add-on', url: '/guides/ai/choosing-compute-addon' },
{
name: 'Vector indexes',
url: '/guides/ai/vector-indexes',
items: vectorIndexItems,
},
{
name: 'Engineering for scale',
url: '/guides/ai/engineering-for-scale',
},
{
name: 'Choosing Compute Add-on',
url: '/guides/ai/choosing-compute-addon',
},
{ name: 'Going to Production', url: '/guides/ai/going-to-prod' },
{ name: 'RAG with Permissions', url: '/guides/ai/rag-with-permissions' },
{
name: 'RAG with Permissions',
url: '/guides/ai/rag-with-permissions',
},
],
},
{
@@ -1211,11 +1445,23 @@ export const ai = {
name: 'Python Examples',
url: undefined,
items: [
{ name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' },
{ name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' },
{
name: 'Developing locally with Vecs',
url: '/guides/ai/vecs-python-client',
},
{
name: 'Creating and managing collections',
url: '/guides/ai/quickstarts/hello-world',
},
{ name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
{
name: 'Text Deduplication',
url: '/guides/ai/quickstarts/text-deduplication',
},
{
name: 'Face similarity search',
url: '/guides/ai/quickstarts/face-similarity',
},
{
name: 'Image search with OpenAI CLIP',
url: '/guides/ai/examples/image-search-openai-clip',
@@ -1274,7 +1520,10 @@ export const supabase_cli: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Local Development', url: '/guides/cli/local-development' },
{ name: 'Managing environments', url: '/guides/cli/managing-environments' },
{
name: 'Managing environments',
url: '/guides/cli/managing-environments',
},
{
name: 'Managing config and secrets',
url: '/guides/cli/managing-config',
@@ -1326,6 +1575,7 @@ export const platform: NavMenuConstant = {
{ name: 'Compute Add-ons', url: '/guides/platform/compute-add-ons' },
{ name: 'Custom Domains', url: '/guides/platform/custom-domains' },
{ name: 'Database Backups', url: '/guides/platform/backups' },
{ name: 'IPv4 Address', url: '/guides/platform/ipv4-address' },
{ name: 'Read Replicas', url: '/guides/platform/read-replicas' },
],
},
@@ -1334,10 +1584,16 @@ export const platform: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Access Control', url: '/guides/platform/access-control' },
{ name: 'Custom Postgres Config', url: '/guides/platform/custom-postgres-config' },
{
name: 'Custom Postgres Config',
url: '/guides/platform/custom-postgres-config',
},
{ name: 'Database Size', url: '/guides/platform/database-size' },
{ name: 'Fly Postgres', url: '/guides/platform/fly-postgres' },
{ name: 'HTTP Status Codes', url: '/guides/platform/http-status-codes' },
{
name: 'HTTP Status Codes',
url: '/guides/platform/http-status-codes',
},
{ name: 'Logging', url: '/guides/platform/logs' },
{ name: 'Metrics', url: '/guides/platform/metrics' },
{
@@ -1352,7 +1608,10 @@ export const platform: NavMenuConstant = {
name: 'Transfer Project',
url: '/guides/platform/project-transfer',
},
{ name: 'Network Restrictions', url: '/guides/platform/network-restrictions' },
{
name: 'Network Restrictions',
url: '/guides/platform/network-restrictions',
},
{ name: 'Performance Tuning', url: '/guides/platform/performance' },
{ name: 'Permissions', url: '/guides/platform/permissions' },
{ name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' },
@@ -1363,9 +1622,22 @@ export const platform: NavMenuConstant = {
name: 'Billing',
url: undefined,
items: [
{ name: 'How billing works', url: '/guides/platform/org-based-billing' },
{ name: 'Spend caps and cost-controls', url: '/guides/platform/spend-cap' },
{ name: 'Enterprise Billing', url: '/guides/platform/enterprise-billing' },
{
name: 'How billing works',
url: '/guides/platform/org-based-billing',
},
{
name: 'Spend caps and cost-controls',
url: '/guides/platform/spend-cap',
},
{
name: 'Enterprise Billing',
url: '/guides/platform/enterprise-billing',
},
{
name: 'Billing FAQ',
url: '/guides/platform/billing-faq',
},
],
},
{
@@ -1396,7 +1668,10 @@ export const platform: NavMenuConstant = {
name: 'Maturity Model',
url: '/guides/platform/maturity-model',
},
{ name: 'Production Checklist', url: '/guides/platform/going-into-prod' },
{
name: 'Production Checklist',
url: '/guides/platform/going-into-prod',
},
],
},
{
@@ -1515,28 +1790,46 @@ export const self_hosting: NavMenuConstant = {
{
name: 'Storage Server',
items: [
{ name: 'Reference', url: '/reference/self-hosting-storage/introduction' },
{
name: 'Reference',
url: '/reference/self-hosting-storage/introduction',
},
{ name: 'Configuration', url: '/guides/self-hosting/storage/config' },
],
},
{
name: 'Realtime Server',
items: [
{ name: 'Reference', url: '/reference/self-hosting-realtime/introduction' },
{
name: 'Reference',
url: '/reference/self-hosting-realtime/introduction',
},
{ name: 'Configuration', url: '/guides/self-hosting/realtime/config' },
],
},
{
name: 'Analytics Server',
items: [
{ name: 'Reference', url: '/reference/self-hosting-analytics/introduction', items: [] },
{ name: 'Configuration', url: '/guides/self-hosting/analytics/config', items: [] },
{
name: 'Reference',
url: '/reference/self-hosting-analytics/introduction',
items: [],
},
{
name: 'Configuration',
url: '/guides/self-hosting/analytics/config',
items: [],
},
],
},
{
name: 'Functions Server',
items: [
{ name: 'Reference', url: '/reference/self-hosting-functions/introduction', items: [] },
{
name: 'Reference',
url: '/reference/self-hosting-functions/introduction',
items: [],
},
],
},
],
@@ -1833,3 +2126,9 @@ export const references = [
],
},
]
export const navDataForMdx = {
nativeMobileLoginItems: NativeMobileLoginItems,
phoneLoginsItems: PhoneLoginsItems,
socialLoginItems: SocialLoginItems,
}
@@ -260,21 +260,21 @@ function getMenuElement(menu: Menu) {
const NavigationMenu = () => {
const router = useRouter()
function handleRouteChange(url: string) {
const menu = getMenuByUrl(router.basePath, url)
if (menu) {
menuState.setMenuLevelId(menu.id)
}
}
useEffect(() => {
function handleRouteChange(url: string) {
const menu = getMenuByUrl(router.basePath, url)
if (menu) {
menuState.setMenuLevelId(menu.id)
}
}
handleRouteChange(router.basePath + router.asPath)
// Listen for page changes after a navigation or when the query changes
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
}, [router.asPath, router.basePath, router.events])
const level = useMenuLevelId()
const menu = getMenuById(level)
@@ -1,61 +1,21 @@
import { useTheme } from 'next-themes'
import Image from 'next/legacy/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useState } from 'react'
import { FC } from 'react'
import { Button, IconCommand, IconGitHub, IconSearch, SearchButton } from 'ui'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { getPageType } from '~/lib/helpers'
import { REFERENCES } from './NavigationMenu.constants'
import ThemeToggle from '@ui/components/ThemeProvider/ThemeToggle'
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
const TopNavBar: FC = () => {
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const { resolvedTheme } = useTheme()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const { asPath, push } = useRouter()
const pathSegments = asPath.split('/')
const library = pathSegments.length >= 3 ? pathSegments[2] : undefined
const libraryMeta = REFERENCES?.[library] ?? undefined
const versions = libraryMeta?.versions ?? []
const version = versions.includes(pathSegments[pathSegments.indexOf(library) + 1])
? pathSegments[pathSegments.indexOf(library) + 1]
: versions[0]
const pageType = getPageType(asPath)
const pageLinks = [
{ text: 'Guides', key: 'docs', link: '/' },
{ text: 'Reference', key: 'reference', link: '/reference' },
]
const onSelectVersion = (version: string) => {
// [Joshen] Ideally we use <Link> but this works for now
if (!library) return
if (version === versions[0]) {
push(`/reference/${library}`)
} else {
push(`/reference/${library}/${version}`)
}
}
// [Joshen] Kaizen: Use UI library's SidePanel for this
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen)
const sidebar = document.querySelector('.sidebar-menu-container')
const contentPane = document.querySelector('.main-content-pane')
sidebar.classList.toggle('hidden')
contentPane.classList.toggle('hidden')
}
return (
<nav className="h-[60px] border-b backdrop-blur backdrop-filter bg bg-opacity-75">
<nav
aria-label="top bar"
className="h-[60px] border-b backdrop-blur backdrop-filter bg bg-opacity-75"
>
<div className="px-5 max-w-7xl mx-auto flex gap-3 justify-between items-center h-full">
<div className="lg:hidden">
<Link href="/" className=" flex items-center gap-2">
@@ -97,7 +57,10 @@ const TopNavBar: FC = () => {
<p className="hidden md:flex text-sm">Search docs...</p>
</div>
<div className="hidden md:flex items-center space-x-1">
<div className="md:flex items-center justify-center h-5 w-10 border rounded bg-surface-300 gap-1">
<div
aria-hidden="true"
className="md:flex items-center justify-center h-5 w-10 border rounded bg-surface-300 gap-1"
>
<IconCommand size={12} strokeWidth={1.5} />
<span className="text-[12px]">K</span>
</div>
@@ -118,12 +81,18 @@ const TopNavBar: FC = () => {
</a>
</Button>
)}
{process.env.NEXT_PUBLIC_DEV_AUTH_PAGE === 'true' && (
<Button asChild>
<Link href="/__dev-secret-auth">Dev-only secret sign-in</Link>
</Button>
)}
<Link
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
className="px-2.5 py-1"
>
<span className="sr-only">GitHub</span>
<IconGitHub
size={16}
className="text-foreground-light hover:text-foreground transition"
@@ -0,0 +1,103 @@
import { Check, ChevronsUpDown } from 'lucide-react'
import { noop } from 'lodash'
import { useState } from 'react'
import {
Button_Shadcn_ as Button,
Popover_Shadcn_ as Popover,
PopoverTrigger_Shadcn_ as PopoverTrigger,
PopoverContent_Shadcn_ as PopoverContent,
Command_Shadcn_ as Command,
CommandInput_Shadcn_ as CommandInput,
CommandEmpty_Shadcn_ as CommandEmpty,
CommandItem_Shadcn_ as CommandItem,
CommandGroup_Shadcn_ as CommandGroup,
cn,
ScrollArea,
} from 'ui'
export interface ComboBoxOption {
id: string
value: string
displayName: string
}
export function ComboBox<Opt extends ComboBoxOption>({
isLoading,
disabled,
name,
options,
selectedOption,
onSelectOption = noop,
className,
}: {
isLoading: boolean
disabled?: boolean
name: string
options: Opt[]
selectedOption?: string
onSelectOption?: (newValue: string) => void
className?: string
}) {
const [open, setOpen] = useState(false)
const selectedOptionDisplayName = options.find(
(option) => option.value === selectedOption
)?.displayName
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={disabled}
aria-expanded={open}
className={cn(
'overflow-hidden',
'h-auto min-h-10',
'flex justify-between',
'border-none',
'py-0 pl-0 pr-1 text-left',
className
)}
>
{isLoading
? 'Loading...'
: options.length === 0
? `No ${name} found`
: selectedOptionDisplayName ?? `Select a ${name}...`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" side="bottom">
<Command>
<CommandInput placeholder={`Search ${name}...`} className="border-none ring-0" />
<CommandEmpty>No {name} found.</CommandEmpty>
<CommandGroup>
<ScrollArea className={options.length > 10 ? 'h-[280px]' : ''}>
{options.map((option) => (
<CommandItem
key={option.id}
value={option.value}
onSelect={(selectedValue: string) => {
setOpen(false)
onSelectOption(selectedValue)
}}
className="cursor-pointer"
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedOption === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.displayName}
</CommandItem>
))}
</ScrollArea>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}
@@ -0,0 +1,409 @@
import { useIsLoggedIn, useIsUserLoading } from 'common'
import Link from 'next/link'
import { useEffect, useMemo } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { Button_Shadcn_ as Button, Input_Shadcn_ as Input, cn, IconCopy, IconCheck } from 'ui'
import { proxy, useSnapshot } from 'valtio'
import { useCopy } from '~/hooks/useCopy'
import { useBranchesQuery } from '~/lib/fetch/branches'
import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { useProjectApiQuery } from '~/lib/fetch/projectApi'
import { useProjectsQuery } from '~/lib/fetch/projects'
import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
import { ComboBox, ComboBoxOption } from './ProjectConfigVariables.ComboBox'
import {
type Branch,
type Org,
type Project,
type Variable,
fromBranchValue,
fromOrgProjectValue,
prettyFormatVariable,
toBranchValue,
toDisplayNameOrgProject,
toOrgProjectValue,
} from './ProjectConfigVariables.utils'
type ProjectOrgDataState =
| 'userLoading'
| 'loggedOut'
| 'loggedIn.dataPending'
| 'loggedIn.dataError'
| 'loggedIn.dataSuccess.hasData'
| 'loggedIn.dataSuccess.hasNoData'
type BranchesDataState =
| 'userLoading'
| 'loggedOut'
| 'loggedIn.noBranches'
| 'loggedIn.branches.dataPending'
| 'loggedIn.branches.dataError'
| 'loggedIn.branches.dataSuccess.hasData'
| 'loggedIn.branches.dataSuccess.noData'
type VariableDataState =
| 'userLoading'
| 'loggedOut'
| 'loggedIn.noSelectedProject'
| 'loggedIn.selectedProject.dataPending'
| 'loggedIn.selectedProject.dataError'
| 'loggedIn.selectedProject.dataSuccess'
const projectsStore = proxy({
selectedOrg: null as Org | null,
selectedProject: null as Project | null,
setSelectedOrgProject: (org: Org | null, project: Project | null) => {
projectsStore.selectedOrg = org
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_ORG, org?.id)
projectsStore.selectedProject = project
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT, project?.ref)
},
selectedBranch: null as Branch | null,
setSelectedBranch: (branch: Branch | null) => {
projectsStore.selectedBranch = branch
storeOrRemoveNull('local', LOCAL_STORAGE_KEYS.SAVED_BRANCH, branch?.id)
},
clear: () => {
projectsStore.setSelectedOrgProject(null, null)
projectsStore.setSelectedBranch(null)
},
})
function OrgProjectSelector() {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
const { selectedOrg, selectedProject, setSelectedOrgProject } = useSnapshot(projectsStore)
const {
data: organizations,
isPending: organizationsIsPending,
isError: organizationsIsError,
} = useOrganizationsQuery({ enabled: isLoggedIn })
const {
data: projects,
isPending: projectsIsPending,
isError: projectsIsError,
} = useProjectsQuery({ enabled: isLoggedIn })
const anyIsPending = organizationsIsPending || projectsIsPending
const anyIsError = organizationsIsError || projectsIsError
const stateSummary: ProjectOrgDataState = isUserLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: anyIsPending
? 'loggedIn.dataPending'
: anyIsError
? 'loggedIn.dataError'
: projects?.length === 0
? 'loggedIn.dataSuccess.hasNoData'
: 'loggedIn.dataSuccess.hasData'
const formattedData: ComboBoxOption[] = useMemo(
() =>
stateSummary !== 'loggedIn.dataSuccess.hasData'
? []
: projects.map((project) => {
const organization = organizations.find((org) => org.id === project.organization_id)
return {
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
id: project.ref,
value: toOrgProjectValue(organization, project),
displayName: toDisplayNameOrgProject(organization, project),
}
}),
[organizations, projects, stateSummary]
)
useEffect(() => {
if (stateSummary === 'loggedIn.dataSuccess.hasData' && (!selectedOrg || !selectedProject)) {
const storedMaybeOrgId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_ORG)
const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT)
let storedOrg: Org
let storedProject: Project
if (storedMaybeOrgId && storedMaybeProjectRef) {
// @ts-ignore -- problem in OpenAPI spec -- org id is returned as number, not string
storedOrg = organizations.find((org) => org.id === Number(storedMaybeOrgId))
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
storedProject = projects.find((project) => project.ref === storedMaybeProjectRef)
}
if (storedOrg && storedProject && storedProject.organization_id === storedOrg.id) {
setSelectedOrgProject(storedOrg, storedProject)
} else {
const firstProject = projects[0]
const matchingOrg = organizations.find((org) => org.id === firstProject.organization_id)
if (matchingOrg) setSelectedOrgProject(matchingOrg, firstProject)
}
}
}, [organizations, projects, selectedOrg, selectedProject, setSelectedOrgProject, stateSummary])
return (
<ComboBox
name="project"
isLoading={stateSummary === 'userLoading' || stateSummary === 'loggedIn.dataPending'}
disabled={
stateSummary === 'loggedOut' ||
stateSummary === 'loggedIn.dataError' ||
stateSummary === 'loggedIn.dataSuccess.hasNoData'
}
options={formattedData}
selectedOption={
selectedOrg && selectedProject ? toOrgProjectValue(selectedOrg, selectedProject) : undefined
}
onSelectOption={(optionValue) => {
const [orgId, projectRef] = fromOrgProjectValue(optionValue)
if (!orgId || !projectRef) return
const org = organizations.find((org) => org.id === orgId)
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
const project = projects.find((project) => project.ref === projectRef)
if (org && project && project.organization_id === org.id) {
setSelectedOrgProject(org, project)
}
}}
/>
)
}
function BranchSelector() {
const userLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
const { selectedProject, selectedBranch, setSelectedBranch } = useSnapshot(projectsStore)
// @ts-ignore -- problem in OpenAPI spec -- project has is_branch_enabled property
const hasBranches = selectedProject?.is_branch_enabled ?? false
const { data, isPending, isError } = useBranchesQuery(
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
{ projectRef: selectedProject?.ref },
{ enabled: isLoggedIn && hasBranches }
)
const stateSummary: BranchesDataState = userLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !hasBranches
? 'loggedIn.noBranches'
: isPending
? 'loggedIn.branches.dataPending'
: isError
? 'loggedIn.branches.dataError'
: data.length === 0
? 'loggedIn.branches.dataSuccess.noData'
: 'loggedIn.branches.dataSuccess.hasData'
const formattedData: ComboBoxOption[] =
stateSummary !== 'loggedIn.branches.dataSuccess.hasData'
? []
: data.map((branch) => ({
id: branch.id,
displayName: branch.name,
value: toBranchValue(branch),
}))
useEffect(() => {
if (stateSummary === 'loggedIn.branches.dataSuccess.hasData' && !selectedBranch) {
const storedMaybeBranchId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_BRANCH)
let storedBranch: Branch
if (storedMaybeBranchId) {
storedBranch = data.find((branch) => branch.id === storedMaybeBranchId)
}
if (storedBranch) {
setSelectedBranch(storedBranch)
} else {
const productionBranch = data.find(
(branch) => branch.project_ref === branch.parent_project_ref
)
setSelectedBranch(productionBranch ?? data[0])
}
}
}, [data, selectedBranch, setSelectedBranch, stateSummary])
return hasBranches ? (
<ComboBox
name="branch"
isLoading={stateSummary === 'userLoading' || stateSummary === 'loggedIn.branches.dataPending'}
disabled={
stateSummary === 'loggedOut' ||
stateSummary === 'loggedIn.noBranches' ||
stateSummary === 'loggedIn.branches.dataError' ||
stateSummary === 'loggedIn.branches.dataSuccess.noData'
}
options={formattedData}
selectedOption={selectedBranch ? toBranchValue(selectedBranch) : undefined}
onSelectOption={(option) => {
const [branchId] = fromBranchValue(option)
if (branchId) {
const branch = data.find((branch) => branch.id === branchId)
if (branch) setSelectedBranch(branch)
}
}}
/>
) : null
}
function VariableView({ variable, className }: { variable: Variable; className?: string }) {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
const { selectedProject, selectedBranch } = useSnapshot(projectsStore)
// @ts-ignore -- problem in OpenAPI spec -- project has is_branch-enabled property
const hasBranches = selectedProject?.is_branch_enabled ?? false
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref
const {
data: apiData,
isPending,
isError,
} = useProjectApiQuery(
{
projectRef: ref,
},
{ enabled: isLoggedIn && !!ref }
)
const stateSummary: VariableDataState = isUserLoading
? 'userLoading'
: !isLoggedIn
? 'loggedOut'
: !ref
? 'loggedIn.noSelectedProject'
: isPending
? 'loggedIn.selectedProject.dataPending'
: isError
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
let variableValue: string = null
if (stateSummary === 'loggedIn.selectedProject.dataSuccess') {
switch (variable) {
case 'url':
variableValue = `${apiData.autoApiService.protocol || 'https'}://${
apiData.autoApiService.endpoint
}`
break
case 'anonKey':
variableValue = apiData.autoApiService.defaultApiKey
break
}
}
const { copied, handleCopy } = useCopy()
return (
<>
<div className={cn('flex items-center gap-2', className)}>
<Input
disabled
type="text"
className="font-mono"
value={
stateSummary === 'userLoading' ||
stateSummary === 'loggedIn.selectedProject.dataPending'
? 'Loading...'
: stateSummary === 'loggedIn.selectedProject.dataSuccess'
? variableValue
: `YOUR ${prettyFormatVariable[variable].toUpperCase()}`
}
/>
<CopyToClipboard text={variableValue ?? ''}>
<Button
disabled={!variableValue}
variant="ghost"
className="px-0"
onClick={handleCopy}
aria-label="Copy"
>
{copied ? <IconCheck /> : <IconCopy />}
</Button>
</CopyToClipboard>
</div>
{stateSummary === 'loggedIn.selectedProject.dataError' && (
<p className="text-foreground-muted text-sm mt-2 mb-0 ml-1">
You can also copy your {prettyFormatVariable[variable]} from the{' '}
<Link
className="text-foreground-muted"
href="https://supabase.com/dashboard/project/_/settings/api"
rel="noreferrer noopener"
target="_blank"
>
dashboard
</Link>
.
</p>
)}
</>
)
}
function LoginHint({ variable }: { variable: Variable }) {
const isUserLoading = useIsUserLoading()
const isLoggedIn = useIsLoggedIn()
if (isUserLoading || isLoggedIn) return null
return (
<p className="text-foreground-muted text-sm mt-2 mb-0 ml-1">
To get your {prettyFormatVariable[variable]},{' '}
<Link
className="text-foreground-muted"
href="https://supabase.com/dashboard"
rel="noreferrer noopener"
target="_blank"
>
log in
</Link>
.
</p>
)
}
function ProjectConfigVariablesInternal({ variable }: { variable: Variable }) {
const { clear: clearSharedStoreData } = useSnapshot(projectsStore)
useOnLogout(clearSharedStoreData)
return (
<div className="max-w-[min(100%, 500px)] my-6">
<h6 className={cn('mt-0 mb-1', 'text-foreground')}>{prettyFormatVariable[variable]}</h6>
<div className="flex flex-wrap gap-x-6">
<OrgProjectSelector />
<BranchSelector />
</div>
<VariableView variable={variable} className="mt-1" />
<LoginHint variable={variable} />
</div>
)
}
export const ProjectConfigVariables = withErrorBoundary(ProjectConfigVariablesInternal, {
fallback: (
<p>
Couldn&apos;t display your API settings. You can get them from the{' '}
<Link
href="https://supabase.com/dashboard/project/_/settings/api"
rel="noreferrer noopener"
target="_blank"
>
dashboard
</Link>
.
</p>
),
})
@@ -0,0 +1,79 @@
import type { BranchesData } from '~/lib/fetch/branches'
import type { OrganizationsData } from '~/lib/fetch/organizations'
import type { ProjectsData } from '~/lib/fetch/projects'
export type Org = OrganizationsData[number]
export type Project = ProjectsData[number]
export type Branch = BranchesData[number]
export type Variable = 'url' | 'anonKey'
function removeDoubleQuotes(str: string) {
return str.replaceAll('"', '')
}
function escapeDoubleQuotes(str: string) {
return str.replaceAll('"', '\\"')
}
function unescapeDoubleQuotes(str: string) {
/**
* Regex matches a backslash followed by a double quote, except when the
* backslash is escaped.
*
* Negative lookbehind:
* \\(?:\\\\)* -> An odd number of backslashes (means the next backslash is escaped)
* (?:^|[^\\]) -> Backslash group is at start or follows non-backslash
*/
return str.replace(/(?<!(?:^|[^\\])\\(?:\\\\)*)\\"/g, '"')
}
export const prettyFormatVariable: Record<Variable, string> = {
url: 'Project URL',
anonKey: 'Anon key',
}
export function toDisplayNameOrgProject(org: Org, project: Project) {
return `${org.name} / ${project.name}`
}
export function toOrgProjectValue(org: Org, project: Project) {
return escapeDoubleQuotes(
// @ts-ignore -- problem in OpenAPI spec -- project has ref property
JSON.stringify([org.id, project.ref, removeDoubleQuotes(toDisplayNameOrgProject(org, project))])
)
}
export function fromOrgProjectValue(
maybeOrgProject: string
): [string, string, string] | [null, null, null] {
try {
// not restoring the double quotes on the display name is fine because it's only used for
// command fuzzy search, not for exact/literal matching
const data = JSON.parse(unescapeDoubleQuotes(maybeOrgProject))
if (!Array.isArray(data) || data.length !== 3) {
throw Error("Shape of parsed JSON doesn't match form of org and project value")
}
return data as [string, string, string]
} catch {
return [null, null, null]
}
}
export function toBranchValue(branch: Branch) {
return escapeDoubleQuotes(JSON.stringify([branch.id, removeDoubleQuotes(branch.name)]))
}
export function fromBranchValue(maybeBranch: string): [string, string] | [null, null] {
try {
// not restoring the double quotes on the branch name is fine because it's only used for
// command fuzzy search, not for exact/literal matching
const data = JSON.parse(unescapeDoubleQuotes(maybeBranch))
if (!Array.isArray(data) || data.length !== 2) {
throw Error("Shape of parsed JSON doesn't match form of branch value")
}
return data as [string, string]
} catch {
return [null, null]
}
}
@@ -0,0 +1 @@
export { ProjectConfigVariables } from './ProjectConfigVariables'
@@ -158,8 +158,8 @@ export default function RealtimeLimitsEstimater({}) {
{computeAddOn === 'micro'
? 'Micro'
: computeAddOn === 'small'
? 'Small to medium'
: 'Large to 16XL'}
? 'Small to medium'
: 'Large to 16XL'}
</h4>
<table className="table-auto">
<thead>
@@ -0,0 +1,11 @@
import { lazy, Suspense } from 'react'
const Estimator = lazy(() => import('./RealtimeLimitsEstimator'))
export function RealtimeLimitsEstimator() {
return (
<Suspense>
<Estimator />
</Suspense>
)
}
+19
View File
@@ -0,0 +1,19 @@
import { ReactNode } from 'react'
import { logConstants } from 'shared-data'
const sharedData = {
logConstants,
}
function SharedData({
data,
children,
}: {
data: keyof typeof sharedData
children: (selectedData: (typeof sharedData)[keyof typeof sharedData]) => ReactNode
}) {
const selectedData = sharedData[data]
return children(selectedData)
}
export { SharedData }
+115 -73
View File
@@ -1,78 +1,100 @@
/**
* This entire file could do with a cleanup and better code-splitting, but one thing at a time.
*/
// Basic UI things
import Link from 'next/link'
import { Alert, Button, CodeBlock, GlassPanel, markdownComponents, Tabs } from 'ui'
import StepHikeCompact from '~/components/StepHikeCompact'
import { Accordion, Admonition, Alert, Button, CodeBlock, markdownComponents, Tabs } from 'ui'
import { GlassPanel } from 'ui-patterns/GlassPanel'
import { IconPanel } from 'ui-patterns/IconPanel'
import { ThemeImage } from 'ui-patterns/ThemeImage'
// Common components
import { CH } from '@code-hike/mdx/components'
import StepHikeCompact from '~/components/StepHikeCompact'
import ButtonCard from './ButtonCard'
import { Heading } from './CustomHTMLElements'
// Reference guide specific
// [Charis] I think we can factor these out so they aren't in the bundle for absolutely everything
import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
import RefHeaderSection from './reference/RefHeaderSection'
// Other components
import RefSubLayout from '~/layouts/ref/RefSubLayout'
import { Heading } from './CustomHTMLElements'
import AuthProviders from '~/components/AuthProviders'
import Options from '~/components/Options'
import Param from '~/components/Params'
import { ProjectConfigVariables } from './ProjectConfigVariables'
// Data wrappers
import { NavData } from './NavData'
import { SharedData } from './SharedData'
// Partials
import DatabaseSetup from './MDX/database_setup.mdx'
import GetSessionWarning from './MDX/get_session_warning.mdx'
import HuggingFaceDeployment from './MDX/ai/quickstart_hf_deployment.mdx'
import KotlinProjectSetup from './MDX/kotlin_project_setup.mdx'
import MigrationWarnings from './MDX/migration_warnings.mdx'
import ProjectSetup from './MDX/project_setup.mdx'
import QuickstartIntro from './MDX/quickstart_intro.mdx'
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
import StorageManagement from './MDX/storage_management.mdx'
import MigrationWarnings from './MDX/migration_warnings.mdx'
import GetSessionWarning from './MDX/get_session_warning.mdx'
import { CH } from '@code-hike/mdx/components'
import RefHeaderSection from './reference/RefHeaderSection'
// Ref version specific
import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlobalFlagsHandler'
import Options from '~/components/Options'
import Param from '~/components/Params'
import { Admonition, ThemeImage } from 'ui'
// Icons
import {
IconMenuJavascript,
IconMenuHome,
IconMenuGettingStarted,
IconMenuDatabase,
IconMenuRestApis,
IconMenuAuth,
IconMenuEdgeFunctions,
IconMenuRealtime,
IconMenuStorage,
IconMenuPlatform,
IconMenuResources,
IconMenuSelfHosting,
IconMenuIntegrations,
IconMenuFlutter,
IconMenuPython,
IconMenuCsharp,
IconMenuSwift,
IconMenuKotlin,
IconMenuApi,
IconMenuAuth,
IconMenuCli,
IconMenuCsharp,
IconMenuDatabase,
IconMenuEdgeFunctions,
IconMenuFlutter,
IconMenuGettingStarted,
IconMenuHome,
IconMenuIntegrations,
IconMenuJavascript,
IconMenuKotlin,
IconMenuPlatform,
IconMenuPython,
IconMenuRealtime,
IconMenuResources,
IconMenuRestApis,
IconMenuSelfHosting,
IconMenuStorage,
IconMenuSwift,
} from './Navigation/NavigationMenu/HomeMenuIcons'
import { IconArrowDown, IconCheck } from 'ui'
// Heavy/rare (lazy-loaded)
import { AppleSecretGenerator } from './AppleSecretGenerator'
import { Extensions } from './Extensions'
import { JwtGenerator } from './JwtGenerator'
import { Mermaid } from './Mermaid'
import { RealtimeLimitsEstimator } from './RealtimeLimitsEstimator'
const components = {
...markdownComponents,
Accordion,
Admonition,
Button,
ButtonCard,
CH,
CodeBlock,
GlassPanel,
Link,
QuickstartIntro,
DatabaseSetup,
ProjectSetup,
MigrationWarnings,
GetSessionWarning,
SocialProviderSetup,
SocialProviderSettingsSupabase,
StepHikeCompact,
StorageManagement,
Alert: (props: any) => (
<Alert {...props} className="not-prose">
{props.children}
</Alert>
),
Tabs: (props: any) => <Tabs wrappable {...props} />,
TabPanel: (props: any) => <Tabs.Panel {...props}>{props.children}</Tabs.Panel>,
AppleSecretGenerator,
AuthProviders,
Button,
ButtonCard,
CH,
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
CodeBlock,
DatabaseSetup,
Extensions,
GetSessionWarning,
GlassPanel,
h2: (props: any) => (
<Heading tag="h2" {...props}>
{props.children}
@@ -88,32 +110,52 @@ const components = {
{props.children}
</Heading>
),
HuggingFaceDeployment,
IconCheck,
IconMenuApi,
IconArrowDown,
IconMenuAuth,
IconMenuCli,
IconMenuCsharp,
IconMenuDatabase,
IconMenuEdgeFunctions,
IconMenuFlutter,
IconMenuGettingStarted,
IconMenuHome,
IconMenuIntegrations,
IconMenuJavascript,
IconMenuKotlin,
IconMenuPlatform,
IconMenuPython,
IconMenuRealtime,
IconMenuResources,
IconMenuRestApis,
IconMenuSelfHosting,
IconMenuStorage,
IconMenuSwift,
IconPanel,
Image: (props: any) => <ThemeImage fill className="object-contain" {...props} />,
RefSubLayout,
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
JwtGenerator,
KotlinProjectSetup,
Link,
Mermaid,
MigrationWarnings,
NavData,
Options,
Param,
IconMenuJavascript,
IconMenuHome,
IconMenuGettingStarted,
IconMenuDatabase,
IconMenuRestApis,
IconMenuAuth,
IconMenuEdgeFunctions,
IconMenuRealtime,
IconMenuStorage,
IconMenuPlatform,
IconMenuResources,
IconMenuSelfHosting,
IconMenuIntegrations,
IconMenuFlutter,
IconMenuPython,
IconMenuCsharp,
IconMenuKotlin,
IconMenuSwift,
IconMenuApi,
IconMenuCli,
ProjectConfigVariables,
ProjectSetup,
QuickstartIntro,
RealtimeLimitsEstimator,
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
RefSubLayout,
SharedData,
SocialProviderSettingsSupabase,
SocialProviderSetup,
StepHikeCompact,
StorageManagement,
TabPanel: (props: any) => <Tabs.Panel {...props}>{props.children}</Tabs.Panel>,
Tabs: (props: any) => <Tabs wrappable {...props} />,
}
export default components
@@ -126,14 +126,14 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
const codeBlockLang = example?.code?.startsWith('```js')
? 'js'
: example?.code?.startsWith('```ts')
? 'ts'
: example?.code?.startsWith('```dart')
? 'dart'
: example?.code?.startsWith('```c#')
? 'csharp'
: example?.code?.startsWith('```kotlin')
? 'kotlin'
: 'js'
? 'ts'
: example?.code?.startsWith('```dart')
? 'dart'
: example?.code?.startsWith('```c#')
? 'csharp'
: example?.code?.startsWith('```kotlin')
? 'kotlin'
: 'js'
// `
// import { createClient } from '@supabase/supabase-js'
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai',
title: 'AI & Vectors',
description: 'The best vector database is the database you already have.',
subtitle: 'The best vector database is the database you already have.',
sidebar_label: 'Overview',
}
---
id: 'ai'
title: 'AI & Vectors'
description: 'The best vector database is the database you already have.'
subtitle: 'The best vector database is the database you already have.'
hideToc: true
---
Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale.
@@ -109,7 +107,26 @@ export const integrations = [
## Case studies
<div className="grid md:grid-cols-12 gap-4 not-prose">
{customers.map((x) => (
{[
{
name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector',
description:
'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.',
href: 'https://supabase.com/customers/berriai',
},
{
name: 'Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings',
description:
'How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector',
href: 'https://supabase.com/customers/mendableai',
},
{
name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites',
description:
"AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions",
href: 'https://supabase.com/customers/markprompt',
},
].map((x) => (
<div className="col-span-4" key={x.href}>
<Link href={x.href} passHref>
<GlassPanel title={x.name}>{x.description}</GlassPanel>
@@ -117,28 +134,3 @@ export const integrations = [
</div>
))}
</div>
export const customers = [
{
name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector',
description:
'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.',
href: 'https://supabase.com/customers/berriai',
},
{
name: 'Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings',
description:
'How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector',
href: 'https://supabase.com/customers/mendableai',
},
{
name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites',
description:
"AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions",
href: 'https://supabase.com/customers/markprompt',
},
]
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export default Page
@@ -0,0 +1,337 @@
---
id: 'ai-choosing-compute-addon'
title: 'Choosing your Compute Add-on'
description: 'Choosing the right Compute Add-on for your vector workload.'
subtitle: 'Choosing the right Compute Add-on for your vector workload.'
sidebar_label: 'Choosing Compute Add-on'
---
You have two options for scaling your vector workload:
1. Increase the size of your database. This guide will help you choose the right size for your workload.
2. Spread your workload across multiple databases. You can find more details about this approach in [Engineering for Scale](engineering-for-scale).
## Dimensionality
The number of dimensions in your embeddings is the most important factor in choosing the right Compute Add-on. In general, the lower the dimensionality the better the performance. We've provided guidance for some of the more common embedding dimensions below. For each benchmark, we used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create both the `IVFFlat` and `HNSW` indexes for `inner-product` distance measure for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:
## HNSW
### 384 dimensions
This benchmark uses the dbpedia-entities-openai-1M dataset containing 1,000,000 embeddings of text, regenerated for 384 dimension embeddings. Each embedding is generated using [gte-small](https://huggingface.co/Supabase/gte-small).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gte384"
queryGroup="benchmark"
>
<TabPanel id="gte384" label="gte-small-384">
| Plan | Vectors | m | ef_construction | ef_search | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ---------- | ------ |
| Micro | 100,000 | 16 | 64 | 60 | 580 | 0.017 sec | 0.024 sec | 1.2 (Swap) | 1 GB |
| Small | 250,000 | 24 | 64 | 60 | 440 | 0.022 sec | 0.033 sec | 2 GB | 2 GB |
| Medium | 500,000 | 24 | 64 | 80 | 350 | 0.028 sec | 0.045 sec | 4 GB | 4 GB |
| Large | 1,000,000 | 32 | 80 | 100 | 270 | 0.073 sec | 0.108 sec | 7 GB | 8 GB |
| XL | 1,000,000 | 32 | 80 | 100 | 525 | 0.038 sec | 0.059 sec | 9 GB | 16 GB |
| 2XL | 1,000,000 | 32 | 80 | 100 | 790 | 0.025 sec | 0.037 sec | 9 GB | 32 GB |
| 4XL | 1,000,000 | 32 | 80 | 100 | 1650 | 0.015 sec | 0.018 sec | 11 GB | 64 GB |
| 8XL | 1,000,000 | 32 | 80 | 100 | 2690 | 0.015 sec | 0.016 sec | 13 GB | 128 GB |
| 12XL | 1,000,000 | 32 | 80 | 100 | 3900 | 0.014 sec | 0.016 sec | 13 GB | 192 GB |
| 16XL | 1,000,000 | 32 | 80 | 100 | 4200 | 0.014 sec | 0.016 sec | 20 GB | 256 GB |
Accuracy was 0.99 for benchmarks.
</TabPanel>
</Tabs>
### 960 dimensions
This benchmark uses the [gist-960](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="openai1536"
queryGroup="benchmark"
>
<TabPanel id="openai1536" label="OpenAI-1536">
| Plan | Vectors | m | ef_construction | ef_search | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 30,000 | 16 | 64 | 65 | 430 | 0.024 sec | 0.034 sec | 1.2 GB (Swap) | 1 GB |
| Small | 100,000 | 32 | 80 | 60 | 260 | 0.040 sec | 0.054 sec | 2.2 GB (Swap) | 2 GB |
| Medium | 250,000 | 32 | 80 | 90 | 120 | 0.083 sec | 0.106 sec | 4 GB | 4 GB |
| Large | 500,000 | 32 | 80 | 120 | 160 | 0.063 sec | 0.087 sec | 7 GB | 8 GB |
| XL | 1,000,000 | 32 | 80 | 200 | 200 | 0.049 sec | 0.072 sec | 13 GB | 16 GB |
| 2XL | 1,000,000 | 32 | 80 | 200 | 340 | 0.025 sec | 0.029 sec | 17 GB | 32 GB |
| 4XL | 1,000,000 | 32 | 80 | 200 | 630 | 0.031 sec | 0.050 sec | 18 GB | 64 GB |
| 8XL | 1,000,000 | 32 | 80 | 200 | 1100 | 0.034 sec | 0.048 sec | 19 GB | 128 GB |
| 12XL | 1,000,000 | 32 | 80 | 200 | 1420 | 0.041 sec | 0.095 sec | 21 GB | 192 GB |
| 16XL | 1,000,000 | 32 | 80 | 200 | 1650 | 0.037 sec | 0.081 sec | 23 GB | 256 GB |
Accuracy was 0.99 for benchmarks.
QPS can also be improved by increasing [`m` and `ef_construction`](/docs/guides/ai/going-to-prod#hnsw-understanding-efconstruction--efsearch--and-m). This will allow you to use a smaller value for `ef_search` and increase QPS.
</TabPanel>
</Tabs>
### 1536 dimensions
This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. And 224,482 embeddings from [Wikipedia articles](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings) for compute add-ons `large` and below. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="openai1536"
queryGroup="benchmark"
>
<TabPanel id="openai1536" label="OpenAI-1536">
| Plan | Vectors | m | ef_construction | ef_search | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 15,000 | 16 | 40 | 40 | 480 | 0.011 sec | 0.016 sec | 1.2 GB (Swap) | 1 GB |
| Small | 50,000 | 32 | 64 | 100 | 175 | 0.031 sec | 0.051 sec | 2.2 GB (Swap) | 2 GB |
| Medium | 100,000 | 32 | 64 | 100 | 240 | 0.083 sec | 0.126 sec | 4 GB | 4 GB |
| Large | 224,482 | 32 | 64 | 100 | 280 | 0.017 sec | 0.028 sec | 8 GB | 8 GB |
| XL | 500,000 | 24 | 56 | 100 | 360 | 0.055 sec | 0.135 sec | 13 GB | 16 GB |
| 2XL | 1,000,000 | 24 | 56 | 250 | 560 | 0.036 sec | 0.058 sec | 32 GB | 32 GB |
| 4XL | 1,000,000 | 24 | 56 | 250 | 950 | 0.021 sec | 0.033 sec | 39 GB | 64 GB |
| 8XL | 1,000,000 | 24 | 56 | 250 | 1650 | 0.016 sec | 0.023 sec | 40 GB | 128 GB |
| 12XL | 1,000,000 | 24 | 56 | 250 | 1900 | 0.015 sec | 0.021 sec | 38 GB | 192 GB |
| 16XL | 1,000,000 | 24 | 56 | 250 | 2200 | 0.015 sec | 0.020 sec | 40 GB | 256 GB |
Accuracy was 0.99 for benchmarks.
QPS can also be improved by increasing [`m` and `ef_construction`](/docs/guides/ai/going-to-prod#hnsw-understanding-efconstruction--efsearch--and-m). This will allow you to use a smaller value for `ef_search` and increase QPS. For example, increasing `m` to 32 and `ef_construction` to 80 for 4XL will increase QPS to 1280.
</TabPanel>
</Tabs>
<Admonition type="note">
It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.
</Admonition>
<div>
<Image
alt="multi database"
src={{
light: '/docs/img/ai/instance-type/hnsw-dims--light.png',
dark: '/docs/img/ai/instance-type/hnsw-dims--dark.png',
}}
/>
</div>
## IVFFlat
### 384 dimensions
This benchmark uses the dbpedia-entities-openai-1M dataset containing 1,000,000 embeddings of text, regenerated for 384 dimension embeddings. Each embedding is generated using [gte-small](https://huggingface.co/Supabase/gte-small).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gte384_98"
queryGroup="benchmark"
>
<TabPanel id="gte384_98" label="gte-small-384, accuracy=.98">
| Plan | Vectors | Lists | Probes | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ------ | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 100,000 | 500 | 50 | 205 | 0.048 sec | 0.066 sec | 1.2 GB (Swap) | 1 GB |
| Small | 250,000 | 1000 | 60 | 160 | 0.062 sec | 0.079 sec | 2 GB | 2 GB |
| Medium | 500,000 | 2000 | 80 | 120 | 0.082 sec | 0.104 sec | 3.2 GB | 4 GB |
| Large | 1,000,000 | 5000 | 150 | 75 | 0.269 sec | 0.375 sec | 6.5 GB | 8 GB |
| XL | 1,000,000 | 5000 | 150 | 150 | 0.131 sec | 0.178 sec | 9 GB | 16 GB |
| 2XL | 1,000,000 | 5000 | 150 | 300 | 0.066 sec | 0.099 sec | 10 GB | 32 GB |
| 4XL | 1,000,000 | 5000 | 150 | 570 | 0.035 sec | 0.046 sec | 10 GB | 64 GB |
| 8XL | 1,000,000 | 5000 | 150 | 1400 | 0.023 sec | 0.028 sec | 12 GB | 128 GB |
| 12XL | 1,000,000 | 5000 | 150 | 1550 | 0.030 sec | 0.039 sec | 12 GB | 192 GB |
| 16XL | 1,000,000 | 5000 | 150 | 1800 | 0.030 sec | 0.039 sec | 16 GB | 256 GB |
</TabPanel>
<TabPanel id="gte384_99" label="gte-small-384, accuracy=.99">
| Plan | Vectors | Lists | Probes | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ------ | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 100,000 | 500 | 70 | 160 | 0.062 sec | 0.079 sec | 1.2 GB (Swap) | 1 GB |
| Small | 250,000 | 1000 | 100 | 100 | 0.096 sec | 0.113 sec | 2 GB | 2 GB |
| Medium | 500,000 | 2000 | 120 | 85 | 0.117 sec | 0.147 sec | 3.2 GB | 4 GB |
| Large | 1,000,000 | 5000 | 250 | 50 | 0.394 sec | 0.521 sec | 6.5 GB | 8 GB |
| XL | 1,000,000 | 5000 | 250 | 100 | 0.197 sec | 0.255 sec | 10 GB | 16 GB |
| 2XL | 1,000,000 | 5000 | 250 | 200 | 0.098 sec | 0.140 sec | 10 GB | 32 GB |
| 4XL | 1,000,000 | 5000 | 250 | 390 | 0.051 sec | 0.066 sec | 11 GB | 64 GB |
| 8XL | 1,000,000 | 5000 | 250 | 850 | 0.036 sec | 0.042 sec | 12 GB | 128 GB |
| 12XL | 1,000,000 | 5000 | 250 | 1000 | 0.043 sec | 0.055 sec | 13 GB | 192 GB |
| 16XL | 1,000,000 | 5000 | 250 | 1200 | 0.043 sec | 0.055 sec | 16 GB | 256 GB |
</TabPanel>
</Tabs>
### 960 dimensions
This benchmark uses the [gist-960](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gist960"
queryGroup="benchmark"
>
<TabPanel id="gist960" label="gist-960, probes = 10">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 30,000 | 30 | 75 | 0.065 sec | 0.088 sec | 1.1 GB (Swap) | 1 GB |
| Small | 100,000 | 100 | 78 | 0.064 sec | 0.092 sec | 1.8 GB | 2 GB |
| Medium | 250,000 | 250 | 58 | 0.085 sec | 0.129 sec | 3.2 GB | 4 GB |
| Large | 500,000 | 500 | 55 | 0.088 sec | 0.140 sec | 5 GB | 8 GB |
| XL | 1,000,000 | 1000 | 110 | 0.046 sec | 0.070 sec | 14 GB | 16 GB |
| 2XL | 1,000,000 | 1000 | 235 | 0.083 sec | 0.136 sec | 10 GB | 32 GB |
| 4XL | 1,000,000 | 1000 | 420 | 0.071 sec | 0.106 sec | 11 GB | 64 GB |
| 8XL | 1,000,000 | 1000 | 815 | 0.072 sec | 0.106 sec | 13 GB | 128 GB |
| 12XL | 1,000,000 | 1000 | 1150 | 0.052 sec | 0.078 sec | 15.5 GB | 192 GB |
| 16XL | 1,000,000 | 1000 | 1345 | 0.072 sec | 0.106 sec | 17.5 GB | 256 GB |
</TabPanel>
</Tabs>
### 1536 dimensions
This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dbpedia1536"
queryGroup="benchmark"
>
<TabPanel id="dbpedia1536" label="OpenAI-1536, probes = 10">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro | 20,000 | 40 | 135 | 0.372 sec | 0.412 sec | 1.2 GB (Swap) | 1 GB |
| Small | 50,000 | 100 | 140 | 0.357 sec | 0.398 sec | 1.8 GB | 2 GB |
| Medium | 100,000 | 200 | 130 | 0.383 sec | 0.446 sec | 3.7 GB | 4 GB |
| Large | 250,000 | 500 | 130 | 0.378 sec | 0.434 sec | 7 GB | 8 GB |
| XL | 500,000 | 1000 | 235 | 0.213 sec | 0.271 sec | 13.5 GB | 16 GB |
| 2XL | 1,000,000 | 2000 | 380 | 0.133 sec | 0.236 sec | 30 GB | 32 GB |
| 4XL | 1,000,000 | 2000 | 720 | 0.068 sec | 0.120 sec | 35 GB | 64 GB |
| 8XL | 1,000,000 | 2000 | 1250 | 0.039 sec | 0.066 sec | 38 GB | 128 GB |
| 12XL | 1,000,000 | 2000 | 1600 | 0.030 sec | 0.052 sec | 41 GB | 192 GB |
| 16XL | 1,000,000 | 2000 | 1790 | 0.029 sec | 0.051 sec | 45 GB | 256 GB |
For 1,000,000 vectors 10 probes results to accuracy of 0.91. And for 500,000 vectors and below 10 probes results to accuracy in the range of 0.95 - 0.99. To increase accuracy, you need to increase the number of probes.
</TabPanel>
<TabPanel id="dbpedia1536_40" label="OpenAI-1536, probes = 40">
| Plan | Vectors | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Micro | 20,000 | 40 | - | - | - | - | 1 GB |
| Small | 50,000 | 100 | - | - | - | - | 2 GB |
| Medium | 100,000 | 200 | - | - | - | - | 4 GB |
| Large | 250,000 | 500 | - | - | - | - | 8 GB |
| XL | 500,000 | 1000 | - | - | - | - | 16 GB |
| 2XL | 1,000,000 | 2000 | 140 | 0.358 sec | 0.575 sec | 30 GB | 32 GB |
| 4XL | 1,000,000 | 2000 | 270 | 0.186 sec | 0.304 sec | 35 GB | 64 GB |
| 8XL | 1,000,000 | 2000 | 470 | 0.104 sec | 0.166 sec | 38 GB | 128 GB |
| 12XL | 1,000,000 | 2000 | 600 | 0.085 sec | 0.132 sec | 41 GB | 192 GB |
| 16XL | 1,000,000 | 2000 | 670 | 0.081 sec | 0.129 sec | 45 GB | 256 GB |
For 1,000,000 vectors 40 probes results to accuracy of 0.98. Note that exact values may vary depending on the dataset and queries, we recommend to run benchmarks with your own data to get precise results. Use this table as a reference.
</TabPanel>
</Tabs>
<div>
<Image
alt="multi database"
src={{
light: '/docs/img/ai/going-prod/size-to-rps--light.png',
dark: '/docs/img/ai/going-prod/size-to-rps--dark.png',
}}
/>
</div>
<Admonition type="note">
It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.
</Admonition>
## Performance tips
There are various ways to improve your pgvector performance. Here are some tips:
### Pre-warming your database
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 instance size for your workload.
### Finetune 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.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="hnsw"
queryGroup="index-type"
>
<TabPanel id="hnsw" label="HNSW">
<div>
<Image
alt="multi database"
src={{
light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png',
dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png',
}}
/>
</div>
</TabPanel>
<TabPanel id="ivfflat" label="IVFFlat">
<div>
<Image
alt="multi database"
src={{
light: '/docs/img/ai/instance-type/lists-for-1m--light.png',
dark: '/docs/img/ai/instance-type/lists-for-1m--dark.png',
}}
/>
</div>
</TabPanel>
</Tabs>
Check out more tips and the complete step-by-step guide in [Going to Production for AI applications](going-to-prod).
## Benchmark methodology
We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology. A Python test runner is responsible for uploading the data, creating the index, and running the queries. The pgvector engine is implemented using [vecs](https://github.com/supabase/vecs), a Python client for pgvector.
<div>
<Image
alt="multi database"
src={{
light: '/docs/img/ai/instance-type/vecs-benchmark--light.png',
dark: '/docs/img/ai/instance-type/vecs-benchmark--dark.png',
}}
/>
</div>
Each test is run for a minimum of 30-40 minutes. They include a series of experiments executed at different concurrency levels to measure the engine's performance under different load types. The results are then averaged.
As a general recommendation, we suggest using a concurrency level of 5 or more for most workloads and 30 or more for high-load workloads.
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-concepts',
title: 'Concepts',
description: 'Learn about embeddings within AI and vector applications.',
sidebar_label: 'Concepts',
}
---
id: 'ai-concepts'
title: 'Concepts'
description: 'Learn about embeddings within AI and vector applications.'
sidebar_label: 'Concepts'
---
Embeddings are core to many AI and vector applications. This guide covers these concepts. If you prefer to get started right away, see our guide on [Generating Embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
@@ -61,7 +59,3 @@ Why is this useful? Once we have generated embeddings on multiple texts, it is t
## See also
- [Structured and Unstructured embeddings](/docs/guides/ai/structured-unstructured)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-engineering-for-scale',
title: 'Engineering for Scale',
description: 'Building an enterprise-grade vector architecture',
subtitle: 'Building an enterprise-grade vector architecture.',
sidebar_label: 'Engineering for Scale',
}
---
id: 'ai-engineering-for-scale'
title: 'Engineering for Scale'
description: 'Building an enterprise-grade vector architecture'
subtitle: 'Building an enterprise-grade vector architecture.'
sidebar_label: 'Engineering for Scale'
---
Content sources for vectors can be extremely large. As you grow you should run your Vector workloads across several secondary databases (sometimes called "pods"), which allows each collection to scale independently.
@@ -145,7 +143,3 @@ This diagram provides an example architecture that allows you to access the coll
}}
/>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Building ChatGPT plugins',
subtitle: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.',
breadcrumb: 'AI Examples',
}
---
title: 'Building ChatGPT plugins'
subtitle: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.'
breadcrumb: 'AI Examples'
---
ChatGPT recently released [Plugins](https://openai.com/blog/chatgpt-plugins) which help ChatGPT access up-to-date information, run computations, or use third-party services.
If you're building a plugin for ChatGPT, you'll probably want to answer questions from a specific source. We can solve this with “retrieval plugins”, which allow ChatGPT to access information from a database.
@@ -151,6 +148,3 @@ And after ChatGPT receives a response from the plugin it will answer your questi
- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
- ChatGPT Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Adding generative Q&A for your documentation',
subtitle:
'Learn how to build a ChatGPT-style doc search powered using our headless search toolkit.',
breadcrumb: 'AI Examples',
}
---
title: 'Adding generative Q&A for your documentation'
subtitle: 'Learn how to build a ChatGPT-style doc search powered using our headless search toolkit.'
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.
@@ -37,7 +33,7 @@ There are 3 steps to build similarity search inside your documentation:
### Prepare your database
To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](https://supabase.com/dashboard/_/settings).
To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](https://supabase.com/dashboard/project/_/settings).
Now we can use the [Headless Vector Search](https://github.com/supabase/headless-vector-search#set-up) instructions to set up the database:
@@ -119,6 +115,3 @@ const onSubmit = (e: Event) => {
- Read about how we built [ChatGPT for the Supabase Docs](https://supabase.com/blog/chatgpt-supabase-docs).
- Read the pgvector Docs for [Embeddings and vector similarity](/docs/guides/database/extensions/pgvector)
- See how to build something like this from scratch [using Next.js](/docs/guides/ai/examples/nextjs-vector-search).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,14 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Generate image captions using Hugging Face',
description:
'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.',
subtitle:
'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.',
video: 'https://www.youtube.com/v/OgnYxRkxEUw',
tocVideo: 'OgnYxRkxEUw',
}
---
title: 'Generate image captions using Hugging Face'
description: 'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.'
subtitle: 'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.'
video: 'https://www.youtube.com/v/OgnYxRkxEUw'
tocVideo: 'OgnYxRkxEUw'
---
We can combine Hugging Face with [Supabase Storage](https://supabase.com/storage) and [Database Webhooks](https://supabase.com/docs/guides/database/webhooks) to automatically caption for any image we upload to a storage bucket.
@@ -93,7 +89,3 @@ serve(async (req) => {
return new Response('ok')
})
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'examples-image-search-python',
title: 'Image Search with OpenAI CLIP',
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
subtitle: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
}
---
id: 'examples-image-search-python'
title: 'Image Search with OpenAI CLIP'
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.'
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:
@@ -171,7 +169,3 @@ That's it, go ahead and test it out by running `poetry run search` and you will
## Conclusion
With just a couple of lines of Python you are able to implement image search as well as reverse image search using OpenAI's CLIP model and Supabase Vector.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,14 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Vector search with Next.js and OpenAI',
subtitle:
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
breadcrumb: 'AI Examples',
video: 'https://www.youtube.com/v/xmfNUCjszh4',
tocVideo: 'xmfNUCjszh4',
}
---
title: 'Vector search with Next.js and OpenAI'
subtitle: 'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.'
breadcrumb: 'AI Examples'
video: 'https://www.youtube.com/v/xmfNUCjszh4'
tocVideo: 'xmfNUCjszh4'
---
While our [Headless Vector search](/docs/guides/ai/examples/headless-vector-search) provides a toolkit for generative Q&A, in this tutorial we'll go more in-depth, build a custom ChatGPT-like search experience from the ground-up using Next.js. You will:
@@ -543,6 +539,3 @@ Want to learn more about the awesome tech that is powering this?
allowFullScreen
></iframe>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,13 +1,11 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'examples-openai',
title: 'Generating OpenAI GPT3 completions',
description: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.',
subtitle: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.',
video: 'https://www.youtube-nocookie.com/v/29p8kIqyU_Y',
tocVideo: '29p8kIqyU_Y',
}
---
id: 'examples-openai'
title: 'Generating OpenAI GPT3 completions'
description: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.'
subtitle: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.'
video: 'https://www.youtube-nocookie.com/v/29p8kIqyU_Y'
tocVideo: '29p8kIqyU_Y'
---
OpenAI provides a [completions API](https://platform.openai.com/docs/api-reference/completions) that allows you to use their generative GPT models in your own applications.
@@ -37,7 +35,7 @@ We'll design the function to take your user's query (via POST request) and forwa
import OpenAI from 'https://deno.land/x/openai@v4.24.0/mod.ts'
Deno.serve(async (req) => {
const query = await req.json()
const { query } = await req.json()
const apiKey = Deno.env.get('OPENAI_API_KEY')
const openai = new OpenAI({
apiKey: apiKey,
@@ -112,7 +110,3 @@ If you're interesting in learning how to use this to build your own ChatGPT, rea
allowFullScreen
></iframe>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-going-to-prod',
title: 'Going to Production',
description: 'Checklist for going to production with your AI application.',
subtitle: 'Going to production checklist for AI applications.',
sidebar_label: 'Going to Production',
}
---
id: 'ai-going-to-prod'
title: 'Going to Production'
description: 'Checklist for going to production with your AI application.'
subtitle: 'Going to production checklist for AI applications.'
sidebar_label: 'Going to Production'
---
This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
@@ -125,7 +123,3 @@ Or take a look at our [pgvector 0.5.0 performance](https://supabase.com/blog/inc
}}
/>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-google-colab',
title: 'Google Colab',
description: 'Use Google Colab to manage your Supabase Vector store.',
subtitle: 'Use Google Colab to manage your Supabase Vector store.',
sidebar_label: 'Google Colab',
}
---
id: 'ai-google-colab'
title: 'Google Colab'
description: 'Use Google Colab to manage your Supabase Vector store.'
subtitle: 'Use Google Colab to manage your Supabase Vector store.'
sidebar_label: 'Google Colab'
---
<a
className="w-64"
@@ -39,7 +37,7 @@ pip install vecs
## Connect to your database
Find the Postgres pooler connection string for your Supabase project in the [database settings](https://supabase.com/dashboard/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres`
Find the Postgres pooler connection string for your Supabase project in the [database settings](https://supabase.com/dashboard/project/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres`
Create a new code block below the install block (`ctrl+m b`) and add the following code using the Postgres URI you copied above:
@@ -113,7 +111,3 @@ You can lean more about creating indexes in the [Vecs documentation](https://sup
## Resources
- Vecs API: [supabase.github.io/vecs/api](https://supabase.github.io/vecs/api)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-hugging-face',
title: 'Hugging Face Inference API',
description: 'Learn how to integrate hugging face models with Supabase',
sidebar_label: 'Hugging Face',
}
---
id: 'ai-hugging-face'
title: 'Hugging Face Inference API'
description: 'Learn how to integrate hugging face models with Supabase'
sidebar_label: 'Hugging Face'
---
[Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application.
@@ -170,7 +168,3 @@ Try running some other [AI tasks](#ai-tasks).
- Official [Hugging Face site](https://huggingface.co/).
- Official [Hugging Face JS docs](https://huggingface.co/docs/huggingface.js).
- [Generate image captions](/docs/guides/ai/examples/huggingface-image-captioning) using Hugging Face.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,13 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-integration-llamaindex',
title:
'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.',
subtitle:
'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.',
breadcrumb: 'AI Integrations',
}
---
id: 'ai-integration-llamaindex'
title: 'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.'
subtitle: 'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.'
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).
@@ -46,7 +42,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -60,7 +68,3 @@ You can view the inserted items in the [Table Editor](https://supabase.com/dashb
- 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/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-integration-roboflow',
title: 'Roboflow',
subtitle:
'Learn how to integrate Supabase with Roboflow, a tool for running fine-tuned and foundation vision models.',
breadcrumb: 'AI Integrations',
}
---
id: 'ai-integration-roboflow'
title: 'Roboflow'
subtitle: 'Learn how to integrate Supabase with Roboflow, a tool for running fine-tuned and foundation vision models.'
breadcrumb: 'AI Integrations'
---
In this guide, we will walk through two examples of using [Roboflow Inference](https://inference.roboflow.com) to run fine-tuned and foundation models. We will run inference and save predictions using an object detection model and [CLIP](https://github.com/openai/CLIP).
@@ -238,7 +235,3 @@ print(result[0])
- [Roboflow Inference documentation](https://inference.roboflow.com)
- [Roboflow Getting Started guide](https://blog.roboflow.com/getting-started-with-roboflow/)
- [How to Build a Semantic Image Search Engine with Supabase and OpenAI CLIP](https://blog.roboflow.com/how-to-use-semantic-search-supabase-openai-clip/)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,14 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { Tabs } from 'ui'
export const TabPanel = Tabs.Panel
export const meta = {
id: 'ai-lang-chain',
title: 'LangChain',
description:
'Learn how to integrate Supabase with LangChain, a popular framework for composing AI, Vectors, and embeddings',
sidebar_label: 'LangChain',
}
---
id: 'ai-lang-chain'
title: 'LangChain'
description: 'Learn how to integrate Supabase with LangChain, a popular framework for composing AI, Vectors, and embeddings'
sidebar_label: 'LangChain'
---
[LangChain](https://langchain.com/) is a popular framework for working with AI, Vectors, and embeddings. LangChain supports using Supabase as a [vector store](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase), using the `pgvector` extension.
@@ -230,7 +225,3 @@ You can install the LangChain Hybrid Search function though our [database.dev pa
- Official [LangChain site](https://langchain.com/).
- Official [LangChain docs](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase).
- Supabase [Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,18 +1,18 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-python-clients',
title: 'Choosing a Client',
description: 'Learn how to manage vectors using Python',
sidebar_label: 'Choosing a Client',
}
---
id: 'ai-python-clients'
title: 'Choosing a Client'
description: 'Learn how to manage vectors using Python'
sidebar_label: 'Choosing a Client'
---
As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured), AI workloads come in many forms.
For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata.
<Admonition type="tip">
You can get your connection string from the [**Database Settings**](https://supabase.com/dashboard/project/_/settings/database) page in your dashboard. Make sure to check **Use connection pooling**, then copy the URI. Also, change the URI scheme from `postgres` to `postgresql`. `vecs` uses SQLAlchemy under the hood, which only supports `postgresql` as a dialect.
</Admonition>
For production python applications with version controlled migrations, we recommend adding first class vector support to your toolchain by [registering the vector type with your ORM](https://github.com/pgvector/pgvector-python). pgvector provides bindings for the most commonly used SQL drivers/libraries including Django, SQLAlchemy, SQLModel, psycopg, asyncpg and Peewee.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Face similarity search',
subtitle: 'Identify the celebrities who look most similar to you using Supabase Vecs.',
breadcrumb: 'AI Quickstarts',
}
---
id: 'ai-vecs-python-client'
title: 'Face similarity search'
subtitle: 'Identify the celebrities who look most similar to you using Supabase Vecs.'
---
This guide will walk you through a ["Face Similarity Search"](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) example using Colab and Supabase Vecs. You will be able to identify the celebrities who look most similar to you (or any other person). You will:
@@ -43,7 +40,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -56,7 +65,3 @@ You can view the inserted items in the [Table Editor](https://supabase.com/dashb
## Next steps
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-generating-embeddings',
title: 'Generate Embeddings',
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.',
breadcrumb: 'AI Quickstarts',
}
---
id: 'ai-generating-embeddings'
title: 'Generate Embeddings'
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.'
breadcrumb: 'AI Quickstarts'
---
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required.
@@ -205,7 +203,3 @@ Let's build an Edge Function that will accept an input string and generate an em
- Learn more about [embedding concepts](/docs/guides/ai/concepts)
- [Store your embeddings](/docs/guides/ai/vector-columns) in a database
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Creating and managing collections',
subtitle: 'Connecting to your database with Colab.',
breadcrumb: 'AI Quickstarts',
}
---
id: 'ai-vecs-python-client'
title: 'Creating and managing collections'
subtitle: 'Connecting to your database with Colab.'
breadcrumb: 'AI Quickstarts'
---
This guide will walk you through a basic ["Hello World"](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) example using Colab and Supabase Vecs. You'll learn how to:
@@ -43,7 +41,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -56,7 +66,3 @@ You can view the inserted items in the [Table Editor](https://supabase.com/dashb
## Next steps
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
import HuggingFaceDeployment from '~/components/MDX/ai/quickstart_hf_deployment.mdx'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Semantic Text Deduplication',
subtitle: 'Finding duplicate movie reviews with Supabase Vecs.',
breadcrumb: 'AI Quickstarts',
}
---
id: 'ai-vecs-python-client'
title: 'Semantic Text Deduplication'
subtitle: 'Finding duplicate movie reviews with Supabase Vecs.'
breadcrumb: 'AI Quickstarts'
---
This guide will walk you through a ["Semantic Text Deduplication"](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) example using Colab and Supabase Vecs. You'll learn how to find similar movie reviews using embeddings, and remove any that seem like duplicates. You will:
@@ -44,7 +41,19 @@ DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
vx = vecs.create_client(DB_CONNECTION)
```
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
Replace the `DB_CONNECTION` with your own connection string for your database. You can find the Postgres connection string in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) of your Supabase project.
<Admonition type='note'>
SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.
</Admonition>
<Admonition type='note'>
You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.
</Admonition>
## Stepping through the notebook
@@ -59,7 +68,3 @@ You can view the inserted items in the [Table Editor](https://supabase.com/dashb
## Next steps
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-rag-with-permissions',
title: 'RAG with Permissions',
subtitle: 'Fine-grain access control with Retrieval Augmented Generation.',
description: 'Implement fine-grain access control with retrieval augmented generation',
sidebar_label: 'RAG with Permissions',
}
---
id: 'ai-rag-with-permissions'
title: 'RAG with Permissions'
subtitle: 'Fine-grain access control with Retrieval Augmented Generation.'
description: 'Implement fine-grain access control with retrieval augmented generation'
sidebar_label: 'RAG with Permissions'
---
Since pgvector is built on top of Postgres, you can implement fine-grain access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
@@ -272,7 +270,3 @@ order by document_sections.embedding <#> embedding;
There are endless approaches to this problem based on the complexities of each system. Luckily Postgres comes with all the primitives needed to provide access control in the way that works best for your project.
If the examples above didn't fit your use case or you need to adjust them slightly to better fit your existing system, feel free to reach out to [support](https://supabase.com/dashboard/support/new) and we'll be happy to assist you.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,14 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'structured-unstructured-embeddings',
title: 'Structured and Unstructured',
description:
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
subtitle:
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
sidebar_label: 'Structured and unstructured embeddings',
}
---
id: 'structured-unstructured-embeddings'
title: 'Structured and Unstructured'
description: 'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.'
subtitle: 'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.'
sidebar_label: 'Structured and unstructured embeddings'
---
Most vector stores treat metadata associated with embeddings like NoSQL, unstructured data. Supabase is flexible enough to store unstructured and structured metadata.
@@ -108,7 +104,3 @@ Both approaches create a table where you can store your embeddings and some meta
- Unstructured metadata is best when fields are unknown/user-defined or when working with data interactively e.g. exploratory research
Both approaches are valid, and the one you should choose depends on your use-case.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Python client',
subtitle: 'Manage unstructured vector stores in PostgreSQL.',
breadcrumb: 'AI Quickstarts',
}
---
id: 'ai-vecs-python-client'
title: 'Python client'
subtitle: 'Manage unstructured vector stores in PostgreSQL.'
breadcrumb: 'AI Quickstarts'
---
Supabase provides a Python client called [`vecs`](https://github.com/supabase/vecs) for managing unstructured vector stores. This client provides a set of useful tools for creating and querying collections in PostgreSQL using the [pgvector](/docs/guides/database/extensions/pgvector) extension.
@@ -85,7 +82,3 @@ For a more in-depth guide on `vecs` collections, see [API](/docs/guides/ai/pytho
- Official Vecs Documentation: https://supabase.github.io/vecs/api
- Source Code: https://github.com/supabase/vecs
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vector-columns',
title: 'Vector columns',
description: 'Learn how to use vectors within your own Postgres tables',
sidebar_label: 'Vector columns',
}
---
id: 'ai-vector-columns'
title: 'Vector columns'
description: 'Learn how to use vectors within your own Postgres tables'
sidebar_label: 'Vector columns'
---
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
@@ -166,7 +164,3 @@ Vectors and embeddings can be used for much more than search. Learn more about e
### Indexes
Once your vector table starts to grow, you will likely want to add an index to speed up queries. See [Vector indexes](/docs/guides/ai/vector-indexes) to learn how vector indexes work and how to create them.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vector-indexes',
title: 'Vector indexes',
description: 'Understanding vector indexes',
sidebar_label: 'Vector indexes',
}
---
id: 'ai-vector-indexes'
title: 'Vector indexes'
description: 'Understanding vector indexes'
sidebar_label: 'Vector indexes'
---
Once your vector table starts to grow, you will likely want to add an index to speed up queries. Without indexes, you'll be performing a sequential scan which can be a resource-intensive operation when you have many records.
@@ -33,7 +31,3 @@ Currently vectors with up to 2,000 dimensions can be indexed.
## Resources
Read more about indexing on `pgvector`'s [GitHub page](https://github.com/pgvector/pgvector#indexing).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-hnsw-indexes',
title: 'HNSW indexes',
description: 'Understanding HNSW indexes in pgvector',
sidebar_label: 'HNSW indexes',
}
---
id: 'ai-hnsw-indexes'
title: 'HNSW indexes'
description: 'Understanding HNSW indexes in pgvector'
sidebar_label: 'HNSW indexes'
---
HNSW is an algorithm for approximate nearest neighbor search. It is a frequently used index type that can improve performance when querying highly-dimensional vectors, like those representing embeddings.
@@ -92,7 +90,3 @@ Unlike IVFFlat indexes, you are safe to build an HNSW index immediately after th
## Resources
Read more about indexing on `pgvector`'s [GitHub page](https://github.com/pgvector/pgvector#indexing).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-ivf-indexes',
title: 'IVFFlat indexes',
description: 'Understanding IVFFlat indexes in pgvector',
sidebar_label: 'IVFFlat indexes',
}
---
id: 'ai-ivf-indexes'
title: 'IVFFlat indexes'
description: 'Understanding IVFFlat indexes in pgvector'
sidebar_label: 'IVFFlat indexes'
---
IVFFlat is a type of vector index for approximate nearest neighbor search. It is a frequently used index type that can improve performance when querying highly-dimensional vectors, like those representing embeddings.
@@ -96,7 +94,3 @@ One important note with IVF indexes is that nearest neighbor search is approxima
## Resources
Read more about indexing on `pgvector`'s [GitHub page](https://github.com/pgvector/pgvector#indexing).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'api',
title: 'REST API',
description: 'Auto-generating REST API.',
sidebar_label: 'Overview',
video: 'https://www.youtube.com/v/rPAJJFdtPw0',
}
---
id: 'api'
title: 'REST API'
description: 'Auto-generating REST API.'
sidebar_label: 'Overview'
video: 'https://www.youtube.com/v/rPAJJFdtPw0'
---
Supabase auto-generates an API directly from your database schema allowing you to connect to your database through a restful interface, directly from the browser.
@@ -53,7 +51,3 @@ You can find the API URL and Keys in the [Dashboard](https://supabase.com/dashbo
type="video/mp4"
/>
</video>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'api-keys',
title: 'Understanding API Keys',
description: 'Securing your Data API with Postgres Row Level Security.',
}
---
id: 'api-keys'
title: 'Understanding API Keys'
description: 'Securing your Data API with Postgres Row Level Security.'
---
Supabase provides two default keys when you create a project: an `anon` key, and a `service_role` key. You can find both keys in the [API Settings](https://supabase.com/dashboard/project/_/settings/api).
@@ -66,7 +64,3 @@ select
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'creating-routes',
title: 'Creating API Routes',
description:
'API routes are automatically created when you create Postgres Tables, Views, or Functions.',
}
---
id: 'creating-routes'
title: 'Creating API Routes'
description: 'API routes are automatically created when you create Postgres Tables, Views, or Functions.'
---
API routes are automatically created when you create Postgres Tables, Views, or Functions.
@@ -114,7 +111,3 @@ JS Reference: [select()](/docs/reference/javascript/select),
[upsert()](/docs/reference/javascript/upsert),
[delete()](/docs/reference/javascript/delete),
[rpc()](/docs/reference/javascript/rpc) (call Postgres functions).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'data-apis',
title: 'Working with the Data APIs',
subtitle: 'Perform database operations using REST API calls',
}
---
id: 'data-apis'
title: 'Working with the Data APIs'
subtitle: 'Perform database operations using REST API calls'
---
Supabase's auto-generated Data APIs let you perform operations on your database using REST API calls. You can read, write, and delete data, and even call functions, without writing an intermediate layer to translate between your app and Postgres.
@@ -32,7 +30,3 @@ For examples of how to perform common database operations, see the Database sect
- <Link href="/guides/database/debugging-performance" id="debugging-performance-issues">
Debugging performance issues
</Link>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'Build an API route in less than 2 minutes.',
subtitle: 'Create your first API route by creating a table called `todos` to store tasks.',
breadcrumb: 'API Quickstart',
}
---
title: 'Build an API route in less than 2 minutes.'
subtitle: 'Create your first API route by creating a table called `todos` to store tasks.'
breadcrumb: 'API Quickstart'
hideToc: true
---
Let's create our first REST route which we can query using `cURL` or the browser.
@@ -173,7 +171,3 @@ response = supabase.table('todos').select("*").execute()
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auto-docs',
title: 'Auto-generated documentation',
description: 'Supabase provides documentation that updates automatically.',
}
---
id: 'auto-docs'
title: 'Auto-generated documentation'
description: 'Supabase provides documentation that updates automatically.'
---
Supabase generates documentation in the [Dashboard](https://supabase.com/dashboard) which updates as you make database changes.
@@ -18,7 +16,3 @@ Supabase generates documentation in the [Dashboard](https://supabase.com/dashboa
type="video/mp4"
/>
</video>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'client-libs',
title: 'Client Libraries',
description: 'Supabase provides several client libraries for the REST and Realtime APIs.',
video: 'https://www.youtube.com/v/7CqlTU9aOR4',
}
---
id: 'client-libs'
title: 'Client Libraries'
description: 'Supabase provides several client libraries for the REST and Realtime APIs.'
video: 'https://www.youtube.com/v/7CqlTU9aOR4'
---
Supabase provides client libraries for the REST and Realtime APIs. Some libraries are officially supported, and some are contributed by the community.
@@ -27,7 +25,3 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'generating-types',
title: 'Generating TypeScript Types',
description: 'How to generate types for your API and Supabase libraries.',
subtitle: 'How to generate types for your API and Supabase libraries.',
tocVideo: '/7CqlTU9aOR4',
}
---
id: 'generating-types'
title: 'Generating TypeScript Types'
description: 'How to generate types for your API and Supabase libraries.'
subtitle: 'How to generate types for your API and Supabase libraries.'
tocVideo: '/7CqlTU9aOR4'
---
Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions.
@@ -243,7 +241,3 @@ Alternatively, you can use a community-supported GitHub action: [generate-supaba
## Resources
- [Generating Supabase types with GitHub Actions](https://blog.esteetey.dev/how-to-create-and-test-a-github-action-that-generates-types-from-supabase-database)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,349 @@
---
id: 'securing-your-api'
title: 'Securing your API'
description: 'Securing your Data API with Postgres Row Level Security.'
---
The data APIs are designed to work with Postgres Row Level Security (RLS). If you use [Supabase Auth](/docs/guides/auth), you can restrict data based on the logged-in user.
To control access to your data, you can use [Policies](/docs/guides/auth#policies).
## Enabling row level security
Any table you create in the `public` schema will be accessible via the Supabase Data API.
To restrict access, enable Row Level Security (RLS) on all tables, views, and functions in the `public` schema. You can then write RLS policies to grant users access to specific database rows or functions based on their authentication token.
<Admonition type="danger">
Always enable Row Level Security on tables, views, and functions in the `public` schema to protect your data.
</Admonition>
Any table created through the Supabase Dashboard will have RLS enabled by default. If you created the tables via the SQL editor or via another way, enable RLS like so:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="database-method"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Authentication > Policies](/dashboard/project/_/auth/policies) page in the Dashboard.
2. Select **Enable RLS** to enable Row Level Security.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
alter table
todos enable row level security;
```
</TabPanel>
</Tabs>
With RLS enabled, you can create Policies that allow or disallow users to access and update data. We provide a detailed guide for creating Row Level Security Policies in our [Authorization documentation](/docs/guides/auth/row-level-security).
<Admonition type="danger">
Any table **without RLS enabled** in the `public` schema will be accessible to the public, using the `anon` role. Always make sure that RLS is enabled or that you've got other security measures in place to avoid unauthorized access to your project's data!
</Admonition>
## Safeguards towards accidental deletes and updates
By default, all projects have the [safeupdate](https://github.com/eradman/pg-safeupdate) Postgres extension enabled for API queries. This ensures that `delete()` and `update()` requests will fail if there are no filters provided.
To confirm that safeupdate is enabled for API queries, run the following query:
```sql
select
usename,
useconfig
from pg_shadow
where usename = 'authenticator';
```
The expected value for `useconfig` should be:
```javascript
;['session_preload_libraries=supautils, safeupdate']
```
## Enforce additional rules on each request
Using Row Level Security policies may not always be adequate or sufficient to protect APIs.
Here are some common situations where additional protections are necessary:
- Enforcing per-IP or per-user rate limits.
- Checking custom or additional API keys before allowing further access.
- Rejecting requests after exceeding a quota or requiring payment.
- Disallowing direct access to certain tables, views or functions in the `public` schema.
You can build these cases in your application by creating a Postgres function that will read information from the request and perform additional checks, such as counting the number of requests received or checking that an API key is already registered in your database before serving the response.
Define a function like so:
```sql
create function public.check_request()
returns void
language plpgsql
security definer
as $$
begin
-- your logic here
end;
$$;
```
And register it to run on every Data API request using:
```sql
alter role authenticator
set pgrst.db_pre_request = 'public.check_request';
```
This configures the `public.check_request` function to run on every Data API request. To have the changes take effect, you should run:
```sql
notify pgrst, 'reload config';
```
Inside the function you can perform any additional checks on the request headers or JWT and raise an exception to prevent the request from completing. For example, this exception raises a HTTP 402 Payment Required response with a `hint` and additional `X-Powered-By` header:
```sql
raise sqlstate 'PGRST' using
message = json_build_object(
'code', '123',
'message', 'Payment Required',
'details', 'Quota exceeded',
'hint', 'Upgrade your plan')::text,
detail = json_build_object(
'status', 402,
'headers', json_build_object(
'X-Powered-By', 'Nerd Rage'))::text;
```
When raised within the `public.check_request` function, the resulting HTTP response will look like:
```http
HTTP/1.1 402 Payment Required
Content-Type: application/json; charset=utf-8
X-Powered-By: Nerd Rage
{
"message": "Payment Required",
"details": "Quota exceeded",
"hint": "Upgrade your plan",
"code": "123"
}
```
Use the [JSON operator functions](https://www.postgresql.org/docs/current/functions-json.html) to build rich and dynamic responses from exceptions.
If you use a custom HTTP status code like 419, you can supply the `status_text` key in the `detail` clause of the exception to describe the HTTP status.
If you're using PostgREST version 11 or lower ([find out your PostgREST version](/dashboard/project/_/settings/infrastructure)) a different and less powerful [syntax](https://postgrest.org/en/stable/references/errors.html#raise-errors-with-http-status-codes) needs to be used.
### Accessing request information
Like with RLS policies, you can access information about the request by using the `current_setting()` Postgres function. Here are some examples on how this works:
```sql
-- To get all the headers sent in the request
SELECT current_setting('request.headers', true)::json;
-- To get a single header, you can use JSON arrow operators
SELECT current_setting('request.headers', true)::json->>'user-agent';
-- Access Cookies
SELECT current_setting('request.cookies', true)::json;
```
| `current_setting()` | Example | Description |
| ------------------- | ----------------------------------------------- | ------------------------------------ |
| `request.method` | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` | Request's method |
| `request.path` | `table` | Table's path |
| `request.path` | `view` | View's path |
| `request.path` | `rpc/function` | Functions's path |
| `request.headers` | `{ "User-Agent": "...", ... }` | JSON object of the request's headers |
| `request.cookies` | `{ "cookieA": "...", "cookieB": "..." }` | JSON object of the request's cookies |
| `request.jwt` | `{ "sub": "a7194ea3-...", ... }` | JSON object of the JWT payload |
To access the IP address of the client look up the [X-Forwarded-For header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) in the `request.headers` setting. For example:
```sql
SELECT split_part(
current_setting('request.headers', true)::json->>'x-forwarded-for',
',', 1); -- takes the client IP before the first comma (,)
```
Read more about [PostgREST's pre-request function](https://postgrest.org/en/stable/references/transactions.html#pre-request).
### Examples
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="rate-limit-per-ip"
queryGroup="pre-request"
>
<TabPanel id="rate-limit-per-ip" label="Rate limit per IP">
You can only rate-limit `POST`, `PUT`, `PATCH` and `DELETE` requests. This is because `GET` and `HEAD` requests run in read-only mode, and will be served by [Read Replicas](/guides/platform/read-replicas) which do not support writing to the database.
Outline:
- A new row is added to a `private.rate_limits` table each time a modifying action is done to the database containing the IP address and the timestamp of the action.
- If there are over 100 requests from the same IP address in the last 5 minutes, the request is rejected with a HTTP 420 code.
Create the table:
```sql
create table private.rate_limits (
ip inet,
request_at timestamp
);
-- add an index so that lookups are fast
create index rate_limits_ip_request_at_idx on private.rate_limits (ip, request_at desc);
```
The `private` schema is used as it cannot be accessed over the API!
Create the `public.check_request` function:
```sql
create function public.check_request()
returns void
language plpgsql
security definer
as $$
declare
req_method text := current_setting('request.method', true);
req_ip inet := split_part(
current_setting('request.headers', true)::json->>'x-forwarded-for',
',', 1)::inet;
count_in_five_mins integer;
begin
if req_method = 'GET' or req_method = 'HEAD' or req_method is null then
-- rate limiting can't be done on GET and HEAD requests
return;
end if;
select
count(*) into count_in_five_mins
from private.rate_limits
where
ip = req_ip and request_at between now() - interval '5 minutes' and now();
if count_in_five_mins > 100 then
raise sqlstate 'PGRST' using
message = json_build_object(
'message', 'Rate limit exceeded, try again after a while')::text,
detail = json_build_object(
'status', 420,
'status_text', 'Enhance Your Calm')::text;
end if;
insert into private.rate_limits (ip, request_at) values (req_ip, now());
end;
$$;
```
Finally, configure the `public.check_request()` function to run on every Data API request:
```sql
alter role authenticator
set pgrst.db_pre_request = 'public.check_request';
notify pgrst, 'reload config';
```
To clear old entries in the `private.rate_limits` table, set up a [pg_cron](/guides/database/extensions/pg_cron) job to clean them up.
</TabPanel>
<TabPanel id="use-additional-api-key" label="Use additional API keys">
Some applications can benefit from using additional API keys managed by the application **in addition to the [Supabase API keys](/docs/guides/api/api-keys)**. This is commonly necessary in cases like:
- Applications that use the Data API without RLS policies.
- Applications that do not use [Supabase Auth](/auth) or any other authentication system and rely on the `anon` role.
<Admonition type="tip">
Using the `apikey` header with the [Supabase API keys](/docs/guides/api/api-keys) is mandatory and not configurable. If you use additional API keys, you have to distribute both the `anon` API key and your application's custom API key.
</Admonition>
Outline:
- Your application requires the presence of the `x-app-api-key` header when the `anon` role is used to prevent abuse of your API.
- These API keys are stored in the `private.anon_api_keys` table, and are distributed independently.
- Each request using the `anon` role will be blocked with HTTP 403 if the `x-app-api-key` header is not registered in the table.
Set up the table:
```sql
create table private.anon_api_keys (
id uuid primary key,
-- other relevant fields
);
```
Create the `public.check_request` function:
```sql
create function public.check_request()
returns void
language plpgsql
security definer
as $$
declare
req_app_api_key text := current_setting('request.headers', true)::json->>'x-app-api-key';
is_app_api_key_registered boolean;
begin
if current_role <> 'anon' then
-- not `anon` role, allow the request to pass
return;
end if;
select
true into is_app_api_key_registered
from private.anon_api_keys
where
id = req_app_api_key
limit 1;
if is_app_api_key_registered is true then
-- api key is registered, allow the request to pass
return;
end if;
raise sqlstate 'PGRST' using
message = json_build_object(
'message', 'No registered API key found in x-app-api-key header.')::text,
detail = json_build_object(
'status', 403)::text;
end;
$$;
```
Finally, configure the `public.check_request()` function to run on every Data API request:
```sql
alter role authenticator
set pgrst.db_pre_request = 'public.check_request';
notify pgrst, 'reload config';
```
</TabPanel>
</Tabs>
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'using-custom-schemas',
title: 'Using Custom Schemas',
description: 'You need additional steps to use custom database schemas with data APIs.',
}
---
id: 'using-custom-schemas'
title: 'Using Custom Schemas'
description: 'You need additional steps to use custom database schemas with data APIs.'
---
By default, your database has a `public` schema which is automatically exposed on data APIs. You can also expose custom database schemas - to do so you need to follow these steps:
@@ -56,7 +54,3 @@ curl '<SUPABASE_URL>/rest/v1/todos' \
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,13 +1,10 @@
import Layout from '~/layouts/DefaultGuideLayout'
import AuthProviders from '~/components/AuthProviders'
export const meta = {
id: 'auth',
title: 'Auth',
description: 'Use Supabase to Authenticate and Authorize your users.',
subtitle: 'Use Supabase to authenticate and authorize your users.',
video: 'https://www.youtube.com/v/6ow_jW4epf8',
}
---
id: 'auth'
title: 'Auth'
description: 'Use Supabase to Authenticate and Authorize your users.'
subtitle: 'Use Supabase to authenticate and authorize your users.'
video: 'https://www.youtube.com/v/6ow_jW4epf8'
---
There are two parts to every Auth system:
@@ -149,7 +146,3 @@ When users sign up, Supabase assigns them a unique ID. You can reference this ID
type="video/mp4"
/>
</video>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-captcha',
title: 'Enable Captcha Protection',
description: 'Add Captcha Protection to your Supabase project',
video: 'https://www.youtube.com/v/em1cpOAXknM',
}
---
id: 'auth-captcha'
title: 'Enable Captcha Protection'
description: 'Add Captcha Protection to your Supabase project'
video: 'https://www.youtube.com/v/em1cpOAXknM'
---
Supabase provides you with the option of adding captcha to your sign-in, sign-up, and password reset forms. This keeps your website safe from bots and malicious scripts. Supabase authentication has support for [hCaptcha](https://www.hcaptcha.com/) and [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/).
@@ -52,11 +50,9 @@ 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 the **Security and Protection** section.
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.
![supabase_auth_general_settings.png](/docs/img/guides/auth-captcha/supabase_auth_general_settings.png)
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
@@ -101,7 +97,9 @@ We will pass it the sitekey we copied from the hCaptcha website as a property al
```jsx
<HCaptcha
sitekey="your-sitekey"
onVerify={(token) => { setCaptchaToken(token) }}
onVerify={(token) => {
setCaptchaToken(token)
}}
/>
```
@@ -147,7 +145,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. This example uses React and the Turnstile React component, but Turnstile can be used with any JavaScript framework.
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.
@@ -169,7 +167,7 @@ const [captchaToken, setCaptchaToken] = useState()
Now lets add the Cloudflare Turnstile component to the JSX section of our code:
```html
```jsx
<Turnstile />
```
@@ -178,11 +176,13 @@ We will pass it the sitekey we copied from the Cloudflare website as a property
```jsx
<Turnstile
siteKey="your-sitekey"
onSuccess={(token) => { setCaptchaToken(token) }}
onSuccess={(token) => {
setCaptchaToken(token)
}}
/>
```
We can now use the captchaToken we receive in our Supabase `signUp` function.
We can now use the `captchaToken` we receive in our Supabase `signUp` function.
```jsx
await supabase.auth.signUp({
@@ -198,7 +198,3 @@ To test locally, you will need to add localhost to the domain whitelist as per t
</Tabs>
Run the application and you should now be provided with a captcha challenge.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-deep-dive-jwts',
title: 'Part One: JWTs',
description: 'Supabase Auth Deep Dive Part 1 - JWTs',
video: 'https://www.youtube.com/v/v3Exg5YpJvE',
}
---
id: 'auth-deep-dive-jwts'
title: 'Part One: JWTs'
description: 'Supabase Auth Deep Dive Part 1 - JWTs'
video: 'https://www.youtube.com/v/v3Exg5YpJvE'
---
### About
@@ -192,7 +190,3 @@ Now that you understand what JWTs are and where they're used in Supabase, you ca
- [Part Four: GoTrue](../../learn/auth-deep-dive/auth-gotrue)
- [Part Five: Google Oauth](../../learn/auth-deep-dive/auth-google-oauth)
- Sign up for Supabase: [supabase.com/dashboard](https://supabase.com/dashboard)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-google-oauth',
title: 'Part Five: Google OAuth',
description: 'Supabase Deep Dive Part 5: Google OAuth Provider',
video: 'https://www.youtube.com/v/_XM9ziOzWk4',
}
---
id: 'auth-google-oauth'
title: 'Part Five: Google OAuth'
description: 'Supabase Deep Dive Part 5: Google OAuth Provider'
video: 'https://www.youtube.com/v/_XM9ziOzWk4'
---
### About
@@ -105,7 +103,3 @@ For any support please get in touch at beta at [supabase.com](https://supabase.c
- Watch [Part Four: GoTrue](../../learn/auth-deep-dive/auth-gotrue)
{/* <!-- - Watch [Part Five: Google Oauth](../../learn/auth-deep-dive/auth-google-oauth) --> */}
- Sign up for Supabase: [supabase.com/dashboard](https://supabase.com/dashboard)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-gotrue',
title: 'Part Four: GoTrue',
description: 'Supabase Deep Dive Part 4: GoTrue Overview',
}
---
id: 'auth-gotrue'
title: 'Part Four: GoTrue'
description: 'Supabase Deep Dive Part 4: GoTrue Overview'
---
### About
@@ -79,7 +77,3 @@ In the next guide we'll be looking at how to setup external OAuth providers: Wat
{/* <!-- Watch [Part Four: GoTrue](../../learn/auth-deep-dive/auth-gotrue) --> */}
- Watch [Part Five: Google OAuth](../../learn/auth-deep-dive/auth-google-oauth)
- Sign up for Supabase: [supabase.com/dashboard](https://supabase.com/dashboard)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-policies',
title: 'Part Three: Policies',
description: 'Supabase Auth Deep Dive Part 3: User Based Access Policies',
video: 'https://www.youtube.com/v/0LvCOlELs5U',
}
---
id: 'auth-policies'
title: 'Part Three: Policies'
description: 'Supabase Auth Deep Dive Part 3: User Based Access Policies'
video: 'https://www.youtube.com/v/0LvCOlELs5U'
---
### About
@@ -186,7 +184,3 @@ You can get as creative as you like with these policies.
- Watch [Part Four: GoTrue](../../learn/auth-deep-dive/auth-gotrue)
- Watch [Part Five: Google Oauth](../../learn/auth-deep-dive/auth-google-oauth)
- Sign up for Supabase: [supabase.com/dashboard](https://supabase.com/dashboard)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-row-level-security',
title: 'Part Two: Row Level Security',
description: 'Supabase Auth Deep Dive Part Two - Row Level Security',
video: 'https://www.youtube.com/v/qY_iQ10IUhs',
}
---
id: 'auth-row-level-security'
title: 'Part Two: Row Level Security'
description: 'Supabase Auth Deep Dive Part Two - Row Level Security'
video: 'https://www.youtube.com/v/qY_iQ10IUhs'
---
### About
@@ -110,6 +108,12 @@ If you'd prefer to use the dashboard to add your policy you can do so by clickin
![Add a read only policy in Supabase](/docs/img/auth-deep-dive-2-2.png)
<Admonition type="caution">
Please note that comments are not supported when using RLS policies via the dashboard.
</Admonition>
You should now be able to read from your leaderboard, but will still not be able to write, update, or delete from it, which is exactly what we wanted!
A quick reminder that you can always use your `service_role` API key to bypass these row level security policies. But be extra careful that you don't leak this key by including it in the client. This can be useful if you're building internal admin tools, or if you need to bulk insert or delete data via the API.
@@ -129,7 +133,3 @@ In the next guide we will look at using Policies in combination with User Accoun
- Watch [Part Four: GoTrue](../../learn/auth-deep-dive/auth-gotrue)
- Watch [Part Five: Google Oauth](../../learn/auth-deep-dive/auth-google-oauth)
- Sign up for Supabase: [supabase.com/dashboard](https://supabase.com/dashboard)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Email Templates',
description: 'Learn how to manage the email templates in Supabase.',
subtitle: 'Learn how to manage the email templates in Supabase.',
}
---
title: 'Email Templates'
description: 'Learn how to manage the email templates in Supabase.'
subtitle: 'Learn how to manage the email templates in Supabase.'
---
You can customize the email messages used for the authentication flows. You can edit the following email templates:
@@ -18,13 +16,14 @@ You can customize the email messages used for the authentication flows. You can
The templating system provides the following variables for use:
| Name | Description |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . |
| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . |
| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. |
| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](/dashboard/project/_/auth/url-configuration). |
| `{{ .RedirectTo }}` | Contains the redirect URL passed when `signUp`, `signInWithOtp`, `signInWithOAuth`, `resetPasswordForEmail` or `inviteUserByEmail` is called. The redirect URL allow list can be configured in your project's [authentication settings](/dashboard/project/_/auth/url-configuration). |
| Name | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . |
| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . |
| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. |
| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](/dashboard/project/_/auth/url-configuration). |
| `{{ .RedirectTo }}` | Contains the redirect URL passed when `signUp`, `signInWithOtp`, `signInWithOAuth`, `resetPasswordForEmail` or `inviteUserByEmail` is called. The redirect URL allow list can be configured in your project's [authentication settings](/dashboard/project/_/auth/url-configuration). |
| `{{ .Data }}` | Contains metadata from `auth.users.user_metadata`. Use this to personalize the email message. |
## Limitations
@@ -58,21 +57,46 @@ If you intend to use [Server-side rendering](/docs/guides/auth/server-side-rende
You can customize the email link in the email template to redirect the user to a server-side endpoint successfully. For example:
```html
<a href="https://api.example.com/v1/authenticate?token_hash={{ .TokenHash }}&type=invite&redirect_to={{ .RedirectTo }}"
>Accept the invite
</a>
<a
href="https://api.example.com/v1/authenticate?token_hash={{ .TokenHash }}&type=invite&redirect_to={{ .RedirectTo }}"
>Accept the invite
</a>
```
When the user clicks on the link, the request will hit `https://api.example.com/v1/authenticate` and you can grab the `token_hash`, `type` and `redirect_to` query parameters from the URL. Then, you can call the [`verifyOtp`](/docs/reference/javascript/auth-verifyotp) method to get back an authenticated session before redirecting the user back to the client. Since the `verifyOtp` method makes a `POST` request to Supabase Auth to verify the user, the session will be returned in the response body, which can be read by the server. For example:
```js
const { token_hash, type } = Object.fromEntries(new URLSearchParams(window.location.search))
const { data: { session }, error } = await supabase.auth.verifyOtp({ token_hash, type })
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({ token_hash, type })
// subsequently redirect the user back to the client using the redirect_to param
// ...
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
## Customization
export default Page
Supabase Auth makes use of [Go Templates](https://pkg.go.dev/text/template). This means it is possible to conditionally render information based on template properties.You may wish to checkout this [guide by Hugo](https://gohugo.io/templates/introduction/) for a guide on the templating language.
### Send different email to early access users
Send a different email to users who signed up via an early access domain (`https://www.earlyaccess.trial.com`).
```
{{ if eq .Data.Domain "https://www.example.com" }}
<h1>Welcome to Our Database Service!</h1>
<p>Dear Developer,</p>
<p>Welcome to Billy, the scalable developer platform!</p>
<p>Best Regards,<br>
Billy Team</p>
{{ else if eq .Data.Domain "https://www.earlyaccess.trial.com" }}
<h1>Welcome to Our Database Service!</h1>
<p>Dear Developer,</p>
<p>Welcome Billy, the scalable developer platform!</p>
<p> As an early access member, you have access to select features like Point To Space Restoration.</p>
<p>Best Regards,<br>
Billy Team</p>
{{ end }}
```
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-email',
title: 'Login With Email',
description: 'Use Supabase to Authenticate and Authorize your users using email.',
}
---
id: 'auth-email'
title: 'Login With Email'
description: 'Use Supabase to Authenticate and Authorize your users using email.'
---
Set up Email Logins for your Supabase application.
@@ -20,7 +18,8 @@ See the [self-hosting docs](/docs/guides/hosting/overview#configuration) for det
</Admonition>
## Sign up the user
## Sign up the user
<Tabs
scrollable
size="small"
@@ -38,8 +37,8 @@ async function signUpNewUser() {
email: 'example@email.com',
password: 'example-password',
options: {
emailRedirectTo: 'https://example.com/welcome'
}
emailRedirectTo: 'https://example.com/welcome',
},
})
}
```
@@ -92,7 +91,7 @@ When your user signs in, call [signInWithPassword()](/docs/reference/javascript/
async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password'
password: 'example-password',
})
}
```
@@ -161,7 +160,6 @@ Future<void> signOut() async {
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
When your user signs out, call [logout()](/docs/reference/kotlin/auth-signout) to remove them from the browser session and any objects from localStorage:
```kotlin
@@ -179,7 +177,3 @@ suspend fun logout() {
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'index',
title: 'Auth Helpers',
description: 'Server-Side Auth guides and utilities for working with Supabase.',
sidebar_label: 'Overview',
}
---
id: 'index'
title: 'Auth Helpers'
description: 'Server-Side Auth guides and utilities for working with Supabase.'
sidebar_label: 'Overview'
---
<Admonition type="caution">
@@ -52,7 +50,3 @@ The Auth Helpers are in `beta`. They are usable in their current state, but it's
- [Source code](https://github.com/supabase/auth-helpers)
- [Known bugs and issues](https://github.com/supabase/auth-helpers/issues)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-ui',
title: 'Auth UI',
description: 'A prebuilt, customizable React component for authenticating users.',
}
---
id: 'auth-ui'
title: 'Auth UI'
description: 'A prebuilt, customizable React component for authenticating users.'
---
Auth UI is a pre-built React component for authenticating users.
It supports custom themes and extensible styles to match your brand and aesthetic.
@@ -472,7 +470,3 @@ Setting `showLinks` to `false` will hide the following links:
- Already have an account? Sign in
- Send a magic link email
- Forgot your password?
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,10 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'flutter-auth-ui',
title: 'Flutter Auth UI',
description: 'Prebuilt, customizable Flutter widgets for authenticating users.',
}
---
id: 'flutter-auth-ui'
title: 'Flutter Auth UI'
description: 'Prebuilt, customizable Flutter widgets for authenticating users.'
---
Flutter Auth UI is a Flutter package containing pre-built widgets for authenticating users.
It is unstyled and can match your brand and aesthetic.
@@ -124,7 +122,3 @@ SupaSocialsAuth(
### Theming
This package uses plain Flutter components allowing you to control the appearance of the components using your own theme.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,16 +1,13 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs-pages',
title: 'Supabase Auth with Next.js Pages Directory',
description:
'Authentication helpers for Next.js API routes, middleware, and SSR in the Pages Directory.',
sidebar_label: 'Next.js (pages)',
}
---
id: 'nextjs-pages'
title: 'Supabase Auth with Next.js Pages Directory'
description: 'Authentication helpers for Next.js API routes, middleware, and SSR in the Pages Directory.'
sidebar_label: 'Next.js (pages)'
---
<Admonition type="caution">
We generally recommend using the new `@supabase/ssr` package instead of `auth-helpers`. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server language or framework. Check out the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more.
The `auth-helpers` package has been replaced with the `@supabase/ssr` package. We recommend setting up Auth for your Next.js app with `@supabase/ssr` instead. See the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs?router=pages) to learn how.
</Admonition>
@@ -917,7 +914,3 @@ import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>()
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,16 +1,13 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs',
title: 'Supabase Auth with the Next.js App Router',
description:
'Authentication and Authorization helpers for creating an authenticated Supabase client with the Next.js 13 App Router.',
sidebar_label: 'Next.js',
}
---
id: 'nextjs'
title: 'Supabase Auth with the Next.js App Router'
description: 'Authentication and Authorization helpers for creating an authenticated Supabase client with the Next.js 13 App Router.'
sidebar_label: 'Next.js'
---
<Admonition type="caution">
We generally recommend using the new `@supabase/ssr` package instead of `auth-helpers`. `@supabase/ssr` takes the core concepts of the Auth Helpers package and makes them available to any server framework. Check out the [migration doc](/docs/guides/auth/server-side/migrating-to-ssr-from-auth-helpers) to learn more.
The `auth-helpers` package has been replaced with the `@supabase/ssr` package. We recommend setting up Auth for your Next.js app with `@supabase/ssr` instead. See the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs) to learn how.
</Admonition>
@@ -1315,7 +1312,3 @@ export default function() {
```
For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'remix',
title: 'Supabase Auth with Remix',
description: 'Authentication helpers for loaders and actions in Remix.',
sidebar_label: 'Remix',
}
---
id: 'remix'
title: 'Supabase Auth with Remix'
description: 'Authentication helpers for loaders and actions in Remix.'
sidebar_label: 'Remix'
---
<Admonition type="caution">
@@ -784,7 +782,3 @@ supabaseClient.auth.signUp({
},
})
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'sveltekit',
title: 'Supabase Auth with SvelteKit',
description: 'Convenience helpers for implementing user authentication in SvelteKit.',
sidebar_label: 'SvelteKit',
}
---
id: 'sveltekit'
title: 'Supabase Auth with SvelteKit'
description: 'Convenience helpers for implementing user authentication in SvelteKit.'
sidebar_label: 'SvelteKit'
---
<Admonition type="caution">
@@ -1968,7 +1966,3 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
- [SvelteKit example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,12 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { CodeBlock, Admonition, Accordion } from 'ui'
import Image from 'next/image'
export const meta = {
id: 'auth-hooks',
title: 'Auth Hooks',
description: 'Use Supabase Postgres Functions to customize your authentication flow',
}
---
id: 'auth-hooks'
title: 'Auth Hooks'
description: 'Use Supabase Postgres Functions to customize your authentication flow'
---
Supabase allows you to use Postgres functions to alter the default Supabase Auth flow. Developers can use hooks to add custom behavior that's not supported natively.
@@ -35,12 +31,14 @@ A hook is a [Postgres Function](https://www.postgresql.org/docs/current/sql-crea
This function is invoked by Supabase Auth at its point in the flow by providing an event object. The object returned by the function gives instructions on how Supabase Auth should continue processing.
To access properties of the `event` argument, you can use the [JSON operators and functions](https://www.postgresql.org/docs/current/functions-json.html).
To access properties of the `event` argument, you can use the [JSON operators and functions](https://www.postgresql.org/docs/current/functions-json.html).
There are no restrictions as to what language can be used to write Auth Hooks. If [PL/pgSQL](https://www.postgresql.org/docs/current/plpgsql.html) is too difficult consider using the [plv8](/docs/guides/database/extensions/plv8) extension which lets you use JavaScript to define functions.
<Admonition type="caution">
If you're using the Supabase SQL Editor, there's an issue when using the `?` (*Does the string exist as a top-level key within the JSON value?*) operator. Use a direct connection to the database if you need to use it when defining a function.
If you're using the Supabase SQL Editor, there's an issue when using the `?` (*Does the string
exist as a top-level key within the JSON value?*) operator. Use a direct connection to the
database if you need to use it when defining a function.
</Admonition>
Here is an example hook signature:
@@ -73,6 +71,12 @@ grant execute
to supabase_auth_admin;
```
You also need to grant usage to `supabase_auth_admin`:
```sql
grant usage on schema public to supabase_auth_admin;
```
Also revoke permissions from the `authenticated` and `anon` roles to ensure the function is not accessible by Supabase Serverless APIs.
```sql
@@ -414,6 +418,7 @@ Next, store the API key of our email API provider:
```sql
select vault.create_secret('my_api_key', 'my_api_key_name', 'description_of_my_api_key');
```
Create the hook:
```sql
@@ -567,20 +572,20 @@ as $$
is_admin boolean;
begin
-- Check if the user is marked as admin in the profiles table
select is_admin into is_admin from profiles where user_id = event->>'user_id';
select is_admin into is_admin from profiles where user_id = event->>'user_id'::uuid;
-- Proceed only if the user is an admin
if is_admin then
claims := event->'claims';
-- Check if 'user_metadata' exists in claims
if jsonb_typeof(claims->'user_metadata') is null then
-- If 'user_metadata' does not exist, create an empty object
claims := jsonb_set(claims, '{user_metadata}', '{}');
-- Check if 'app_metadata' exists in claims
if jsonb_typeof(claims->'app_metadata') is null then
-- If 'app_metadata' does not exist, create an empty object
claims := jsonb_set(claims, '{app_metadata}', '{}');
end if;
-- Set a claim of 'admin'
claims := jsonb_set(claims, '{user_metadata, admin}', 'true');
claims := jsonb_set(claims, '{app_metadata, admin}', 'true');
-- Update the 'claims' object in the original event
event := jsonb_set(event, '{claims}', claims);
@@ -598,6 +603,14 @@ grant execute
revoke execute
on function public.custom_access_token_hook
from authenticated, anon;
grant all
on table public.profiles
to supabase_auth_admin;
revoke all
on table public.profiles
from authenticated, anon;
```
</TabPanel>
@@ -642,6 +655,14 @@ as $$
return event;
$$;
grant all
on table public.profiles
to supabase_auth_admin;
revoke all
on table public.profiles
from authenticated, anon;
```
</TabPanel>
@@ -691,5 +712,24 @@ revoke execute
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
## Local Development
Unlike the hosted platform, the local development dashboard does not have a user interface for linking a Hook. Instead, edit `config.toml` to link an Auth Hook and run it with your local setup. Modify the `auth.hook.<hook_name>` field and set `uri` to a value of `pg-functions://postgres/<schema>/<function_name>`
For instance, the `config.toml` for a Custom Access Token Hook which uses the function `public.custom_access_token_hook`:
```
...
[auth.hook.custom_access_token]
enabled = true
uri = "pg-functions://postgres/public/custom_access_token_hook"
...
```
Save your Auth Hook as a migration in order to version the Auth Hook and share it with other team members. Run [`supabase migration new`](https://supabase.com/docs/reference/cli/supabase-migration-new) to create a migration.
### Debugging Your Hook
Test the Postgres Function in isolation before enabling the hook by connecting to your database with `psql` and running `select <your_function_name>`. Where necessary, use [`RAISE` statements](https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html) to print information to the terminal.
@@ -1,11 +1,9 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-identity-linking',
title: 'Identity Linking',
description: 'Manage the identities associated with your user',
subtitle: 'Manage the identities associated with your user',
}
---
id: 'auth-identity-linking'
title: 'Identity Linking'
description: 'Manage the identities associated with your user'
subtitle: 'Manage the identities associated with your user'
---
## The user identity
@@ -20,9 +18,9 @@ The user identity object contains the following attributes:
| identity_data | `object` | The identity metadata. |
| identity_id | `string` | The unique id of the identity. |
| provider | `string` | The provider name. |
| created_at | `string` | The timestamp that the identity was created. |
| last_sign_in_at | `string` | The timestamp that the identity was last used to sign in. |
| updated_at | `string` | The timestamp that the identity was last updated. |
| created_at | `string` | The timestamp that the identity was created. |
| last_sign_in_at | `string` | The timestamp that the identity was last used to sign in. |
| updated_at | `string` | The timestamp that the identity was last updated. |
## Identity linking strategies
@@ -64,6 +62,7 @@ Supabase Auth allows a user to initiate identity linking with a different email
```kotlin
supabase.auth.linkIdentity(Google)
```
</TabPanel>
</Tabs>
@@ -110,9 +109,6 @@ val googleIdentity = identities.first { it.provider == "google" }
//unlink the google identity from the user
supabase.auth.unlinkIdentity(googleIdentity.identityId!!)
```
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,13 +1,8 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
export const meta = {
id: 'auth-mfa',
title: 'Multi-Factor Authentication',
description:
'Add an additional layer of security to your apps with Supabase Auth multi-factor authentication.',
}
---
id: 'auth-mfa'
title: 'Multi-Factor Authentication'
description: 'Add an additional layer of security to your apps with Supabase Auth multi-factor authentication.'
---
Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), adds an additional layer of security to your application by verifying their identity through additional verification steps.
@@ -117,7 +112,6 @@ Enrolling a factor for use with MFA takes three steps:
immediately becomes active for the user account. If not, you should repeat
steps 2 and 3.
#### Example: React
Below is an example that creates a new `EnrollMFA` component that illustrates the important pieces of the MFA enrollment flow.
@@ -223,7 +217,6 @@ An unenrollment flow provides a UI for users to manage and unenroll factors link
When a user unenrolls a factor, call `supabase.auth.mfa.unenroll()` with the ID of the factor. For example, call `supabase.auth.mfa.unenroll({factorId: "d30fd651-184e-4748-a928-0a4b9be1d429"})` to unenroll a factor with ID `d30fd651-184e-4748-a928-0a4b9be1d429`.
#### Example: React
Below is an example that creates a new `UnenrollMFA` component that illustrates the important pieces of the MFA enrollment flow. Note that users can only unenroll a factor after completing the enrollment flow and obtaining an `aal2` JWT claim. Here are some points of note:
@@ -266,26 +259,22 @@ export function UnenrollMFA() {
<>
{error && <div className="error">{error}</div>}
<tbody>
<tr>
<td>Factor ID</td>
<td>Friendly Name</td>
<td>Factor Status</td>
</tr>
{factors.map(factor => (
<tr>
<td>{factor.id}</td>
<td>{factor.friendly_name}</td>
<td>{factor.factor_type}</td>
<td>{factor.status}</td>
<td>Factor ID</td>
<td>Friendly Name</td>
<td>Factor Status</td>
</tr>
))}
{factors.map((factor) => (
<tr>
<td>{factor.id}</td>
<td>{factor.friendly_name}</td>
<td>{factor.factor_type}</td>
<td>{factor.status}</td>
</tr>
))}
</tbody>
<input
type="text"
value={verifyCode}
onChange={(e) => setFactorId(e.target.value.trim())}
/>
<button onClick={()=> supabase.auth.mfa.unenroll({factorId})}>Unenroll</button>
<input type="text" value={verifyCode} onChange={(e) => setFactorId(e.target.value.trim())} />
<button onClick={() => supabase.auth.mfa.unenroll({ factorId })}>Unenroll</button>
</>
)
}
@@ -581,6 +570,10 @@ TOTP is not going to be the only MFA factor Supabase Auth is going to support in
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially introduced by Google Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) but is now universally accepted by all authenticator apps.
### How long is the TOTP code valid for?
In our TOTP implementation, each generated code remains valid for one interval, which spans 30 seconds. To account for minor time discrepancies, we allow for a one-interval clock skew. This ensures that users can successfully authenticate within this timeframe, even if there are slight variations in system clocks.
### How do I check _when_ a user went through MFA?
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods Reference) claim. It is an array of objects that indicate what authentication methods the user has used so far.
@@ -629,13 +622,10 @@ Currently recognized authentication methods are:
- `sso/saml` - any Single Sign On (SAML) method.
The following additional claims are available when using PKCE flow:
- `invite` - any sign in via an invitation.
- `magiclink` - any sign in via magic link. Excludes logins resulting from invocation of `signUp`.
- `email/signup` - any login resulting from an email signup.
- `email_change` - any login resulting from a change in email.
More authentication methods will be added over time as we increase the number of authentication methods supported by Supabase.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
Loaded 100 of 1431 files, more files were not shown because too many files have changed in this diff. Show more