Merge branch 'master' into chore/add-build-check-to-ci

This commit is contained in:
Ziinc authored and GitHub committed 2023-06-21 02:05:49 +08:00
commit 836d66c607
1101 files changed
+134394 -131633

No files matched your search

+7 -5
View File
@@ -14,11 +14,13 @@ jobs:
uses: reviewdog/action-misspell@v1
with:
github_token: ${{ secrets.github_token }}
locale: "US"
locale: 'US'
reporter: github-pr-review
level: error
exclude: |
"*.css"
"**/package.json"
"**/package-lock.json"
".git/*"
*.css
**/package.json
**/package-lock.json
./.git/*
*.ipynb
./i18n/README.*.md
+30
View File
@@ -0,0 +1,30 @@
name: Docs Tests
on:
pull_request:
branches: ['master']
paths:
- 'apps/docs/**/*.ts*'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm run test:docs
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v1
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.PROD_AWS_ROLE }}
aws-region: us-east-1
+4 -1
View File
@@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
cmd:
- npm run test:studio
- npm run build:studio
@@ -36,5 +36,8 @@ jobs:
run: npm ci
working-directory: ./
- name: Run ${{ matrix.cmd }}
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: '--max_old_space_size=3072'
run: ${{ matrix.cmd }}
working-directory: ./
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- uses: actions/checkout@v3
+7
View File
@@ -122,5 +122,12 @@ typings/
apps/new-docs/*
# UI tokens
packages/ui/tokens/**/*.json
# For self-hosted logs: https://github.com/supabase/supabase/blob/86e3ab20abfdb9c3e666334d3d2f8efeef9ccf2c/docker/docker-compose-logging.yml#L101
gcloud.json
# sitemaps
apps/www/public/*.xml
apps/docs/public/*.xml
+1 -1
View File
@@ -1 +1 @@
^./i18n
^./i18n
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1
View File
@@ -0,0 +1 @@
18
+10 -11
View File
@@ -17,18 +17,17 @@
## Getting started
Thanks for your interest in [Supabase](https://supabase.com) and for wanting to contribute! Before you begin, read the
[code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) and check out the
[existing issues](https://github.com/supabase/supabase/issues).
This document describes how to set up your development environment to build and test [Supabase](https://supabase.com).
Thank you for expressing your interest in [Supabase](https://supabase.com) and your willingness to contribute!
To ensure a positive and inclusive environment, we kindly request you to read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md). Additionally, we encourage you to explore the existing [issues](https://github.com/supabase/supabase/issues) to see how you can make a meaningful impact. This document will guide you through the process of setting up your development environment, enabling you to successfully build and test [Supabase](https://supabase.com).
### Install dependencies
You need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](http://git-scm.com/)
- [Node.js v16.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 8.x.x or [Yarn](https://yarnpkg.com/)
- [Node.js v18.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 9.x.x
## Local development
@@ -48,7 +47,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
git clone https://github.com/<github_username>/supabase.git
```
1. Go to the Supabase directory:
2. Go to the Supabase directory:
```sh
cd supabase
```
@@ -63,7 +62,7 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
npm install # install dependencies
```
2. You can then run the apps simultaneously with the following.
2. After that you can run the apps simultaneously with the following.
```sh
npm run dev # start all the applications
```
@@ -121,7 +120,7 @@ Now when you run a local development docs server you will see the new docs site.
After making your changes, open a pull request (PR). Once you submit your pull request, others from the Supabase team/community will review it with you.
Did you have an issue, like a merge conflict, or don't know how to open a pull request? Check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
If you have an issue, like a merge conflict, or don't know how to open a pull request then check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
---
@@ -135,7 +134,7 @@ Create a new entry in the [`redirects.js`](https://github.com/supabase/supabase/
## Community channels
Stuck somewhere? Have any questions? Join the [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
If you are stuck somewhere or have any questions, join our [Discord Community Server](https://discord.supabase.com/) or the [Github Discussions](https://github.com/supabase/supabase/discussions). We are here to help!
## Contributors
+18 -9
View File
@@ -19,6 +19,7 @@
- [x] Database Functions. [Docs](https://supabase.com/docs/guides/database/functions)
- [x] Edge Functions [Docs](https://supabase.com/docs/guides/functions)
- [x] File Storage. [Docs](https://supabase.com/docs/guides/storage)
- [x] AI + Vector/Embeddings Toolkit. [Docs](https://supabase.com/docs/guides/ai)
- [x] Dashboard
![Supabase Dashboard](https://raw.githubusercontent.com/supabase/supabase/master/apps/www/public/images/github/supabase-dashboard.png)
@@ -79,6 +80,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<th>Client</th>
<th colspan="5">Feature-Clients (bundled in Supabase client)</th>
</tr>
<!-- notranslate -->
<tr>
<th></th>
<th>Supabase</th>
@@ -99,7 +101,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase-community/storage-lang" target="_blank" rel="noopener noreferrer">storage-lang</a></td>
</tr>
END ROW -->
<!-- /notranslate -->
<th colspan="7">⚡️ Official ⚡️</th>
<!-- notranslate -->
<tr>
<td>JavaScript (TypeScript)</td>
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
@@ -118,7 +122,9 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
</tr>
<!-- /notranslate -->
<th colspan="7">💚 Community 💚</th>
<!-- notranslate -->
<tr>
<td>C#</td>
<td><a href="https://github.com/supabase-community/supabase-csharp" target="_blank" rel="noopener noreferrer">supabase-csharp</a></td>
@@ -200,6 +206,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
</tr>
<!-- /notranslate -->
</table>
<!--- Remove this list if you're translating to another language, it's hard to keep updated across multiple files-->
@@ -212,23 +219,28 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
- [Bangla / বাংলা](/i18n/README.bn.md)
- [Bulgarian / Български](/i18n/README.bg.md)
- [Catalan / Català](/i18n/README.ca.md)
- [Czech / čeština](/i18n/README.cs.md)
- [Danish / Dansk](/i18n/README.da.md)
- [Dutch / Nederlands](/i18n/README.nl.md)
- [English](https://github.com/supabase/supabase)
- [Estonian / eesti keel](/i18n/README.et.md)
- [Finnish / Suomalainen](/i18n/README.fi.md)
- [French / Français](/i18n/README.fr.md)
- [German / Deutsch](/i18n/README.de.md)
- [Greek / Ελληνικά](/i18n/README.gr.md)
- [Greek / Ελληνικά](/i18n/README.el.md)
- [Gujarati / ગુજરાતી](/i18n/README.gu.md)
- [Hebrew / עברית](/i18n/README.he.md)
- [Hindi / हिंदी](/i18n/README.hi.md)
- [Hungarian / Magyar](/i18n/README.hu.md)
- [Nepali / नेपाली](/i18n/README.ne.md)
- [Indonesian / Bahasa Indonesia](/i18n/README.id.md)
- [Italian / Italiano](/i18n/README.it.md)
- [Japanese / 日本語](/i18n/README.jp.md)
- [Italiano / Italian](/i18n/README.it.md)
- [Japanese / 日本語](/i18n/README.ja.md)
- [Korean / 한국어](/i18n/README.ko.md)
- [Lithuanian / lietuvių](/i18n/README.lt.md)
- [Latvian / latviski](/i18n/README.lv.md)
- [Malay / Bahasa Malaysia](/i18n/README.ms.md)
- [Norwegian (Bokmål) / Norsk (Bokmål)](/i18n/README.nb-no.md)
- [Norwegian (Bokmål) / Norsk (Bokmål)](/i18n/README.nb.md)
- [Persian / فارسی](/i18n/README.fa.md)
- [Polish / Polski](/i18n/README.pl.md)
- [Portuguese / Português](/i18n/README.pt.md)
@@ -237,6 +249,8 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
- [Russian / Pусский](/i18n/README.ru.md)
- [Serbian / Srpski](/i18n/README.sr.md)
- [Sinhala / සිංහල](/i18n/README.si.md)
- [Slovak / slovenský](/i18n/README.sk.md)
- [Slovenian / Slovenščina](/i18n/README.sl.md)
- [Spanish / Español](/i18n/README.es.md)
- [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md)
- [Swedish / Svenska](/i18n/README.sv.md)
@@ -247,8 +261,3 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
- [Vietnamese / Tiếng Việt](/i18n/README.vi-vn.md)
- [List of translations](/i18n/languages.md) <!--- Keep only this -->
---
## Sponsors
[![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase)
+1
View File
@@ -14,6 +14,7 @@
.env.test.local
.env.staging.local
.env.production.local
*.swp
npm-debug.log*
yarn-debug.log*
-372
View File
@@ -1,372 +0,0 @@
module.exports = {
name: 'Stripe Docs Blue',
type: 'dark',
colors: {
'editor.background': '#232323',
'editor.foreground': '#fafafa',
'activityBar.background': 'var(--colors-scale2)',
'sideBar.background': 'yellow',
'editorGroupHeader.tabsBackground': 'var(--colors-scale2)',
'sideBarSectionHeader.background': 'var(--colors-scale2)',
'tab.activeBackground': 'var(--colors-scale3)',
'tab.inactiveBackground': 'var(--colors-scale2)',
'tab.border': 'var(--colors-scale2)',
'input.background': '#ffffff1a',
'panel.background': '#1A2652',
'panel.border': '#1A2652',
'editorWidget.background': '#0d0f2b',
'editorWidget.foreground': '#ffffff4d',
'editorWidget.border': 'var(--colors-scale5)',
'list.hoverBackground': '#ffffff1a',
'list.activeSelectionBackground': '#ffffff1a',
'list.inactiveSelectionBackground': '#ffffff1a',
'editor.hoverHighlightBackground': '#ffffff1a',
'editor.selectionHighlightBackground': '#ffffff1a',
'activityBarBadge.background': 'yellow',
'sideBarTitle.foreground': 'var(--colors-scale2)',
'statusBar.background': 'var(--colors-scale2)',
},
tokenColors: [
{
name: 'Comment',
scope: ['comment', 'punctuation.definition.comment'],
settings: {
foreground: '#a3acb9',
fontStyle: '',
},
},
{
name: 'Variables',
scope: ['source', 'variable', 'variable.other.object', 'string constant.other.placeholder'],
settings: {
foreground: '#f5fbff',
},
},
{
name: 'Colors',
scope: ['variable.other.constant', 'constant.other.color'],
settings: {
foreground: '#ffffff',
fontStyle: 'bold',
},
},
{
name: 'Invalid',
scope: ['invalid', 'invalid.illegal'],
settings: {
foreground: '#FF5370',
},
},
{
name: 'Keyword, Storage',
scope: ['keyword', 'storage.type', 'storage.modifier'],
settings: {
foreground: '#98C1FE',
fontStyle: 'bold',
},
},
{
name: 'Function',
scope: ['entity.name.function'],
settings: {
foreground: '#7fd3ed',
fontStyle: 'bold',
},
},
{
name: 'Tag',
scope: ['entity.name.tag', 'meta.tag.sgml', 'markup.deleted.git_gutter'],
settings: {
foreground: '#98C1FE',
fontStyle: 'bold',
},
},
{
name: 'Parameter, Property',
scope: [
'variable.parameter',
'variable.other.object.property',
'variable.other.property',
'keyword.other.unit',
'keyword.other',
],
settings: {
foreground: '#F2AFE3',
},
},
{
name: 'Number, Constant, Function Argument, Tag Attribute, Embedded',
scope: [
'constant.numeric',
'constant.language',
'support.constant',
'constant.character',
'constant.escape',
],
settings: {
foreground: '#f8b886',
},
},
{
name: 'String, Symbols, Inherited Class, Markup Heading',
scope: [
'string',
'constant.other.symbol',
'constant.other.key',
'entity.other.inherited-class',
'markup.heading',
'markup.inserted.git_gutter',
'meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js',
],
settings: {
foreground: '#85d99e',
},
},
{
name: 'Entity Types',
scope: ['support.type'],
settings: {
foreground: '#B2CCD6',
},
},
{
name: 'CSS Class and Support',
scope: [
'source.css support.type.property-name',
'source.sass support.type.property-name',
'source.scss support.type.property-name',
'source.less support.type.property-name',
'source.stylus support.type.property-name',
'source.postcss support.type.property-name',
],
settings: {
foreground: '#B2CCD6',
},
},
{
name: 'Language methods',
scope: ['variable.language'],
settings: {
fontStyle: 'italic',
foreground: '#FF5370',
},
},
{
name: 'Attributes',
scope: ['entity.other.attribute-name'],
settings: {
foreground: '#98C1FE',
fontStyle: 'italic',
},
},
{
name: 'Inserted',
scope: ['markup.inserted'],
settings: {
foreground: '#C3E88D',
},
},
{
name: 'Deleted',
scope: ['markup.deleted'],
settings: {
foreground: '#FF5370',
},
},
{
name: 'Changed',
scope: ['markup.changed'],
settings: {
foreground: '#C792EA',
},
},
{
name: 'Regular Expressions',
scope: ['string.regexp'],
settings: {
foreground: '#89DDFF',
},
},
{
name: 'Escape Characters',
scope: ['constant.character.escape'],
settings: {
foreground: '#89DDFF',
},
},
{
name: 'URL',
scope: ['*url*', '*link*', '*uri*'],
settings: {
fontStyle: 'underline',
},
},
{
name: 'ES7 Bind Operator',
scope: ['source.js constant.other.object.key.js string.unquoted.label.js'],
settings: {
fontStyle: 'italic',
foreground: '#FF5370',
},
},
{
name: 'Markdown - Plain',
scope: ['text.html', 'punctuation.definition.list_item'],
settings: {
foreground: '#f5fbff',
},
},
{
name: 'Markdown - Markup Raw Inline',
scope: ['text.html.markdown markup.inline.raw.markdown'],
settings: {
foreground: '#C792EA',
},
},
{
name: 'Markdown - Markup Raw Inline Punctuation',
scope: ['text.html.markdown markup.inline.raw.markdown punctuation.definition.raw.markdown'],
settings: {
foreground: '#65737E',
},
},
{
name: 'Markdown - Heading',
scope: [
'markdown.heading',
'markup.heading | markup.heading entity.name',
'markup.heading.markdown punctuation.definition.heading.markdown',
],
settings: {
foreground: '#C3E88D',
},
},
{
name: 'Markup - Italic',
scope: ['markup.italic'],
settings: {
fontStyle: 'italic',
foreground: '#f07178',
},
},
{
name: 'Markup - Bold',
scope: ['markup.bold', 'markup.bold string'],
settings: {
fontStyle: 'bold',
foreground: '#f07178',
},
},
{
name: 'Markup - Bold-Italic',
scope: [
'markup.bold markup.italic',
'markup.italic markup.bold',
'markup.quote markup.bold',
'markup.bold markup.italic string',
'markup.italic markup.bold string',
'markup.quote markup.bold string',
],
settings: {
fontStyle: 'bold',
foreground: '#f07178',
},
},
{
name: 'Markup - Underline',
scope: ['markup.underline'],
settings: {
fontStyle: 'underline',
foreground: '#F78C6C',
},
},
{
name: 'Markdown - Blockquote',
scope: ['markup.quote punctuation.definition.blockquote.markdown'],
settings: {
foreground: '#65737E',
},
},
{
name: 'Markup - Quote',
scope: ['markup.quote'],
settings: {
fontStyle: 'italic',
},
},
{
name: 'Markdown - Link Description',
scope: ['string.other.link.description.title.markdown'],
settings: {
foreground: '#C792EA',
},
},
{
name: 'Markdown - Link Anchor',
scope: ['constant.other.reference.link.markdown'],
settings: {
foreground: '#FFCB6B',
},
},
{
name: 'Markup - Raw Block',
scope: ['markup.raw.block'],
settings: {
foreground: '#C792EA',
},
},
{
name: 'Markdown - Raw Block Fenced',
scope: ['markup.raw.block.fenced.markdown'],
settings: {
foreground: '#00000050',
},
},
{
name: 'Markdown - Fenced Bode Block',
scope: ['punctuation.definition.fenced.markdown'],
settings: {
foreground: '#00000050',
},
},
{
name: 'Markdown - Fenced Bode Block Variable',
scope: [
'markup.raw.block.fenced.markdown',
'variable.language.fenced.markdown',
'punctuation.section.class.end',
],
settings: {
foreground: '#EEFFFF',
},
},
{
name: 'Markdown - Fenced Language',
scope: ['variable.language.fenced.markdown'],
settings: {
foreground: '#65737E',
},
},
{
name: 'Markdown - Separator',
scope: ['meta.separator'],
settings: {
fontStyle: 'bold',
foreground: '#65737E',
},
},
{
name: 'Markup - Table',
scope: ['markup.table'],
settings: {
foreground: '#EEFFFF',
},
},
],
}
+4 -5
View File
@@ -1,13 +1,12 @@
import { FC } from 'react'
import { IconInfo, IconHelpCircle, IconAlertTriangle } from 'ui'
import { PropsWithChildren } from 'react'
import { IconAlertTriangle, IconHelpCircle, IconInfo } from 'ui'
interface Props {
export interface AdmonitionProps {
type: 'note' | 'tip' | 'info' | 'caution' | 'danger'
label?: string
children: any
}
const Admonition: FC<Props> = ({ type = 'note', label, children }) => {
const Admonition = ({ type = 'note', label, children }: PropsWithChildren<AdmonitionProps>) => {
return (
<div
className={[
@@ -0,0 +1,193 @@
import { useState } from 'react'
import { Input, Button } from 'ui'
import Admonition from '~/components/Admonition'
function base64URL(value: string) {
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
}
/*
Convert a string into an ArrayBuffer
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
*/
function stringToArrayBuffer(value: string) {
const buf = new ArrayBuffer(value.length)
const bufView = new Uint8Array(buf)
for (let i = 0; i < value.length; i++) {
bufView[i] = value.charCodeAt(i)
}
return buf
}
function arrayBufferToString(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf))
}
const generateAppleSecretKey = async (
kid: string,
iss: string,
sub: string,
file: File
): Promise<{ kid: string; jwt: string; exp: number }> => {
if (!kid) {
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
if (match && match[1]) {
kid = match[1]
}
}
if (!kid) {
throw new Error(
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
)
}
const contents = await file.text()
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
}
// remove PEM headers and spaces
const pkcs8 = stringToArrayBuffer(
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
)
const privateKey = await globalThis.crypto.subtle.importKey(
'pkcs8',
pkcs8,
{
name: 'ECDSA',
namedCurve: 'P-256',
},
true,
['sign']
)
const iat = Math.floor(Date.now() / 1000)
const exp = iat + 180 * 24 * 60 * 60
const jwt = [
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
base64URL(
JSON.stringify({
iss,
sub,
iat,
exp,
aud: 'https://appleid.apple.com',
})
),
]
const signature = await globalThis.crypto.subtle.sign(
{
name: 'ECDSA',
hash: 'SHA-256',
},
privateKey,
stringToArrayBuffer(jwt.join('.'))
)
jwt.push(base64URL(arrayBufferToString(signature)))
return { kid, jwt: jwt.join('.'), exp }
}
const AppleSecretGenerator = () => {
const [file, setFile] = useState({ file: null as File | null })
const [teamID, setTeamID] = useState('')
const [serviceID, setServiceID] = useState('')
const [keyID, setKeyID] = useState('')
const [secretKey, setSecretKey] = useState('')
const [expiresAt, setExpiresAt] = useState('')
const [error, setError] = useState('')
return (
<>
<Input
label="Account ID"
labelOptional="required"
placeholder="Apple Developer account ID, 10 alphanumeric digits"
descriptionText="Found in the upper-right corner of Apple Developer Center."
value={teamID}
onChange={(e) => setTeamID(e.target.value.trim())}
/>
<Input
label="Service ID"
labelOptional="required"
placeholder="ID of the service, example: com.example.app.service"
descriptionText="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
value={serviceID}
onChange={(e) => setServiceID(e.target.value.trim())}
/>
<Input
label="Key ID"
labelOptional="(optional)"
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
descriptionText="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
value={keyID}
onChange={(e) => setKeyID(e.target.value.trim())}
/>
<div>
<input
type="file"
onChange={(e) => {
setFile({ file: e.target.files[0] })
}}
/>
</div>
<div style={{ height: '1rem' }} />
<Button
size="medium"
disabled={
!(
teamID.length === 10 &&
serviceID &&
((globalThis && globalThis.showOpenFilePicker) || file.file)
)
}
onClick={async () => {
setError('')
try {
const { kid, jwt, exp } = await generateAppleSecretKey(
keyID,
teamID,
serviceID,
file.file
)
setKeyID(kid)
setSecretKey(jwt)
setExpiresAt(new Date(exp * 1000).toString())
setError('')
} catch (e: any) {
setError(e.message)
console.error(e)
}
}}
>
Generate Secret Key
</Button>
{error && <Admonition type="danger">{error}</Admonition>}
{secretKey && (
<>
<div style={{ height: '1rem' }} />
<Input
label="Secret Key"
value={secretKey}
descriptionText={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
reveal
copy
size="medium"
/>
</>
)}
</>
)
}
export default AppleSecretGenerator
@@ -0,0 +1,96 @@
import { getAnchor, removeAnchor } from './CustomHTMLElements.utils'
describe('CustomHTMLElementsUtils', () => {
describe('getAnchor', () => {
describe('when value is an object', () => {
it('returns slugified version of props.children', () => {
const value = {
props: {
children: 'Full Text Search',
},
}
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
})
describe('when value is an array', () => {
describe('when custom anchor exists', () => {
it('returns inner slug', () => {
const value = ['Proximity: <->', '[#proximity]']
const result = getAnchor(value)
expect(result).toStrictEqual('proximity')
})
it('trims whitespace', () => {
const value = ['Proximity: <->', ' [#proximity] ']
const result = getAnchor(value)
expect(result).toStrictEqual('proximity')
})
})
it('returns concatenated slug of elements', () => {
const value = ['Full', 'Text', 'Search']
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
it('trims whitespace', () => {
const value = [' Full ', ' Text ', ' Search ']
const result = getAnchor(value)
expect(result).toStrictEqual('full-text-search')
})
it('removes special characters', () => {
const value = ['function()']
const result = getAnchor(value)
expect(result).toStrictEqual('function')
})
})
describe('when value is a string', () => {
it('returns slugified version of string', () => {
const value = 'My (Very) Awesome Heading'
const result = getAnchor(value)
expect(result).toStrictEqual('my-very-awesome-heading')
})
})
})
describe('removeAnchor', () => {
describe('when value is an array', () => {
it('filters out custom anchor elements', () => {
const value = ['My (Very) Awesome Heading', '[#my-custom-heading]']
const result = removeAnchor(value)
expect(result).toStrictEqual(['My (Very) Awesome Heading'])
})
})
describe('when value is a string', () => {
it('strips out custom anchor string', () => {
const value = 'My (Very) Awesome Heading [#my-custom-heading]'
const result = removeAnchor(value)
// Original implementation didn't trim the resulting string - not sure if it really matters
expect(result).toStrictEqual('My (Very) Awesome Heading ')
})
})
})
})
@@ -2,53 +2,60 @@
export const getAnchor = (text: any): string | undefined => {
if (typeof text === 'object') {
if (Array.isArray(text)) {
const customAnchor = text.find(
(x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']')
)
if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']'))
const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x))
if (customAnchor !== undefined) {
return parseCustomAnchor(customAnchor)
}
const formattedText = text
.map((x) => {
if (typeof x !== 'string') return x.props.children
else return x.trim()
if (typeof x !== 'string') {
return x.props.children
}
return x.trim()
})
.map((x) => {
if (typeof x !== 'string') return x
else
if (typeof x !== 'string') {
return x
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
}
return slugify(x)
})
return formattedText.join('-').toLowerCase()
} else {
const anchor = text.props.children
if (typeof anchor === 'string') {
return anchor
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
return slugify(anchor)
}
return anchor
}
} else if (typeof text === 'string') {
if (text.includes('[#') && text.endsWith(']')) {
return text.slice(text.indexOf('[#') + 2, text.indexOf(']'))
} else {
return text
.toLowerCase()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
if (hasCustomAnchor(text)) {
return parseCustomAnchor(text)
}
return slugify(text)
} else {
return undefined
}
}
const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']')
const parseCustomAnchor = (value: string): string =>
value.slice(value.indexOf('[#') + 2, value.indexOf(']'))
const slugify = (value: string): string =>
value
.toLowerCase()
.trim()
.replace(/[^a-z0-9- ]/g, '')
.replace(/[ ]/g, '-')
export const removeAnchor = (text: any) => {
if (typeof text === 'object' && Array.isArray(text)) {
return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']')))
return text.filter((x) => !(typeof x === 'string' && hasCustomAnchor(x)))
} else if (typeof text === 'string') {
if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#'))
else return text
+1 -1
View File
@@ -5,7 +5,7 @@ export type FooterHelpCalloutType = 'default' | 'postgres'
const content = {
default: {
title: 'Need some help?',
description: `Not to worry, our specialist engineers are here to help. Submit a support ticket through the [Dashboard](https://app.supabase.com/support/new).`,
description: `Not to worry, our specialist engineers are here to help. Submit a support ticket through the [Dashboard](https://supabase.com/dashboard/support/new).`,
},
postgres: {
title: 'Looking for Serverless Postgres?',
+58 -73
View File
@@ -7,6 +7,49 @@ const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
const iconSize = isXs ? 'sm' : 'lg'
const frameworks = [
{
tooltip: 'ReactJS',
icon: '/docs/img/icons/react-icon',
href: '/guides/getting-started/quickstarts/reactjs',
},
{
tooltip: 'NextJS',
icon: '/docs/img/icons/nextjs-icon',
href: '/guides/getting-started/quickstarts/nextjs',
},
{
tooltip: 'RedwoodJS',
icon: '/docs/img/icons/redwoodjs-icon',
href: '/guides/getting-started/quickstarts/redwoodjs',
},
{
tooltip: 'Flutter',
icon: '/docs/img/icons/flutter-icon',
href: '/guides/getting-started/quickstarts/flutter',
},
{
tooltip: 'SvelteKit',
icon: '/docs/img/icons/svelte-icon',
href: '/guides/getting-started/quickstarts/sveltekit',
},
{
tooltip: 'SolidJS',
icon: '/docs/img/icons/solidjs-icon',
href: '/guides/getting-started/quickstarts/solidjs',
},
{
tooltip: 'Vue',
icon: '/docs/img/icons/vuejs-icon',
href: '/guides/getting-started/quickstarts/vue',
},
{
tooltip: 'NuxtJS',
icon: '/docs/img/icons/nuxt-icon',
href: '/guides/getting-started/quickstarts/nuxtjs',
},
]
const GettingStarted = () => (
<div
className="
@@ -17,7 +60,7 @@ const HomePageCover = (props) => {
p-5 md:p-8
"
>
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-6">
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
<div className="md:max-w-xs xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
@@ -29,77 +72,19 @@ const HomePageCover = (props) => {
Discover how to set up a database to an app making queries in just a few minutes.
</p>
</div>
<div className="flex flex-wrap md:grid md:grid-cols-4 gap-2 sm:gap-3">
<Link href={`/guides/getting-started/quickstarts/reactjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="ReactJS"
icon="/docs/img/icons/react-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/nextjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="NextJS"
icon="/docs/img/icons/nextjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/redwoodjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="RedwoodJS"
icon="/docs/img/icons/redwoodjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/flutter`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="Flutter"
icon="/docs/img/icons/flutter-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/sveltekit`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="SvelteKit"
icon="/docs/img/icons/svelte-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/solidjs`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="SolidJS"
icon="/docs/img/icons/solidjs-icon"
/>
</a>
</Link>
<Link href={`/guides/getting-started/quickstarts/vue`} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip="Vue"
icon="/docs/img/icons/vuejs-icon"
/>
</a>
</Link>
<div className="flex flex-wrap md:grid md:grid-cols-4 2xl:grid-cols-7 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref>
<a className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip={framework.tooltip}
icon={framework.icon}
/>
</a>
</Link>
))}
</div>
</div>
</div>
@@ -118,7 +103,7 @@ const HomePageCover = (props) => {
</p>
</div>
</div>
<div className="w-full xl:max-w-[365px] -mb-40">
<div className="w-full xl:max-w-[365px] 2xl:max-w-[608px] -mb-40">
<GettingStarted />
</div>
</div>
@@ -0,0 +1,5 @@
## Deployment
If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide.
Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.
@@ -0,0 +1,18 @@
import { Tabs } from 'ui'
export const TabPanel = Tabs.Panel
## Project setup
Let's create a new Postgres database. This is as simple as starting a new Project in Supabase:
1. [Create a new project](https://database.new/) in the Supabase dashboard.
1. Enter your project details. Remember to store your password somewhere safe.
Your database will be available in less than a minute.
**Finding your credentials:**
You can find your project credentials inside the project [settings](https://app.supabase.com/project/_/settings/), including:
- [Database credentials](https://app.supabase.com/project/_/settings/database): connection strings and connection pooler details.
- [API credentials](https://app.supabase.com/project/_/settings/database): your serverless API URL and `anon` / `service_role` keys.
@@ -11,7 +11,7 @@ Enable the [http extension for the `extensions` schema](https://app.supabase.com
Then, define the following SQL functions in the SQL Editor to delete
storage objects via the API:
```SQL
```sql
create or replace function delete_storage_object(bucket text, object text, out status int, out content text)
returns record
language 'plpgsql'
@@ -52,7 +52,7 @@ $$;
Next, add a trigger that removes any obsolete avatar whenever the
profile is updated or deleted:
```SQL
```sql
create or replace function delete_old_avatar()
returns trigger
language 'plpgsql'
@@ -89,7 +89,7 @@ Finally, delete the `public.profile` row before a user is deleted.
If this step is omitted, you won't be able to delete users without
first manually deleting their avatar image.
```SQL
```sql
create or replace function delete_old_profile()
returns trigger
language 'plpgsql'
+75 -93
View File
@@ -55,105 +55,87 @@ const Footer = () => (
))}
</div>
<div className="flex items-center gap-2">
<Button
type="text"
as="a"
// @ts-ignore
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="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
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="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
href="https://twitter.com/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
d="M5.52713 13.7223C10.5582 13.7223 13.3101 9.55365 13.3101 5.93898C13.3101 5.82032 13.3101 5.70232 13.3021 5.58565C13.8375 5.198 14.2996 4.71806 14.6667 4.16832C14.1675 4.38965 13.6378 4.53483 13.0955 4.59898C13.6665 4.25702 14.0939 3.71922 14.2981 3.08565C13.7612 3.40424 13.1739 3.6288 12.5615 3.74965C12.1491 3.31083 11.6035 3.02021 11.0093 2.92279C10.4151 2.82537 9.80528 2.92658 9.27438 3.21076C8.74347 3.49494 8.32106 3.94623 8.07255 4.49476C7.82403 5.04328 7.76328 5.65844 7.89969 6.24498C6.81213 6.19048 5.74819 5.90787 4.77693 5.4155C3.80567 4.92313 2.9488 4.232 2.26194 3.38698C1.91217 3.98912 1.80502 4.70193 1.96231 5.38029C2.1196 6.05866 2.52951 6.65157 3.10857 7.03832C2.67343 7.02572 2.2477 6.90865 1.86729 6.69698V6.73165C1.86755 7.36318 2.08621 7.97519 2.4862 8.46389C2.88618 8.95259 3.44287 9.28791 4.06186 9.41298C3.65909 9.52279 3.2365 9.53876 2.82658 9.45965C3.00135 10.0033 3.34168 10.4787 3.79996 10.8194C4.25824 11.1601 4.81154 11.349 5.38247 11.3597C4.81524 11.8054 4.16572 12.1349 3.47104 12.3293C2.77636 12.5238 2.05017 12.5794 1.33398 12.493C2.585 13.2959 4.04066 13.7217 5.52713 13.7197"
fill="var(--colors-scale9)"
/>
</svg>
</Button>
<Button
type="text"
as="a"
// @ts-ignore
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)">
<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="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="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="var(--colors-scale9)"
/>
</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://youtube.com/c/supabase" target="_blank" rel="noreferrer noopener">
<a>
<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="var(--colors-scale9)"
/>
</clipPath>
</defs>
</svg>
</svg>
</a>
</a>
</Button>
<Button type="text" asChild>
<a href="https://twitter.com/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
d="M5.52713 13.7223C10.5582 13.7223 13.3101 9.55365 13.3101 5.93898C13.3101 5.82032 13.3101 5.70232 13.3021 5.58565C13.8375 5.198 14.2996 4.71806 14.6667 4.16832C14.1675 4.38965 13.6378 4.53483 13.0955 4.59898C13.6665 4.25702 14.0939 3.71922 14.2981 3.08565C13.7612 3.40424 13.1739 3.6288 12.5615 3.74965C12.1491 3.31083 11.6035 3.02021 11.0093 2.92279C10.4151 2.82537 9.80528 2.92658 9.27438 3.21076C8.74347 3.49494 8.32106 3.94623 8.07255 4.49476C7.82403 5.04328 7.76328 5.65844 7.89969 6.24498C6.81213 6.19048 5.74819 5.90787 4.77693 5.4155C3.80567 4.92313 2.9488 4.232 2.26194 3.38698C1.91217 3.98912 1.80502 4.70193 1.96231 5.38029C2.1196 6.05866 2.52951 6.65157 3.10857 7.03832C2.67343 7.02572 2.2477 6.90865 1.86729 6.69698V6.73165C1.86755 7.36318 2.08621 7.97519 2.4862 8.46389C2.88618 8.95259 3.44287 9.28791 4.06186 9.41298C3.65909 9.52279 3.2365 9.53876 2.82658 9.45965C3.00135 10.0033 3.34168 10.4787 3.79996 10.8194C4.25824 11.1601 4.81154 11.349 5.38247 11.3597C4.81524 11.8054 4.16572 12.1349 3.47104 12.3293C2.77636 12.5238 2.05017 12.5794 1.33398 12.493C2.585 13.2959 4.04066 13.7217 5.52713 13.7197"
fill="var(--colors-scale9)"
/>
</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"
fill="var(--colors-scale9)"
/>
</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>
@@ -9,8 +9,8 @@ export interface NavMenuGroup {
export interface NavMenuSection {
name: string
url?: string
items: NavMenuSection[]
url?: `/${string}`
items: Partial<NavMenuSection>[]
}
export interface References {
@@ -26,10 +26,17 @@ export interface References {
type MenuItem = {
label: string
icon?: string
href?: string
href?: `/${string}` | `https://${string}`
level?: string
hasLightIcon?: boolean
community?: boolean
}
export type HomepageMenuItems = MenuItem[][]
export type NavMenuConstant = Readonly<{
title: string
icon: string
url?: `/${string}`
items: ReadonlyArray<Partial<NavMenuSection>>
}>
@@ -3,8 +3,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Fragment } from 'react'
import { Badge } from '~/../../packages/ui'
import { cn } from 'ui/src/utils/cn'
import { Badge, cn } from 'ui'
import { HOMEPAGE_MENU_ITEMS } from './NavigationMenu.constants'
import HomeMenuIconPicker from './HomeMenuIconPicker'
@@ -21,6 +21,7 @@ import {
IconMenuSwift,
IconMenuStatus,
IconMenuKotlin,
IconMenuAI,
} from './HomeMenuIcons'
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
@@ -41,6 +42,8 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
return <IconMenuRealtime width={width} height={height} />
case 'storage':
return <IconMenuStorage width={width} height={height} />
case 'ai':
return <IconMenuAI width={width} height={height} />
case 'platform':
return <IconMenuPlatform width={width} height={height} />
case 'resources':
@@ -32,7 +32,7 @@ export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
>
<path
d="M11.7118 15.4651C10.8778 15.8159 9.96154 16.0098 9 16.0098C8.03846 16.0098 7.1222 15.8159 6.28818 15.4651M2.42438 11.4156C2.14983 10.6654 2 9.85506 2 9.00977C2 8.16447 2.14983 7.35417 2.42438 6.60397M6.28818 2.55442C7.1222 2.20364 8.03846 2.00977 9 2.00977C9.96154 2.00977 10.8778 2.20364 11.7118 2.55442M15.4553 6.29795C15.8061 7.13197 16 8.04823 16 9.00977C16 9.97131 15.8061 10.8876 15.4553 11.7216M10.7427 10.861L13.0078 13.1261M4.99476 5.11305L7.25997 7.37826M7.14874 10.7525L4.88369 13.0176M12.8967 5.00452L10.6315 7.26974M6.08663 9.00977C6.08663 7.40076 7.39099 6.0964 9 6.0964C10.609 6.0964 11.9134 7.40076 11.9134 9.00977C11.9134 10.6188 10.609 11.9231 9 11.9231C7.39099 11.9231 6.08663 10.6188 6.08663 9.00977ZM3.10575 3.11551C3.58524 2.63602 4.36265 2.63602 4.84214 3.11551C5.32164 3.59501 5.32164 4.37242 4.84214 4.85191C4.36265 5.3314 3.58524 5.3314 3.10575 4.85191C2.62625 4.37242 2.62625 3.59501 3.10575 3.11551ZM13.1579 13.1676C13.6373 12.6881 14.4148 12.6881 14.8943 13.1676C15.3737 13.6471 15.3737 14.4245 14.8943 14.904C14.4148 15.3835 13.6373 15.3835 13.1579 14.904C12.6784 14.4245 12.6784 13.6471 13.1579 13.1676ZM14.8948 3.11549C15.3743 3.59498 15.3743 4.37239 14.8948 4.85189C14.4153 5.33138 13.6379 5.33138 13.1584 4.85189C12.6789 4.37239 12.6789 3.59498 13.1584 3.11549C13.6379 2.636 14.4153 2.636 14.8948 3.11549ZM4.84214 13.1676C5.32164 13.6471 5.32164 14.4245 4.84214 14.904C4.36265 15.3835 3.58524 15.3835 3.10575 14.904C2.62625 14.4245 2.62625 13.6471 3.10575 13.1676C3.58524 12.6881 4.36265 12.6881 4.84214 13.1676Z"
stroke="#A0A0A0"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
@@ -363,6 +363,26 @@ export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
)
}
export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.99886 7.63216V14.4892M7.99886 7.63216L14.0488 4.11804M7.99886 7.63216L1.94922 4.11819M1.94922 4.11819V8.32332M1.94922 4.11819V4.08217L5.57319 1.97717M14.049 8.36007V4.08217L10.4251 1.97717M11.8165 12.4072L7.99913 14.6245L4.18177 12.4072"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
@@ -412,7 +432,7 @@ export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) {
>
<path
d="M8.9895 1.13281C4.64498 1.13281 1.12305 4.65474 1.12305 8.99927C1.12305 13.3438 4.64498 16.8657 8.9895 16.8657C13.334 16.8657 16.856 13.3438 16.856 8.99927C16.856 7.13428 16.207 5.42087 15.1225 4.07258L8.35267 10.8424L6.39571 8.88546M9.06632 4.19659C6.42145 4.19659 4.27737 6.34068 4.27737 8.98555C4.27737 11.6304 6.42145 13.7745 9.06632 13.7745C11.7112 13.7745 13.8553 11.6304 13.8553 8.98555C13.8553 7.99916 13.5571 7.08243 13.0459 6.32059"
stroke="#A0A0A0"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
@@ -1,4 +1,4 @@
import { References, HomepageMenuItems } from '../Navigation.types'
import type { HomepageMenuItems, NavMenuConstant, References } from '../Navigation.types'
export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
[
@@ -52,6 +52,12 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
href: '/guides/storage',
level: 'storage',
},
{
label: 'AI & Vectors',
icon: 'ai',
href: '/guides/ai',
level: 'ai',
},
],
[
{
@@ -194,7 +200,7 @@ export const REFERENCES: References = {
},
}
export const gettingstarted = {
export const gettingstarted: NavMenuConstant = {
icon: 'getting-started',
title: 'Getting Started',
items: [
@@ -206,6 +212,7 @@ export const gettingstarted = {
items: [
{ name: 'React', url: '/guides/getting-started/quickstarts/reactjs' },
{ name: 'NextJS', url: '/guides/getting-started/quickstarts/nextjs' },
{ name: 'NuxtJS', url: '/guides/getting-started/quickstarts/nuxtjs' },
{ name: 'RedwoodJS', url: '/guides/getting-started/quickstarts/redwoodjs' },
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' },
{ name: 'SvelteKit', url: '/guides/getting-started/quickstarts/sveltekit' },
@@ -280,15 +287,6 @@ export const gettingstarted = {
},
],
},
{
name: 'AI & ML',
items: [
{
name: 'Vector Search with OpenAI',
url: '/guides/getting-started/openai/vector-search',
},
],
},
],
}
@@ -321,7 +319,7 @@ export const SocialLoginItems = [
url: '/guides/auth/social-login/auth-apple',
},
{
name: 'Azure',
name: 'Azure (Microsoft)',
icon: '/docs/img/icons/microsoft-icon',
url: '/guides/auth/social-login/auth-azure',
},
@@ -475,10 +473,10 @@ export const auth = {
items: [
{ name: 'Overview', url: '/guides/auth/auth-helpers' },
{ name: 'Auth UI', url: '/guides/auth/auth-helpers/auth-ui' },
{ name: 'Next.js (pages)', url: '/guides/auth/auth-helpers/nextjs' },
{ name: 'Flutter Auth UI', url: '/guides/auth/auth-helpers/flutter-auth-ui' },
{
name: 'Next.js (app)',
url: '/guides/auth/auth-helpers/nextjs-server-components',
name: 'Next.js',
url: '/guides/auth/auth-helpers/nextjs',
},
{ name: 'Remix', url: '/guides/auth/auth-helpers/remix' },
{ name: 'SvelteKit', url: '/guides/auth/auth-helpers/sveltekit' },
@@ -507,17 +505,43 @@ export const auth = {
],
}
export const database = {
export const database: NavMenuConstant = {
icon: 'database',
title: 'Database',
url: '/guides/database',
items: [
{ name: 'Database Connections', url: '/guides/database/connecting-to-postgres' },
{ name: 'Tables and Data', url: '/guides/database/tables' },
{ name: 'Database Functions', url: '/guides/database/functions' },
{ name: 'Database Webhooks', url: '/guides/database/webhooks' },
{ name: 'Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Database Testing', url: '/guides/database/testing' },
{ name: 'Overview', url: '/guides/database' },
{
name: 'Fundamentals',
url: undefined,
items: [
{ name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres' },
{ name: 'Managing tables, views, and data', url: '/guides/database/tables' },
{ name: 'Managing database functions', url: '/guides/database/functions' },
{ name: 'Managing indexes', url: '/guides/database/postgres/indexes' },
{ name: 'Managing database webhooks', url: '/guides/database/webhooks' },
{ name: 'Managing database replication', url: '/guides/database/replication' },
{ name: 'Managing secrets with Vault', url: '/guides/database/vault' },
],
},
{
name: 'Postgres Guides',
url: undefined,
items: [
{
name: 'JSON and unstructured data',
url: '/guides/database/json',
},
{ name: 'Implementing Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
{ name: 'Implementing column encryption', url: '/guides/database/column-encryption' },
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
{ name: 'Testing your database', url: '/guides/database/testing' },
{ name: 'Managing Timeouts', url: '/guides/database/timeouts' },
{ name: 'Managing Passwords', url: '/guides/database/managing-passwords' },
{ name: 'Configuring Timezones', url: '/guides/database/managing-timezones' },
],
},
{
name: 'Extensions',
url: undefined,
@@ -624,17 +648,9 @@ export const database = {
],
},
{
name: 'Postgres resources',
name: 'Examples',
url: undefined,
items: [
{
name: 'Managing Indexes',
url: '/guides/database/postgres/indexes',
},
{
name: 'Cascade Deletes',
url: '/guides/database/postgres/cascade-deletes',
},
{
name: 'Drop All Tables in Schema',
url: '/guides/database/postgres/dropping-all-tables-in-schema',
@@ -649,20 +665,10 @@ export const database = {
},
],
},
{
name: 'Configuration',
url: undefined,
items: [
{ name: 'Timeouts', url: '/guides/database/timeouts' },
{ name: 'Replication', url: '/guides/database/replication' },
{ name: 'Passwords', url: '/guides/database/managing-passwords' },
{ name: 'Timezones', url: '/guides/database/managing-timezones' },
],
},
],
}
export const api = {
export const api: NavMenuConstant = {
icon: 'serverless-apis',
title: 'Serverless APIs',
url: '/guides/api',
@@ -700,7 +706,7 @@ export const api = {
],
}
export const functions = {
export const functions: NavMenuConstant = {
icon: 'edge-functions',
title: 'Edge Functions',
url: '/guides/functions',
@@ -728,7 +734,7 @@ export const functions = {
url: undefined,
items: [
{ name: 'Developing Functions locally', url: '/guides/functions/local-development' },
{ name: 'Deploying with Git', url: '/guides/functions/cicd-workflow' },
{ name: 'Deploying with GitHub', url: '/guides/functions/cicd-workflow' },
{ name: 'Managing Secrets and Environment Variables', url: '/guides/functions/secrets' },
{ name: 'Integrating With Supabase Auth', url: '/guides/functions/auth' },
{
@@ -741,6 +747,7 @@ export const functions = {
name: 'Connecting directly to Postgres',
url: '/guides/functions/connect-to-postgres',
},
{ name: 'Troubleshooting', url: '/guides/functions/troubleshooting' },
],
},
{
@@ -749,7 +756,9 @@ export const functions = {
items: [
{ name: 'Dart Edge on Supabase', url: '/guides/functions/dart-edge' },
{ name: 'Browserless.io', url: '/guides/functions/examples/screenshots' },
{ name: 'OpenAI API', url: '/guides/functions/examples/openai' },
{ 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' },
],
@@ -758,7 +767,7 @@ export const functions = {
name: 'Examples',
url: '/guides/functions/examples',
items: [
{ name: 'Generating OpenAI GPT3 completions', url: '/guides/functions/examples/openai' },
{ name: 'Generating OpenAI GPT3 completions', url: '/guides/ai/examples/openai' },
{ name: 'Generating OG images ', url: '/guides/functions/examples/og-image' },
{
name: 'CAPTCHA support with Cloudflare Turnstile',
@@ -778,7 +787,7 @@ export const functions = {
],
}
export const realtime = {
export const realtime: NavMenuConstant = {
icon: 'realtime',
title: 'Realtime',
url: '/guides/realtime',
@@ -787,6 +796,10 @@ export const realtime = {
name: 'Overview',
url: '/guides/realtime',
},
{
name: 'Concepts',
url: '/guides/realtime/concepts',
},
{
name: 'Quickstart',
url: '/guides/realtime/quickstart',
@@ -795,18 +808,11 @@ export const realtime = {
name: 'Features',
url: undefined,
items: [
{ name: 'Channels', url: '/guides/realtime/channels' },
{ name: 'Broadcast', url: '/guides/realtime/broadcast' },
{ name: 'Presence', url: '/guides/realtime/presence' },
{
name: 'Extensions',
url: '/guides/realtime/extensions',
items: [
{ name: 'Broadcast', url: '/guides/realtime/extensions/broadcast' },
{ name: 'Presence', url: '/guides/realtime/extensions/presence' },
{
name: 'Postgres Changes',
url: '/guides/realtime/extensions/postgres-changes',
},
],
name: 'Postgres Changes',
url: '/guides/realtime/postgres-changes',
},
],
},
@@ -833,7 +839,7 @@ export const realtime = {
name: 'Deep dive',
url: undefined,
items: [
{ name: 'Rate Limits', url: '/guides/realtime/rate-limits' },
{ name: 'Quotas', url: '/guides/realtime/quotas' },
{ name: 'Architecture', url: '/guides/realtime/architecture' },
{ name: 'Protocol', url: '/guides/realtime/protocol' },
],
@@ -841,7 +847,7 @@ export const realtime = {
],
}
export const storage = {
export const storage: NavMenuConstant = {
icon: 'storage',
title: 'Storage',
url: '/guides/storage',
@@ -855,7 +861,104 @@ export const storage = {
],
}
export const supabase_cli = {
export const ai: NavMenuConstant = {
icon: 'ai',
title: 'AI & Vectors',
url: '/guides/ai',
items: [
{ name: 'Overview', url: '/guides/ai' },
{ name: 'Concepts', url: '/guides/ai/concepts' },
{
name: 'Structured & unstructured',
url: '/guides/ai/structured-unstructured',
},
{
name: 'Quickstarts',
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: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
],
},
{
name: 'Python Client',
url: undefined,
items: [
{ name: 'API', url: '/guides/ai/python/api' },
{ name: 'Collections', url: '/guides/ai/python/collections' },
{ name: 'Indexes', url: '/guides/ai/python/indexes' },
{ name: 'Metadata', url: '/guides/ai/python/metadata' },
],
},
{
name: 'Guides',
url: undefined,
items: [
{ name: 'Managing collections', url: '/guides/ai/managing-collections' },
{ name: 'Managing indexes', url: '/guides/ai/managing-indexes' },
{ name: 'Vector columns', url: '/guides/ai/vector-columns' },
{ name: 'Engineering for scale', url: '/guides/ai/engineering-for-scale' },
{ name: 'Choosing instance type', url: '/guides/ai/choosing-instance-type' },
],
},
{
name: 'Examples',
url: undefined,
items: [
{
name: 'OpenAI completions using Edge Functions',
url: '/guides/ai/examples/openai',
},
{
name: 'Image search with OpenAI CLIP',
url: '/guides/ai/examples/image-search-openai-clip',
},
{
name: 'Generate image captions using Hugging Face',
url: '/guides/ai/examples/huggingface-image-captioning',
},
{
name: 'Building ChatGPT Plugins',
url: '/guides/ai/examples/building-chatgpt-plugins',
},
{
name: 'Adding generative Q&A to your documentation',
url: '/guides/ai/examples/headless-vector-search',
},
{
name: 'Adding generative Q&A to your Next.js site',
url: '/guides/ai/examples/nextjs-vector-search',
},
],
},
{
name: 'Third-Party Tools',
url: undefined,
items: [
{
name: 'LangChain',
url: '/guides/ai/langchain',
},
{
name: 'Hugging Face',
url: '/guides/ai/hugging-face',
},
{
name: 'Google Colab',
url: '/guides/ai/google-colab',
},
{
name: 'LlamaIndex',
url: '/guides/ai/integrations/llamaindex',
},
],
},
],
}
export const supabase_cli: NavMenuConstant = {
icon: 'reference-cli',
title: 'Supabase CLI',
url: '/guides/cli',
@@ -877,7 +980,7 @@ export const supabase_cli = {
],
}
export const platform = {
export const platform: NavMenuConstant = {
icon: 'platform',
title: 'Platform',
url: '/guides/platform',
@@ -896,6 +999,7 @@ export const platform = {
url: undefined,
items: [
{ name: 'Access Control', url: '/guides/platform/access-control' },
{ name: 'Custom Postgres Config', url: '/guides/platform/custom-postgres-config' },
{ name: 'Database Size', url: '/guides/platform/database-size' },
{ name: 'HTTP Status Codes', url: '/guides/platform/http-status-codes' },
{ name: 'Logging', url: '/guides/platform/logs' },
@@ -939,7 +1043,11 @@ export const platform = {
name: 'Shared Responsibility Model',
url: '/guides/platform/shared-responsibility-model',
},
{ name: 'Going into Production', url: '/guides/platform/going-into-prod' },
{
name: 'Maturity Model',
url: '/guides/platform/maturity-model',
},
{ name: 'Production Checklist', url: '/guides/platform/going-into-prod' },
],
},
{
@@ -954,12 +1062,16 @@ export const platform = {
name: 'High CPU Usage',
url: '/guides/platform/exhaust-cpu',
},
{
name: 'High RAM Usage',
url: '/guides/platform/exhaust-ram',
},
],
},
],
}
export const resources = {
export const resources: NavMenuConstant = {
icon: 'resources',
title: 'Resources',
url: '/guides/resources',
@@ -1000,7 +1112,7 @@ export const resources = {
],
}
export const self_hosting = {
export const self_hosting: NavMenuConstant = {
title: 'Self-Hosting',
icon: 'self-hosting',
url: '/guides/self-hosting',
@@ -1057,7 +1169,7 @@ export const migrate = {
],
}
export const integrations = {
export const integrations: NavMenuConstant = {
icon: 'integrations',
title: 'Integrations',
url: '/guides/integrations',
@@ -1088,8 +1200,9 @@ export const integrations = {
name: 'Developer Tools',
url: undefined,
items: [
{ name: 'Cloudflare Workers', url: '/guides/integrations/cloudflare-workers' },
{ name: 'Estuary', url: '/guides/integrations/estuary' },
{ name: 'OpenAI', url: '/guides/functions/examples/openai' },
{ name: 'OpenAI', url: '/guides/ai/examples/openai' },
{ name: 'pgMustard', url: '/guides/integrations/pgmustard' },
{ name: 'Prisma', url: '/guides/integrations/prisma' },
{ name: 'Sequin', url: '/guides/integrations/sequin' },
@@ -1105,10 +1218,12 @@ export const integrations = {
url: undefined,
items: [
{ name: 'Appsmith', url: '/guides/integrations/appsmith' },
{ name: 'Bracket', url: '/guides/integrations/bracket' },
{ name: 'DhiWise', url: '/guides/integrations/dhiwise' },
{ name: 'Directus', url: '/guides/integrations/directus' },
{ name: 'Draftbit', url: '/guides/integrations/draftbit' },
{ name: 'FlutterFlow', url: '/guides/integrations/flutterflow' },
{ name: 'Forest Admin', url: '/guides/integrations/forestadmin' },
{ name: 'Plasmic', url: '/guides/integrations/plasmic' },
{ name: 'ILLA', url: '/guides/integrations/illa' },
],
@@ -68,6 +68,11 @@ const menus: Menu[] = [
path: '/guides/storage',
type: 'guide',
},
{
id: 'ai',
path: '/guides/ai',
type: 'guide',
},
{
id: 'platform',
path: '/guides/platform',
@@ -46,7 +46,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
if (activeItem && activeItemRef.current) {
// this is a hack, but seems a common one on Stackoverflow
setTimeout(() => {
activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, 0)
}
})
@@ -1,7 +1,7 @@
import * as Accordion from '@radix-ui/react-accordion'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { IconChevronLeft, IconChevronUp } from 'ui'
import { IconChevronLeft, IconChevronUp, cn } from 'ui'
import * as NavItems from './NavigationMenu.constants'
import Image from 'next/image'
@@ -10,7 +10,6 @@ import RevVersionDropdown from '~/components/RefVersionDropdown'
import { useMenuActiveRefId } from '~/hooks/useMenuState'
import React, { Fragment } from 'react'
import { cn } from 'ui/src/utils/cn'
import { ICommonItem, ICommonSection } from '~/components/reference/Reference.types'
import HomeMenuIconPicker from './HomeMenuIconPicker'
import { deepFilterSections } from './NavigationMenu.utils'
@@ -197,7 +196,7 @@ const NavigationMenuRefListItems = ({
<HeaderLink title={menu.title} url={menu.url} id={id} />
<RevVersionDropdown />
</div>
<ul className="function-link-list flex flex-col gap-2">
<ul className="function-link-list flex flex-col gap-2 pb-5">
{filteredSections.map((section) => {
return (
<Fragment key={section.title}>
@@ -125,16 +125,15 @@ const TopNavBar: FC = () => {
</div>
</div>
<div className="col-span-4 flex items-center justify-end gap-3">
<Button
type="outline"
as="a"
// @ts-ignore
href="https://app.supabase.com"
className="text-scale-1100 text-sm"
target="_blank"
rel="noreferrer noopener"
>
Go to Dashboard
<Button type="outline" asChild>
<a
href="https://supabase.com/dashboard"
className="text-scale-1100 text-sm"
target="_blank"
rel="noreferrer noopener"
>
Go to Dashboard
</a>
</Button>
<ul className="flex items-center">
<li className="px-4">
@@ -105,56 +105,40 @@ const TopNavBarRef: FC = () => {
</SearchButton>
</div>
<div className="hidden lg:flex grow items-center justify-end gap-3">
<Button
type="text"
as="a"
// @ts-ignore
href="https://supabase.com"
<Button type="text" asChild>
<a href="https://supabase.com" target="_blank" rel="noreferrer noopener">
Supabase.com
</a>
</Button>
<Button type="text" asChild>
<a href="https://supabase.com/dashboard" target="_blank" rel="noreferrer noopener">
Dashboard
</a>
</Button>
<Link
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
>
Supabase.com
</Button>
<Button
type="text"
as="a"
// @ts-ignore
href="https://app.supabase.com"
target="_blank"
rel="noreferrer noopener"
>
Dashboard
</Button>
<ul className="flex items-center">
<Link
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
>
<a className="px-2.5 py-1" target="_blank">
<IconGitHub size={16} />
</a>
</Link>
</ul>
<ul className="flex items-center">
<li className="px-4">
<div className="cursor-pointer" onClick={() => toggleTheme()}>
{isDarkMode ? (
<IconMoon
size={16}
strokeWidth={1}
className="text-scale-1100 hover:text-scale-1200 transition"
/>
) : (
<IconSun
size={16}
strokeWidth={1}
className="text-scale-1100 hover:text-scale-1200 transition"
/>
)}
</div>
</li>
</ul>
<a className="px-2.5 py-1" target="_blank">
<IconGitHub size={16} className="text-scale-1100 hover:text-scale-1200 transition" />
</a>
</Link>
<div className="cursor-pointer px-2.5 py-1" onClick={() => toggleTheme()}>
{isDarkMode ? (
<IconMoon
size={16}
strokeWidth={1}
className="text-scale-1100 hover:text-scale-1200 transition"
/>
) : (
<IconSun
size={16}
strokeWidth={1}
className="text-scale-1100 hover:text-scale-1200 transition"
/>
)}
</div>
</div>
</div>
</nav>
@@ -29,7 +29,7 @@ const StepHikeCompact: FC<IStepHikeCompact> & IStepHikeCompactSubcomponents = ({
const Step: FC<IStep> = ({ children, title, step }) => {
return (
<div className="relative pb-8">
<div className="relative pb-8 [&_div]:[&_div]:last:bg-transparent">
<div
className="
absolute
+4 -1
View File
@@ -16,12 +16,13 @@ import FunctionsExamples from './FunctionsExamples'
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
import { Heading } from './CustomHTMLElements'
import DatabaseSetup from './MDX/database_setup.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 { CH } from '@code-hike/mdx/components'
import { CH } from '@code-hike/mdx/components'
import RefHeaderSection from './reference/RefHeaderSection'
// Ref version specific
@@ -58,6 +59,7 @@ const components = {
Admonition,
Button,
ButtonCard,
CH,
CodeBlock,
GlassPanel,
Link,
@@ -66,6 +68,7 @@ const components = {
FunctionsExamples,
JwtGenerator,
QuickstartIntro,
DatabaseSetup,
ProjectSetup,
SocialProviderSetup,
SocialProviderSettingsSupabase,
@@ -10,6 +10,7 @@ import { extractTsDocNode, generateParameters } from '~/lib/refGenerator/helpers
import RefDetailCollapse from '~/components/reference/RefDetailCollapse'
import { Fragment } from 'react'
import { IRefFunctionSection } from './Reference.types'
import components from '~/components'
const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
const item = props.spec.functions.find((x: any) => x.id === props.funcData.id)
@@ -38,16 +39,16 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
<header className={['prose'].join(' ')}>
{shortText && <ReactMarkdown className="text-sm">{shortText}</ReactMarkdown>}
</header>
{item.description && (
<div className="prose">
<ReactMarkdown className="text-sm">{item.description}</ReactMarkdown>
</div>
)}
{item.notes && (
<div className="prose">
<ReactMarkdown className="text-sm">{item.notes}</ReactMarkdown>
<ReactMarkdown className="text-sm" components={components}>
{item.notes}
</ReactMarkdown>
</div>
)}
{/* // parameters */}
@@ -58,18 +58,21 @@ const RefSectionHandler = (props: RefSectionHandlerProps) => {
}
const pageTitle = getPageTitle()
const section = props.sections.find((section) => section.slug === slug)
const fullTitle = `${pageTitle}${section ? ` - ${section.title}` : ''}`
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageTitle} />
<title>{fullTitle}</title>
<meta name="description" content={section?.title ?? pageTitle} />
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
<meta
name="twitter:image"
content={`https://supabase.com/docs/img/supabase-og-image.png`}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="canonical" href={`https://supabase.com${router.basePath}${router.asPath}`} />
</Head>
{props.isOldVersion && <OldVersionAlert sections={props.sections} />}
<RefSubLayout>
+1 -1
View File
@@ -10,7 +10,7 @@ const authProviders = [
authType: 'social',
},
{
name: 'Azure',
name: 'Azure (Microsoft)',
logo: '/docs/img/icons/microsoft-icon',
href: '/guides/auth/social-login/auth-azure',
official: false,
+168
View File
@@ -3,6 +3,174 @@ id: release-notes
title: Release Notes
---
## 0.11.0 - 2023-05-24
- Update dependency: postgrest-csharp@3.2.0
- General codebase and QOL improvements. Exceptions are generally thrown through `PostgrestException` now instead
of `Exception`. A `FailureHint.Reason` is provided with failures if possible to parse.
- `AddDebugListener` is now available on the client to help with debugging
- Merges [#65](https://github.com/supabase-community/postgrest-csharp/pull/65) Cleanup + Add better exception handling
- Merges [#66](https://github.com/supabase-community/postgrest-csharp/pull/66) Local test Fixes
- Fixes [#67](https://github.com/supabase-community/postgrest-csharp/issues/67) Postgrest Reference attribute is
producing StackOverflow for circular references
- Update dependency: gotrue-csharp@4.0.2
- [#58](https://github.com/supabase-community/gotrue-csharp/issues/58) - Add support for the `reauthentication` endpoint which allows for secure password changes.
- Update dependency: realtime-csharp@6.0.1
- Updates publishing action for future packages, includes README and icon.
- Merges [#28](https://github.com/supabase-community/realtime-csharp/pull/28) and [#30](https://github.com/supabase-community/realtime-csharp/pull/30)
- The realtime client now takes a "fail-fast" approach. On establishing an initial connection, client will throw
a `RealtimeException` in `ConnectAsync()` if the socket server is unreachable. After an initial connection has been
established, the **client will continue attempting reconnections indefinitely until disconnected.**
- [Major, New] C# `EventHandlers` have been changed to `delegates`. This should allow for cleaner event data access over
the previous subclassed `EventArgs` setup. Events are scoped accordingly. For example, the `RealtimeSocket` error
handlers will receive events regarding socket connectivity; whereas the `RealtimeChannel` error handlers will receive
events according to `Channel` joining/leaving/etc. This is implemented with the following methods prefixed by (
Add/Remove/Clear):
- `RealtimeBroadcast.AddBroadcastEventHandler`
- `RealtimePresence.AddPresenceEventHandler`
- `RealtimeSocket.AddStateChangedHandler`
- `RealtimeSocket.AddMessageReceivedHandler`
- `RealtimeSocket.AddHeartbeatHandler`
- `RealtimeSocket.AddErrorHandler`
- `RealtimeClient.AddDebugHandler`
- `RealtimeClient.AddStateChangedHandler`
- `RealtimeChannel.AddPostgresChangeHandler`
- `RealtimeChannel.AddMessageReceivedHandler`
- `RealtimeChannel.AddErrorHandler`
- `Push.AddMessageReceivedHandler`
- [Major, new] `ClientOptions.Logger` has been removed in favor of `Client.AddDebugHandler()` which allows for
implementing custom logging solutions if desired.
- A simple logger can be set up with the following:
```c#
client.AddDebugHandler((sender, message, exception) => Debug.WriteLine(message));
```
- [Major] `Connect()` has been marked `Obsolete` in favor of `ConnectAsync()`
- Custom reconnection logic has been removed in favor of using the built-in logic from `Websocket.Client@4.6.1`.
- Exceptions that are handled within this library have been marked as `RealtimeException`s.
- The local, docker-composed test suite has been brought back (as opposed to remotely testing on live supabase servers)
to test against.
- Comments have been added throughout the entire codebase and an `XML` file is now generated on build.
## 0.10.0 - 2023-05-14
- Changes options to require `Supabase.SupabaseOptions.SessionPersistor` from using `ISupabaseSessionHandler`
to `IGotrueSessionPersistance<Session>` (these are now synchronous operations).
- Update dependency: gotrue-csharp@4.0.1
- [#60](https://github.com/supabase-community/gotrue-csharp/pull/60) - Add interfaces, bug fixes, additional error
reason detection. Thanks [@wiverson](https://github.com/wiverson)!
- [#57](https://github.com/supabase-community/gotrue-csharp/pull/57) Refactor exceptions, code cleanup, and move to
delegate auth state changes
- Huge thank you to [@wiverson](https://github.com/wiverson) for his help on this refactor and release!
- Changes
- Exceptions have been simplified to a single `GotrueException`. A `Reason` field has been added
to `GotrueException` to clarify what happened. This should also be easier to manage as the Gotrue
server API & messages evolve.
- The session delegates for `Save`/`Load`/`Destroy` have been simplified to no longer require `async`.
- Console logging in a few places (most notable the background refresh thread) has been removed
in favor of a notification method. See `Client.AddDebugListener()` and the test cases for examples.
This will allow you to implement your own logging strategy (write to temp file, console, user visible
err console, etc).
- The client now more reliably emits AuthState changes.
- There is now a single source of truth for headers in the stateful Client - the `Options` headers.
- New feature:
- Added a `Settings` request to the stateless API only - you can now query the server instance to
determine if it's got the settings you need. This might allow for things like a visual
component in a tool to verify the GoTrue settings are working correctly, or tests that run differently
depending on the server configuration.
- Implementation notes:
- Test cases have been added to help ensure reliability of auth state change notifications
and persistence.
- Persistence is now managed via the same notifications as auth state change
## 0.9.1 - 2023-04-28
- Update dependency: gotrue-csharp@3.1.1
- Implements `SignInWithIdToken` for Apple/Google signing from LW7. A HUGE thank you
to [@wiverson](https://github.com/wiverson)!
- Update dependency: realtime-csharp@5.0.5
- Re: [#27](https://github.com/supabase-community/realtime-csharp/issues/27) `PostgresChangesOptions` was not
setting `listenType` in constructor. Thanks [@Kuffs2205](https://github.com/Kuffs2205)
- Update dependency: supabase-storage-csharp@1.2.10
- Re: [#7](https://github.com/supabase-community/storage-csharp/issues/7) Implements a `DownloadPublicFile` method.
## 0.9.0 - 2023-04-12
- Update dependency: gotrue-csharp@3.1.0
- [Minor] Implements PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather
than a `string`.
- Update dependency: supabase-storage-csharp@1.2.9
- Implements storage features from LW7:
- feat: custom file size limit and mime types at bucket
level [supabase/storage-js#151](https://github.com/supabase/storage-js/pull/151) file size and mime type
limits per bucket
- feat: quality option, image
transformation [supabase/storage-js#145](https://github.com/supabase/storage-js/pull/152) quality option for
image transformations
- feat: format option for webp
support [supabase/storage-js#142](https://github.com/supabase/storage-js/pull/142) format option for image
transformation
## 0.8.8 - 2023-03-29
- Update dependency: gotrue-csharp@3.0.6
- Supports adding `SignInOptions` (i.e. `RedirectTo`) on `OAuth Provider` SignIn requests.
## 0.8.7 - 2023-03-23
- Update dependency: realtime-csharp@5.0.4
- Re: [#26](https://github.com/supabase-community/realtime-csharp/pull/26) - Fixes Connect() not returning callback
result when the socket isn't null. Thanks [@BlueWaterCrystal](https://github.com/BlueWaterCrystal)!
## 0.8.6 - 2023-03-23
- Update dependency: supabase-storage-csharp@1.2.8
- [Merge #5](https://github.com/supabase-community/storage-csharp/pull/5) Added search string as an optional search
parameter. Thanks [@ElectroKnight22](https://github.com/ElectroKnight22)!
## 0.8.5 - 2023-03-10
- Update dependency: realtime-csharp@5.0.3
- Re: [#25](https://github.com/supabase-community/realtime-csharp/issues/25) - Support Channel being resubscribed
after having been unsubscribed, fixes rejoin timer being erroneously called on channel `Unsubscribe`.
Thanks [@Kuffs2205](https://github.com/Kuffs2205)!
## 0.8.4 - 2023-03-03
- Update dependency: supabase-storage-csharp@1.2.7
- Re: [#4](https://github.com/supabase-community/storage-csharp/issues/4) Implementation for `ClientOptions` which
supports specifying Upload, Download, and Request timeouts.
- Update dependency: realtime-csharp@5.0.2
- Re: [#24](https://github.com/supabase-community/realtime-csharp/issues/24) - Fixes join failing until reconnect
happened + adds access token push on channel join. Big thank you to [@Honeyhead](https://github.com/honeyhead) for
the help debugging and identifying!
## 0.8.3 - 2023-02-26
- Update dependency: supabase-storage-csharp@1.2.5
- Provides fix
for [supabase-community/supabase-csharp#54](https://github.com/supabase-community/supabase-csharp/issues/54) -
Dynamic headers were always being overwritten by initialized token headers, so the storage client would not
receive user's access token as expected.
- Provides fix for upload progress not reporting
in [supabase-community/storage-csharp#3](https://github.com/supabase-community/storage-csharp/issues/3)
- Update dependency: gotrue-csharp@3.0.5
- Fixes [#44](https://github.com/supabase-community/gotrue-csharp/issues/44) - refresh timer should automatically
reattempt (interval of 5s) for HTTP exceptions - gracefully exits on invalid refresh and triggers
an `AuthState.Changed` event
## 0.8.2 - 2023-02-26
- Update dependency: supabase-storage-csharp@1.2.4
- `UploadOrUpdate` now appropriately throws request exceptions
## 0.8.1 - 2023-02-06
- Update dependency: realtime-csharp@5.0.1
- Re: [#22](https://github.com/supabase-community/realtime-csharp/issues/22) - `SerializerSettings` were not being
passed to `PostgresChangesResponse` - Thanks [@Shenrak](https://github.com/Shenrak) for the help debugging!
## 0.8.0 - 2023-01-31
- Update dependency: realtime-csharp@5.0.0
@@ -41,7 +41,7 @@ _Optionally_ if you are using custom configuration with `createClient` then foll
>
<TabPanel id="1.0x" label="Before">
```ts title=src/supabaseClient.ts
```ts src/supabaseClient.ts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
schema: 'custom',
persistSession: false,
@@ -51,7 +51,7 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
</TabPanel>
<TabPanel id="2.0x" label="After">
```ts title=src/supabaseClient.ts
```ts src/supabaseClient.ts
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
db: {
schema: 'custom',
@@ -10,7 +10,7 @@ For `supabase-flutter`, you will be using the static `initialize()` method on `S
### Flutter `initialize()`
```dart title=main.dart
```dart main.dart
Future<void> main() async {
await Supabase.initialize(url: 'https://xyzcompany.supabase.co', anonKey: 'public-anon-key');
runApp(MyApp());
@@ -30,7 +30,7 @@ final supabase = Supabase.instance.client;
You can pass `headers` to initialize your Supabase client with customer headers.
Here is an example of passing a custom auth header to Supabase client.
```dart title=main.dart
```dart main.dart
Future<void> main() async {
await Supabase.initialize(
url: 'https://xyzcompany.supabase.co',
+9
View File
@@ -0,0 +1,9 @@
import type { Config } from '@jest/types'
const config: Config.InitialOptions = {
preset: 'ts-jest',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
testEnvironment: 'jsdom',
}
export default config
+4
View File
@@ -46,6 +46,10 @@ const levelsData = {
icon: '/docs/img/icons/menu/storage',
name: 'Storage',
},
ai: {
icon: '/docs/img/icons/menu/ai',
name: 'AI & Vectors',
},
supabase_cli: {
icon: '/docs/img/icons/menu/reference-cli',
name: 'Supabase CLI',
+1 -1
View File
@@ -139,7 +139,7 @@ const Layout: FC<Props> = (props) => {
<div>
<a
href={`https://github.com/supabase/supabase/edit/master/apps/docs/pages${router.asPath}.mdx`}
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200"
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200 w-fit"
>
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
</a>
+11 -1
View File
@@ -1,7 +1,9 @@
import fs from 'fs'
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
import { ICommonMarkdown } from '~/components/reference/Reference.types'
async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
@@ -31,6 +33,14 @@ async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
const fileContents = markdownExists ? fs.readFileSync(pathName, 'utf8') : ''
const { data, content } = matter(fileContents)
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
markdownContent.push({
id: section.id,
title: section.title,
@@ -41,8 +51,8 @@ async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
// MDX's available options, see the MDX docs for more info.
// https://mdxjs.com/packages/mdx/#compilefile-options
mdxOptions: {
// remarkPlugins: [[remarkCodeHike, { autoImport: false, theme }]],
useDynamicImport: true,
remarkPlugins: [[remarkCodeHike, codeHikeOptions]],
},
// Indicates whether or not to parse the frontmatter from the mdx source
})
@@ -0,0 +1,31 @@
import { Element } from 'hast'
import { hasProperty } from 'hast-util-has-property'
import { Node } from 'unist'
import { visit } from 'unist-util-visit'
export type UrlTransformFunction = (url: string, node: Element) => string
function modify(node: Element, prop: string, fn?: UrlTransformFunction) {
if (hasProperty(node, prop)) {
const property = node.properties[prop]
if (typeof property !== 'string') {
return
}
node.properties[prop] = fn?.(property, node) ?? property
}
}
/**
* Transforms every HAST element that contains a `href` or `src`.
* A `UrlTransformFunction` is called with the current URL. The
* return value from this function will be used as the replacement.
*/
export function linkTransform(fn?: UrlTransformFunction) {
return function transformer(tree: Node) {
visit(tree, 'element', (node: Element) => {
modify(node, 'href', fn)
modify(node, 'src', fn)
})
}
}
@@ -0,0 +1,105 @@
import { Content, Paragraph, Parent } from 'mdast'
import { MdxJsxFlowElement } from 'mdast-util-mdx'
import { Node } from 'unist'
import { visit } from 'unist-util-visit'
import { AdmonitionProps } from '~/components/Admonition'
/**
* Transforms an `mkdocs-material` Admonition to a Supabase Admonition.
*
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/
*/
const remarkMkDocsAdmonition = function () {
return function transformer(root: Parent) {
visit(root, 'paragraph', (paragraph: Paragraph, index: number, parent: Parent) => {
const [firstChild] = paragraph.children
if (firstChild?.type === 'text') {
const match = firstChild.value.match(/^!!! ?(.*?)\n(.*)/s)
if (!match) {
return
}
// Extract the admonition type along with the remaining text
const [, type, value] = match
// Rewrite the node's value to remove the admonition syntax
firstChild.value = value
// Extract sibling nodes that should be linked to this admonition
const siblingsToNest = extractLinkedSiblings(parent, paragraph, index)
const children: any[] = [...paragraph.children, ...siblingsToNest]
// Generate a Supabase Admonition JSX element
const admonitionElement: MdxJsxFlowElement = {
type: 'mdxJsxFlowElement',
name: 'Admonition',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'type',
value: mapAdmonitionType(type),
},
],
children,
}
// Overwrite original node with new element
parent.children.splice(index, 1, admonitionElement)
}
})
}
}
/**
* Identifies sibling nodes that should be linked to this admonition
* based on their indent level (ie. 4 spaces).
*
* Iterates through proceeding siblings until one is found that is
* not indented relative to the original node.
*
* Splices the discovered siblings out of the original parent and returns them.
*/
function extractLinkedSiblings(parent: Parent, node: Node, index: number, indentAmount = 4) {
const { column } = node.position.start
let nextSibling: Content
let i = index
do {
nextSibling = parent.children[++i]
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)
return parent.children.splice(index + 1, i - index - 1)
}
/**
* Maps `mkdocs-material` Admonition types to Supabase Admonition types.
*
* https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types
*/
function mapAdmonitionType(type: string): AdmonitionProps['type'] {
switch (type) {
case 'quote':
case 'example':
case 'note':
return 'note'
case 'tip':
return 'tip'
case 'warning':
return 'caution'
case 'failure':
case 'bug':
case 'danger':
return 'danger'
case 'abstract':
case 'question':
case 'info':
default:
return 'info'
}
}
export default remarkMkDocsAdmonition
@@ -0,0 +1,23 @@
import { Parent } from 'mdast'
/**
* Removes the top heading from a MD file if
* it is the first node and it matches `title`.
*
* Useful when rendering title separately from MD
* and you need to remove the duplicate.
*/
export function removeTitle(title: string) {
return function transformer(root: Parent) {
const [firstNode] = root.children
if (firstNode?.type === 'heading') {
const [text] = firstNode.children
if (text?.type === 'text' && text.value === title) {
// Remove this node
root.children.splice(0, 1)
}
}
}
}
+22 -19
View File
@@ -2,22 +2,18 @@
import nextMdx from '@next/mdx'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
//import theme from 'shiki/themes/nord.json' assert { type: 'json' }
import { remarkCodeHike } from '@code-hike/mdx'
import withTM from 'next-transpile-modules'
import withYaml from 'next-plugin-yaml'
import configureBundleAnalyzer from '@next/bundle-analyzer'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
const withBundleAnalyzer = configureBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
// import admonitions from 'remark-admonitions'
// import { remarkCodeHike } from '@code-hike/mdx'
// import codeHikeTheme from './codeHikeTheme.js'
/**
* Rewrites and redirects are handled by
* apps/www nextjs config
@@ -29,20 +25,18 @@ const withMDX = nextMdx({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
// [
// remarkCodeHike,
// {
// theme: codeHikeTheme,
// autoImport: false,
// lineNumbers: true,
// showCopyButton: true,
// },
// ],
[
remarkCodeHike,
{
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
},
],
remarkGfm,
],
rehypePlugins: [rehypeSlug],
// This is required for `MDXProvider` component
// providerImportSource: '@mdx-js/react',
providerImportSource: '@mdx-js/react',
},
})
@@ -62,6 +56,7 @@ const nextConfig = {
'raw.githubusercontent.com',
'weweb-changelog.ghost.io',
'img.youtube.com',
'archbee-image-uploads.s3.amazonaws.com',
],
},
experimental: {
@@ -108,7 +103,15 @@ const nextConfig = {
const configExport = () => {
const plugins = [
withTM(['ui', 'common', '@supabase/auth-helpers-nextjs']),
withTM([
'ui',
'common',
'@supabase/auth-helpers-nextjs',
'mermaid',
'mdx-mermaid',
'dayjs',
'shared-data',
]),
withMDX,
withYaml,
withBundleAnalyzer,
+24 -17
View File
@@ -9,6 +9,7 @@
"build:analyze": "ANALYZE=true next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"build:sitemap": "node ./internals/generate-sitemap.mjs",
"embeddings": "tsx scripts/search/generate-embeddings.ts",
"embeddings:refresh": "npm run embeddings -- --refresh",
@@ -44,18 +45,19 @@
"dependencies": {
"@algolia/autocomplete-js": "^1.7.2",
"@algolia/autocomplete-plugin-recent-searches": "^1.7.2",
"@code-hike/mdx": "^0.8.3",
"@docsearch/react": "^3.3.0",
"@mdx-js/loader": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5",
"@next/bundle-analyzer": "^13.4.0",
"@next/mdx": "^12.0.4",
"@next/mdx": "^12.3.2",
"@octokit/auth-app": "^4.0.9",
"@octokit/core": "^4.2.0",
"@octokit/plugin-paginate-graphql": "^2.0.1",
"@radix-ui/react-accordion": "^1.0.1",
"@radix-ui/react-accordion": "^1.1.0",
"@supabase/auth-helpers-nextjs": "^0.5.6",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/supabase-js": "^2.13.0",
"@supabase/supabase-js": "^2.23.0",
"algoliasearch": "^4.14.2",
"babel": "^6.23.0",
"clsx": "^1.2.1",
@@ -64,6 +66,7 @@
"framer-motion": "^6.5.1",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"hast-util-has-property": "^2.0.1",
"isbot": "^3.6.5",
"jsrsasign": "^10.5.26",
"lodash": "^4.17.21",
@@ -76,20 +79,19 @@
"mdx-mermaid": "2.0.0-rc3",
"mermaid": "^10.0.2",
"micromark-extension-mdxjs": "^1.0.0",
"next": "12.3.2",
"next": "^12.3.2",
"next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.1.0",
"next-mdx-toc": "^0.1.3",
"next-plugin-yaml": "^1.0.1",
"next-seo": "^5.14.1",
"next-transpile-modules": "^9.0.0",
"openai": "^3.1.0",
"react": "17.0.2",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "17.0.2",
"openai": "^3.2.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-intersection-observer": "^9.4.0",
"react-markdown": "^8.0.3",
"react-syntax-highlighter": "^15.3.1",
"react-syntax-highlighter": "^15.5.0",
"rehype-slug": "^5.1.0",
"remark": "^14.0.2",
"remark-admonitions": "^1.2.1",
@@ -100,25 +102,30 @@
"ui": "*",
"unist-builder": "^3.0.1",
"unist-util-filter": "^4.0.1",
"unist-util-visit": "^4.1.2",
"uuid": "^9.0.0",
"valtio": "^1.7.6"
"valtio": "^1.7.6",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/node": "^17.0.12",
"@types/hast": "^2.3.4",
"@types/node": "^17.0.24",
"@types/react": "17.0.39",
"@types/unist": "^2.0.6",
"@types/yargs": "^17.0.24",
"config": "*",
"dotenv": "^16.0.3",
"ejs": "^3.1.8",
"eslint": "8.9.0",
"eslint": "^8.41.0",
"globby": "^12.0.2",
"minimist": "^1.2.6",
"next-transpile-modules": "9.0.0",
"next-transpile-modules": "^9.0.0",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.0.2",
"sass": "^1.55.0",
"ts-node": "^10.9.1",
"tsconfig": "*",
"tsx": "^3.12.2",
"typescript": "^4.5.3"
"typescript": "^5.0.4"
}
}
+54 -4
View File
@@ -1,3 +1,11 @@
import '../../../packages/ui/build/css/themes/light.css'
import '../../../packages/ui/build/css/themes/dark.css'
import 'config/code-hike.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
@@ -9,10 +17,6 @@ import Favicons from '~/components/Favicons'
import SiteLayout from '~/layouts/SiteLayout'
import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { post } from '~/lib/fetchWrappers'
import '../styles/ch.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const router = useRouter()
@@ -64,6 +68,42 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
}
}, [router, handlePageTelemetry])
/**
* Save/restore scroll position when reloading or navigating back/forward.
*
* Required since scroll happens within a sub-container, not the page root.
*/
useEffect(() => {
const storageKey = 'scroll-position'
const container = document.getElementById('docs-content-container')
if (!container) {
return
}
const previousScroll = Number(sessionStorage.getItem(storageKey))
const [entry] = window.performance.getEntriesByType('navigation')
// Only restore scroll position on reload and back/forward events
if (
previousScroll &&
entry &&
isPerformanceNavigationTiming(entry) &&
['reload', 'back_forward'].includes(entry.type)
) {
container.scrollTop = previousScroll
}
const handler = () => {
// Scroll stored in session storage, so only persisted per tab
sessionStorage.setItem(storageKey, container.scrollTop.toString())
}
window.addEventListener('beforeunload', handler)
return () => window.removeEventListener('beforeunload', handler)
}, [router])
useEffect(() => {
/**
* Send page telemetry on first page load
@@ -120,4 +160,14 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
)
}
/**
* Type guard that checks if a performance entry is a
* `PerformanceNavigationTiming`.
*/
function isPerformanceNavigationTiming(
entry: PerformanceEntry
): entry is PerformanceNavigationTiming {
return entry.entryType === 'navigation'
}
export default MyApp
+149
View File
@@ -0,0 +1,149 @@
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',
}
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.
The toolkit includes:
- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector.
- A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings.
- [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings.
- Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more.
## Examples
Check out all of the AI [templates and examples](https://github.com/supabase/supabase/tree/master/examples/ai) in our GitHub repository.
<div className="grid md:grid-cols-12 gap-4 not-prose">
{examples.map((x) => (
<div className="col-span-4" key={x.href}>
<Link href={x.href} passHref>
<a>
<GlassPanel icon={'/docs/img/icons/github-icon'} hasLightIcon={true} title={x.name}>
{x.description}
</GlassPanel>
</a>
</Link>
</div>
))}
</div>
export const examples = [
{
name: 'Headless Vector Search',
description: 'A toolkit to perform vector similarity search on your knowledge base embeddings.',
href: '/guides/ai/examples/headless-vector-search',
},
{
name: 'Image Search with OpenAI CLIP',
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
href: '/guides/ai/examples/image-search-openai-clip',
},
{
name: 'Hugging Face inference',
description: 'Generate image captions using Hugging Face.',
href: '/guides/ai/examples/huggingface-image-captioning',
},
{
name: 'OpenAI completions',
description: 'Generate GPT text completions using OpenAI in Edge Functions.',
href: '/guides/ai/examples/openai',
},
{
name: 'Building ChatGPT Plugins',
description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.',
href: '/guides/ai/examples/building-chatgpt-plugins',
},
{
name: 'Vector search with Next.js and OpenAI',
description:
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
href: '/guides/ai/examples/nextjs-vector-search',
},
]
## Integrations
<div className="grid md:grid-cols-12 gap-4 not-prose">
{integrations.map((x) => (
<div className="col-span-4" key={x.href}>
<Link href={x.href} passHref>
<a>
<GlassPanel title={x.name}>{x.description}</GlassPanel>
</a>
</Link>
</div>
))}
</div>
export const integrations = [
{
name: 'OpenAI',
description:
'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.',
href: '/guides/ai/examples/building-chatgpt-plugins',
},
{
name: 'Hugging Face',
description:
"Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.",
href: '/guides/ai/hugging-face',
},
{
name: 'LangChain',
description:
'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.',
href: '/guides/ai/langchain',
},
{
name: 'LlamaIndex',
description: 'LlamaIndex is a data framework for your LLM applications.',
href: '/guides/ai/integrations/llamaindex',
},
]
## Case studies
<div className="grid md:grid-cols-12 gap-4 not-prose">
{customers.map((x) => (
<div className="col-span-4" key={x.href}>
<Link href={x.href} passHref>
<a>
<GlassPanel title={x.name}>{x.description}</GlassPanel>
</a>
</Link>
</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,79 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-choosing-instance-type',
title: 'Choosing Instance Type',
description: 'Choosing the right instance type for your workload.',
subtitle: 'Choosing the right instance type for your workload.',
sidebar_label: 'Choosing Instance Type',
}
This guide will help you choose the right instance type for your workload. We'll provide general guidance, as it is impossible to provide specific instructions for every possible use case. The goal is to give you a starting point from which you can make your own benchmarks and optimizations.
For more information about engineering at scale, see our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide.
## Simple workloads
We've run a set of benchmarks using the [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset. This dataset contains 1,000,000 embeddings for images, with each embedding being 960 dimensions.
We used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create an `inner-product` index for the embedding column. We then ran a series of queries to measure the performance of different instance types:
### Results
The number of vectors in `gist-960-angular` was cut to fit the instance size.
| Plan | CPU | Memory | Vectors | RPS | Latency Mean | Latency p95 | CPU Usage - Max % | Memory Usage - Max |
| ------ | ------ | ------ | ------- | --- | ------------ | ----------- | ----------------- | ------------------ |
| Free | 2-core | 1 GB | 30,000 | 75 | 0.065 sec | 0.088 sec | 90% | 1 GB + 100 Mb Swap |
| Small | 2-core | 2 GB | 100,000 | 78 | 0.064 sec | 0.092 sec | 80% | 1.8 GB |
| Medium | 2-core | 4 GB | 250,000 | 58 | 0.085 sec | 0.129 sec | 90% | 3.2 GB |
| Large | 2-core | 8 GB | 500,000 | 55 | 0.088 sec | 0.140 sec | 90% | 5 GB |
The full number of vectors in `gist-960-angular` dataset - 1,000,000.
| Plan | CPU | Memory | Vectors | RPS | Latency Mean | Latency p95 | CPU Usage - Max % | Memory Usage - Max |
| ---- | ------- | ------ | --------- | ---- | ------------ | ----------- | ----------------- | ------------------ |
| XL | 4-core | 16 GB | 1,000,000 | 110 | 0.046 sec | 0.070 sec | 45% | 14 GB |
| 2XL | 8-core | 32 GB | 1,000,000 | 235 | 0.083 sec | 0.136 sec | 33% | 10 GB |
| 4XL | 16-core | 64 GB | 1,000,000 | 420 | 0.071 sec | 0.106 sec | 45% | 11 GB |
| 8XL | 32-core | 128 GB | 1,000,000 | 815 | 0.072 sec | 0.106 sec | 75% | 13 GB |
| 12XL | 48-core | 192 GB | 1,000,000 | 1150 | 0.052 sec | 0.078 sec | 70% | 15.5 GB |
| 16XL | 64-core | 256 GB | 1,000,000 | 1345 | 0.072 sec | 0.106 sec | 60% | 17.5 GB |
- Lists set to `Number of vectors / 1000`
- Probes set to `10`
<Admonition type="note">
It is possible to upload more than 1,000,000 vectors to a single table if Memory allows it (for example, 2XL instance and higher). But it will affect the performance of the queries: RPS 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 instance.
</Admonition>
## 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>
<img
alt="multi database"
className="dark:hidden"
src="/docs/img/ai/instance-type/vecs-benchmark--light.png"
/>
<img
alt="multi database"
className="hidden dark:block"
src="/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.
## Future benchmarks
We'll continue to add more benchmarks on datasets consisting of different vector dimensions, number of `lists` in the index, and number of `probes` in the index. Stay tuned for more information about how it may affect the performance and precision of your queries.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+67
View File
@@ -0,0 +1,67 @@
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',
}
Embeddings are a core concept when building AI and vector applications.
## What are embeddings?
Embeddings capture the "relatedness" of text, images, video, or other types of information. This relatedness is most commonly used for:
- **Search:** how similar is a search term to a body of text?
- **Recommendations:** how similar are two products?
- **Classifications:** how do we categorize a body of text?
- **Clustering:** how do we identify trends?
Let's explore an example of text embeddings. Say we have three phrases:
1. "The cat chases the mouse"
2. "The kitten hunts rodents"
3. "I like ham sandwiches"
Your job is to group phrases with similar meaning. If you are a human, this should be obvious. Phrases 1 and 2 are almost identical, while phrase 3 has a completely different meaning.
Although phrases 1 and 2 are similar, they share no common vocabulary (besides "the"). Yet their meanings are nearly identical. How can we teach a computer that these are the same?
## Human language
Humans use words and symbols to communicate language. But words in isolation are mostly meaningless - we need to draw from shared knowledge & experience in order to make sense of them. The phrase “You should Google it” only makes sense if you know that Google is a search engine and that people have been using it as a verb.
In the same way, we need to train a neural network model to understand human language. An effective model should be trained on millions of different examples to understand what each word, phrase, sentence, or paragraph could mean in different contexts.
So how does this relate to embeddings?
## How do embeddings work?
Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this:
<img src="/docs/img/ai/vector-similarity.png" alt="Vector similarity" width="640" height="640" />
Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese).
In this example we only have 2 dimensions: the X and Y axis. In reality, we would need many more dimensions to effectively capture the complexities of human language.
## Using embeddings
Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example OpenAI's `text-embedding-ada-002` model outputs 1536 dimensions.
Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A common use case for this is search. Your process might look something like this:
1. Pre-process your knowledge base and generate embeddings for each page
2. Store your embeddings to be referenced later
3. Build a search page that prompts your user for input
4. Take user's input, generate a one-time embedding, then perform a similarity search against your pre-processed embeddings.
5. Return the most similar pages to the user
## See also
- [Structured and Unstructured embeddings](/docs/guides/ai/structured-unstructured)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,160 @@
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',
}
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.
## Simple workloads
For small workloads it's typical to store your data in a single database.
If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views):
<div>
<img
alt="single database"
className="dark:hidden"
src="/docs/img/ai/scaling/engineering-for-scale--single-database--light.png"
/>
<img
alt="single database"
className="hidden dark:block"
src="/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png"
/>
</div>
For example, with 3 collections, called `docs`, `posts`, and `images`, we could expose the "docs" inside the public schema like this:
```sql
create view public.docs as
select
id,
embedding,
metadata, # Expose the metadata as JSON
(metadata->>'url')::text as url # Extract the URL as a string
from vector
```
You can then use any of the client libraries to access your collections within your applications:
{/* prettier-ignore */}
```js
const { data, error } = await supabase
.from('docs')
.select('id, embedding, metadata')
.eq('url', '/hello-world')
```
## Enterprise workloads
As you move into production, we recommend running splitting your collections into separate projects. This is because it allows your vector stores to scale independently of your production data. Vectors typically grow faster than operational data, and they have different resource requirements. Running them on separate databases removes the single-point-of-failure.
<div>
<img
alt="With secondaries"
className="dark:hidden"
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png"
/>
<img
alt="With secondaries"
className="hidden dark:block"
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png"
/>
</div>
You can use as many secondary databases as you need to manage your collections. With this architecture, you have 2 options for accessing collections within your application:
1. Query the collections directly using Vecs.
2. Access the collections from your Primary database through a Wrapper.
You can use both of these in tandem to suit your use-case. We recommend option `1` wherever possible, as it offers the most scalability.
### Query collections using Vecs
Vecs provides methods for querying collections, either using a [cosine similarity function](https://supabase.github.io/vecs/api/#basic) or with [metadata filtering](https://supabase.github.io/vecs/api/#metadata-filtering).
```python
# cosine similarity
docs.query(query_vector=[0.4,0.5,0.6], limit=5)
# metadata filtering
docs.query(
query_vector=[0.4,0.5,0.6],
limit=5,
filters={"year": {"$eq": 2012}}, # metadata filters
)
```
### Accessing external collections using Wrappers
Supabase supports [Foreign Data Wrappers](/blog/postgres-foreign-data-wrappers-rust). Wrappers allow you connect two databases together so that you can query them over the network.
This involves 2 steps: connecting to your remote database from the primary, and creating a Foreign Table.
#### Connecting your remote database
Inside your Primary database we need to provide the credentials to access the secondary database:
```sql
create extension postgres_fdw;
create server docs_server
foreign data wrapper postgres_fdw
options (host 'db.xxx.supabase.co', port '5432', dbname 'postgres');
create user mapping for docs_user
server docs_server
options (user 'postgres', password 'password');
```
#### Create a foreign table
We can now create a foreign table to access the data in our secondary project.
```sql
create foreign table docs (
id text not null,
embedding vector(1536),
metadata jsonb,
url text
)
server docs_server
options (schema_name 'public', table_name 'docs');
```
This looks very similar to our View example above, and you can continue to use the client libraries to access your collections through the foreign table:
{/* prettier-ignore */}
```js
const { data, error } = await supabase
.from('docs')
.select('id, embedding, metadata')
.eq('url', '/hello-world')
```
### Enterprise architecture
This diagram provides an example architecture, allowing you to access the collections either with our client libraries or using Vecs. You can add as many secondary databases as you need, in this example we show one only:
<div>
<img
alt="multi database"
className="dark:hidden"
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png"
/>
<img
alt="multi database"
className="hidden dark:block"
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png"
/>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,159 @@
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',
}
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.
## What is ChatGPT Retrieval Plugin?
A [Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) is a Python project designed to inject external data into a ChatGPT conversation. It does a few things:
1. Turn documents into smaller chunks.
2. Converts chunks into embeddings using OpenAI's `text-embedding-ada-002` model.
3. Stores the embeddings into a vector database.
4. Queries the vector database for relevant documents when a question is asked.
It allows ChatGPT to dynamically pull relevant information into conversations from your data sources. This could be PDF documents, Confluence, or Notion knowledge bases.
## Example: Chat with Postgres Docs
Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation. Although ChatGPT already knows about the Postgres documentation because it is publicly available, this is a simple example which demonstrates how to work with PDF files.
This plugin requires several steps:
1. Download all the [Postgres docs as a PDF](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf)
2. Convert the docs into chunks of embedded text and store them in Supabase
3. Run our plugin locally so that we can ask questions about the Postgres docs.
We'll be saving the Postgres documentation in Postgres, and ChatGPT will be retrieving the documentation whenever a user asks a question:
<img
className="dark:hidden !m-0"
alt="diagram reference"
src="/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--light.png"
/>
<img
className="hidden dark:block !m-0"
alt="diagram reference"
src="/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--dark.png"
/>
### Step 1: Fork the ChatGPT Retrieval Plugin repository
Fork the ChatGPT Retrieval Plugin repository to your GitHub account and clone it to your local machine. Read through the `README.md` file to understand the project structure.
### Step 2: Install dependencies
Choose your desired datastore provider and remove unused dependencies from `pyproject.toml`. For this example, we'll use Supabase. And install dependencies with Poetry:
```bash
poetry install
```
### Step 3: Create a Supabase project
Create a [Supabase project](https://supabase.com/dashboard) and database by following the instructions [here](https://supabase.com/docs/guides/platform). Export the environment variables required for the retrieval plugin to work:
```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=supabase
export SUPABASE_URL=<supabase_url>
export SUPABASE_SERVICE_ROLE_KEY=<supabase_key>
```
For Postgres datastore, you'll need to export these environment variables instead:
```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=postgres
export PG_HOST=<postgres_host_url>
export PG_PASSWORD=<postgres_password>
```
### Step 4: Run Postgres Locally
To start quicker you may use Supabase CLI to spin everything up locally as it already includes pgvector from the start. Install `supabase-cli`, go to the `examples/providers` folder in the repo and run:
```bash
supabase start
```
This will pull all docker images and run supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.
Using `supabase-cli` is not required and you can use any other docker image or hosted version of PostgresDB that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.
### Step 5: Obtain OpenAI API key
To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, you just need to go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.
![OpenAI Secret Keys](/docs/img/ai/chatgpt-plugins/openai-secret-keys.png)
### Step 6: Run the plugin
Execute the following command to run the plugin:
```bash
poetry run dev
# output
INFO: Will watch for changes in these directories: ['./chatgpt-retrieval-plugin']
INFO: Uvicorn running on http://localhost:3333 (Press CTRL+C to quit)
INFO: Started reloader process [87843] using WatchFiles
INFO: Started server process [87849]
INFO: Waiting for application startup.
INFO: Application startup complete.
```
The plugin will start on your localhost - port `:3333` by default.
### Step 6: Populating data in the datastore
For this example, we'll upload Postgres documentation to the datastore. Download the [Postgres documentation](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf) and use the `/upsert-file` endpoint to upload it:
```bash
curl -X POST -F \\"file=@./postgresql-15-US.pdf\\" <http://localhost:3333/upsert-file>
```
The plugin will split your data and documents into smaller chunks automatically. You can view the chunks using the Supabase dashboard or any other SQL client you prefer. For the whole Postgres Documentation I got 7,904 records in my documents table, which is not a lot, but we can try to add index for `embedding` column to speed things up by a little. To do so, you should run the following SQL command:
```sql
create index on documents
using ivfflat (embedding vector_ip_ops)
with (lists = 10);
```
This will create an index for the inner product distance function. Important to note that it is an approximate index. It will change the logic from performing the exact nearest neighbor search to the approximate nearest neighbor search.
We are using `lists = 10`, because as a general guideline, you should start looking for optimal lists constant value with the formula: `rows / 1000` when you have less than 1 million records in your table.
### Step 7: Using our plugin within ChatGPT
To integrate our plugin with ChatGPT, register it in the ChatGPT dashboard. Assuming you have access to ChatGPT Plugins and plugin development, select the Plugins model in a new chat, then choose "Plugin store" and "Develop your own plugin." Enter `localhost:3333` into the domain input, and your plugin is now part of ChatGPT.
![ChatGPT Plugin Store](/docs/img/ai/chatgpt-plugins/chatgpt-plugin-store.png)
![ChatGPT Local Plugin](/docs/img/ai/chatgpt-plugins/chatgpt-local-plugin.png)
You can now ask questions about Postgres and receive answers derived from the documentation.
Let's try it out: ask ChatGPT to find out when to use `check` and when to use `using`. You will be able to see what queries were sent to our plugin and what it responded to.
![Ask ChatGPT](/docs/img/ai/chatgpt-plugins/ask-chatgpt.png)
And after ChatGPT receives a response from the plugin it will answer your question with the data from the documentation.
![ChatGPT Reply](/docs/img/ai/chatgpt-plugins/chatgpt-reply.png)
## Resources
- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
- ChatGTP Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,124 @@
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',
}
Supabase provides a [Headless Search Toolkit](https://github.com/supabase/headless-vector-search) for adding "Generative Q&A" to your documentation. The toolkit is "headless", so that you can integrate it into your existing website and style it to match your website theme.
You can see how this works with the Supabase docs. Just him `cmd+k` and "ask" for something like "what are the features of supabase?". You will see that the response is streamed back, using the information provided in the docs:
![headless search](/docs/img/ai/headless-search/headless.png)
## Tech stack
- Supabase: Database & Edge Functions.
- OpenAI: Embeddings and completions.
- GitHub Actions: for ingesting your markdown docs.
## Toolkit
This toolkit consists of 2 parts:
- The [Headless Vector Search](https://github.com/supabase/headless-vector-search) template which you can deploy in your own organization.
- A [GitHub Action](https://github.com/supabase/embeddings-generator) which will ingest your markdown files, convert them to embeddings, and store them in your database.
## Usage
There are 3 steps to build similarity search inside your documentation:
1. Prepare your database.
2. Ingest your documentation.
3. Add a search interface.
### 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://app.supabase.com/_/settings).
Now we can use the [Headless Vector Search](https://github.com/supabase/headless-vector-search#set-up) instructions to set up the database:
1. Clone the repo to your local machine: `git clone git@github.com:supabase/headless-vector-search.git`
2. Link the repo to your remote project: `supabase link --project-ref XXX`
3. Apply the database migrations: `supabase db push`
4. Set your OpenAI key as a secret: `supabase secrets set OPENAI_KEY=sk-xxx`
5. Deploy the Edge Functions: `supabase functions deploy --no-verify-jwt`
6. Expose `docs` schema via API in Supabase Dashboard [settings](https://app.supabase.com/project/_/settings/api) > `API Settings` > `Exposed schemas`
### Ingest your documentation
Now we need to push your documentation into the database as embeddings. You can do this manually, but to make it easier we've created a [GitHub Action](https://github.com/marketplace/actions/supabase-embeddings-generator) which can update your database every time there is a Pull Request.
In your knowledge base repository, create a new action called `.github/workflows/generate_embeddings.yml` with the following content:
```yml
name: 'generate_embeddings'
on: # run on main branch changes
push:
branches:
- main
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: supabase/supabase-embeddings-generator@v0.0.x # Update this to the latest version.
with:
supabase-url: 'https://your-project-ref.supabase.co' # Update this to your project URL.
supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
openai-key: ${{ secrets.OPENAI_KEY }}
docs-root-path: 'docs' # the path to the root of your md(x) files
```
Make sure to choose the latest version, and set your `SUPABASE_SERVICE_ROLE_KEY` and `OPENAI_KEY` as repository secrets in your repo settings (settings > secrets > actions).
### Add a search interface
Now inside your docs, you need to create a search interface. Because this is a headless interface, you can use it with any language. The only requirement is that you send the user query to the `query` Edge Function, which will stream an answer back from OpenAI. It might look something like this:
```js
const onSubmit = (e: Event) => {
e.preventDefault()
answer.value = ""
isLoading.value = true
const query = new URLSearchParams({ query: inputRef.current!.value })
const projectUrl = `https://your-project-ref.functions.supabase.co`
const queryURL = `${projectURL}/${query}`
const eventSource = new EventSource(queryURL)
eventSource.addEventListener("error", (err) => {
isLoading.value = false
console.error(err)
})
eventSource.addEventListener("message", (e: MessageEvent) => {
isLoading.value = false
if (e.data === "[DONE]") {
eventSource.close()
return
}
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
const text = completionResponse.choices[0].text
answer.value += text
});
isLoading.value = true
}
```
## Resources
- 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
@@ -0,0 +1,99 @@
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',
}
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.
## About Hugging Face
[Hugging Face](https://huggingface.co/) is the collaboration platform for the machine learning community.
[Huggingface.js](https://huggingface.co/docs/huggingface.js/index) provides a convenient way to make calls to 100,000+ Machine Learning models, making it easy to incorporate AI functionality into your [Supabase Edge Functions](https://supabase.com/edge-functions).
## Setup
- Open your Supabase project dashboard or [create a new project](https://app.supabase.com/projects).
- [Create a new bucket](https://app.supabase.com/project/_/storage/buckets) called `images`.
- Generate TypeScript types from remote Database.
- Create a new Database table called `image_caption`.
- Create `id` column of type `uuid` which references `storage.objects.id`.
- Create a `caption` column of type `text`.
- Regenerate TypeScript types to include new `image_caption` table.
- Deploy the function to Supabase: `supabase functions deploy huggingface-image-captioning`.
- Create the Database Webhook in the [Supabase Dashboard](https://app.supabase.com/project/_/database/hooks) to trigger the `huggingface-image-captioning` function anytime a record is added to the `storage.objects` table.
## Generate TypeScript Types
To generate the types.ts file for the storage and public schemas, run the following command in the terminal:
```bash
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
```
## Code
Find the complete code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/huggingface-image-captioning).
```ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.7.1'
import { Database } from './types.ts'
console.log('Hello from `huggingface-image-captioning` function!')
const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))
type SoRecord = Database['storage']['Tables']['objects']['Row']
interface WebhookPayload {
type: 'INSERT' | 'UPDATE' | 'DELETE'
table: string
record: SoRecord
schema: 'public'
old_record: null | SoRecord
}
serve(async (req) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
const supabaseAdminClient = createClient<Database>(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Construct image url from storage
const { data, error } = await supabaseAdminClient.storage
.from(soRecord.bucket_id!)
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
if (error) throw error
const { signedUrl } = data
// Run image captioning with Huggingface
const imgDesc = await hf.imageToText({
data: await (await fetch(signedUrl)).blob(),
model: 'nlpconnect/vit-gpt2-image-captioning',
})
// Store image caption in Database table
await supabaseAdminClient
.from('image_caption')
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
.throwOnError()
return new Response('ok')
})
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,177 @@
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.',
}
The [OpenAI CLIP Model](https://github.com/openai/CLIP) was trained on a variety of (image, text)-pairs. You can use the CLIP model for:
- Text-to-Image / Image-To-Text / Image-to-Image / Text-to-Text Search
- You can fine-tune it on your own image and text data with the regular SentenceTransformers training code.
[SentenceTransformers](https://www.sbert.net/examples/applications/image-search/README.html) provides models that allow you to embed images and text into the same vector space. You can use this to find similar images as well as to implement image search.
You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search#image-search-with-supabase-vector).
## Create a new Python Project with Poetry
[Poetry](https://python-poetry.org/) provides packaging and dependency management for Python. If you haven't already, install poetry via pip:
```shell
pip install poetry
```
Then initialize a new project:
```shell
poetry new image-search
```
## Setup Supabase project
If you haven't already, [install the Supabase CLI](/docs/guides/cli), then initialize Supabase in the root of your newly created poetry project:
```shell
supabase init
```
Next, start your local Supabase stack:
```shell
supabase start
```
This will start up the Supabase stack locally and print out a bunch of environtment details, including your local `DB URL`. Make a note of that for later user.
## Install the Dependencies
We will need to add the following dependencies to our project:
- [`vecs`](https://github.com/supabase/vecs#vecs): Supabase Vector Python Client.
- [`sentence-transformers`](https://huggingface.co/sentence-transformers/clip-ViT-B-32): a framework for sentence, text and image embeddings (used with OpenAI CLIP model)
- [`matplotlib`](https://matplotlib.org/): for displaying our image result
```shell
poetry add vecs sentence-transformers matplotlib
```
## Import the necessary dependencies
At the top of your main python script, import the dependencies and store your `DB URL` from above in a variable:
```python
from PIL import Image
from sentence_transformers import SentenceTransformer
import vecs
from matplotlib import pyplot as plt
from matplotlib import image as mpimg
DB_CONNECTION = "postgresql://postgres:postgres@localhost:54322/postgres"
```
## Create embeddings for your images
In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search/images) or you can find license free images on [unsplash](https://unsplash.com).
Next, create a `seed` method, which will create a new Supabase Vector Collection, generate embeddings for your images, and upsert the embeddings into your database:
```python
def seed():
# create vector store client
vx = vecs.create_client(DB_CONNECTION)
# create a collection of vectors with 3 dimensions
images = vx.create_collection(name="image_vectors", dimension=512)
# Load CLIP model
model = SentenceTransformer('clip-ViT-B-32')
# Encode an image:
img_emb1 = model.encode(Image.open('./images/one.jpg'))
img_emb2 = model.encode(Image.open('./images/two.jpg'))
img_emb3 = model.encode(Image.open('./images/three.jpg'))
img_emb4 = model.encode(Image.open('./images/four.jpg'))
# add records to the *images* collection
images.upsert(
vectors=[
(
"one.jpg", # the vector's identifier
img_emb1, # the vector. list or np.array
{"type": "jpg"} # associated metadata
), (
"two.jpg",
img_emb2,
{"type": "jpg"}
), (
"three.jpg",
img_emb3,
{"type": "jpg"}
), (
"four.jpg",
img_emb4,
{"type": "jpg"}
)
]
)
print("Inserted images")
# index the collection for fast search performance
images.create_index()
print("Created index")
```
Add this method as a script in your `pyproject.toml` file:
```toml
[tool.poetry.scripts]
seed = "image_search.main:seed"
search = "image_search.main:search"
```
After activating the virtual environtment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor), selecting the `vecs` schema, and the `image_vectors` database.
## Perform an Image Search from a Text Query
With Supabase Vector we can easily query our embeddings. We can use either an image as search input or alternative we can generate an embedding from a string input and use that as the query input:
```python
def search():
# create vector store client
vx = vecs.create_client(DB_CONNECTION)
images = vx.get_collection(name="image_vectors")
# Load CLIP model
model = SentenceTransformer('clip-ViT-B-32')
# Encode text query
query_string = "a bike in front of a red brick wall"
text_emb = model.encode(query_string)
# query the collection filtering metadata for "type" = "jpg"
results = images.query(
query_vector=text_emb, # required
limit=1, # number of records to return
filters={"type": {"$eq": "jpg"}}, # metadata filters
)
result = results[0]
print(result)
plt.title(result)
image = mpimg.imread('./images/' + result)
plt.imshow(image)
plt.show()
```
By limiting the query to one result, we can show the most relevant image to the user. Finally we use `matplotlib` to show the image result to the user.
That's it, go ahead and test it out by running `poetry run search` and you will be presented with an image of a "bike in front of a red brick wall".
## 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
@@ -2,20 +2,23 @@ import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
export const meta = {
title: 'OpenAI Embeddings & Vector Search',
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: 'OpenAI',
breadcrumb: 'AI Examples',
video: 'https://www.youtube.com/v/xmfNUCjszh4',
tocVideo: 'xmfNUCjszh4',
}
In this tutorial we'll look at how you can build a custom ChatGPT-like search experience for your own knowledge base. See our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for an example of how this will look.
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:
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project.
1. Convert your markdown into embeddings using OpenAI.
2. Store you embeddings in Postgres using pgvector.
3. Deploy a function for answering your users' questions.
<Admonition type="note">
You can find the [full example on
GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
</Admonition>
You can read our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for a full example.
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project. You can find the [full Next.js example on GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).
## Create a project
@@ -281,7 +284,7 @@ With our database set up, we need to process and store all `.mdx` files in the `
<StepHikeCompact.Code>
```txt
```bash
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
@@ -458,7 +461,7 @@ All of this is glued together in a [Vercel Edge Function](https://vercel.com/doc
In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx).
```tsx
```ts
const handleConfirm = React.useCallback(
async (query: string) => {
setAnswer(undefined)
@@ -540,5 +543,5 @@ Want to learn more about the awesome tech that is powering this?
></iframe>
</div>
export const Page = ({ children }) => <Layout meta={meta} children={children} hideToc={true} />
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -3,20 +3,35 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'examples-openai',
title: 'Generating OpenAI GPT3 completions',
description: 'Using OpenAI in Edge Functions.',
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.com/v/29p8kIqyU_Y',
tocVideo: '29p8kIqyU_Y',
}
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/29p8kIqyU_Y"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
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.
Use the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) in Supabase Edge Functions.
OpenAI's API is intended to be used from the server-side. Supabase offers Edge Functions to make it easy to interact with third party APIs like OpenAI.
## Setup Supabase project
If you haven't already, [install the Supabase CLI](/docs/guides/cli) and initialize your project:
```shell
supabase init
```
## Create edge function
Scaffold a new edge function called `openai` by running:
```shell
supabase functions new openai
```
A new edge function will now exist under `./supabase/functions/openai/index.ts`.
We'll design the function to take your user's query (via POST request) and forward it to OpenAI's API.
```ts index.ts
import 'xhr_polyfill'
@@ -31,7 +46,7 @@ serve(async (req) => {
prompt: query,
max_tokens: 256,
temperature: 0,
stream: true,
stream: false,
}
return fetch('https://api.openai.com/v1/completions', {
@@ -45,12 +60,28 @@ serve(async (req) => {
})
```
Note that we are setting `stream` to `false` which will wait until the entire response is complete before returning. If you wish to stream GPT's response word-by-word back to your client, set `stream` to `true`.
## Create OpenAI key
You may have noticed we were passing `OPENAI_API_KEY` in the Authorization header to OpenAI. To generate this key, go to https://platform.openai.com/account/api-keys and create a new secret key.
After getting the key, copy it into a new file called `.env.local` in your `./supabase` folder:
```
OPENAI_API_KEY=your-key-here
```
## Run locally
Serve the edge function locally by running:
```bash
supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt
```
Notice how we are passing in the `.env.local` file.
Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/openai.
```bash
@@ -59,8 +90,12 @@ curl -i --location --request POST http://localhost:54321/functions/v1/openai \
--data '{"query":"What is Supabase?"}'
```
You should see a GPT response come back from OpenAI!
## Deploy
Deploy your function to the cloud by runnning:
```bash
supabase functions deploy --no-verify-jwt openai
supabase secrets set --env-file ./supabase/.env.local
+119
View File
@@ -0,0 +1,119 @@
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',
}
<a
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
</a>
Google Colab is a hosted Jupyter Notebook service. It provides free access to computing resources, including GPUs and TPUs, and is well-suited to machine learning, data science, and education. We can use Colab to manage collections using [Supabase Vecs](/docs/guides/ai/vecs-python-client).
In this tutorial we'll connect to a database running on the Supabase [platform](https://app.supabase.com/). If you don't already have a database, you can create one here: [database.new](https://database.new).
## Create a new notebook
Start by visiting [colab.research.google.com](https://colab.research.google.com/). There you can create a new notebook.
![Google Colab new notebook](/docs/img/ai/google-colab/colab-new.png)
## Install Vecs
We'll use the Supabase Vector client, [Vecs](/docs/guides/ai/vecs-python-client), to manage our collections.
At the top of the notebook add the notebook paste the following code and hit the "execute" button (`ctrl+enter`):
```py
pip install vecs
```
![Install vecs](/docs/img/ai/google-colab/install-vecs.png)
## Connect to your database
Find the Postgres connection string for your Supabase project in the [database settings](https://app.supabase.com/_/settings/database) of the dashboard. Copy the "URI" format, which should look something like `postgresql:/postgres:<password>@<host>:5432/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:
```py
import vecs
DB_CONNECTION = "postgresql://postgres:<password>@<host>:5432/postgres"
# create vector store client
vx = vecs.create_client(DB_CONNECTION)
```
Execute the code block (`ctrl+enter`). If no errors were returned then your connection was successful.
## Create a collection
Now we're going to create a new collection and insert some documents.
Create a new code block below the install block (`ctrl+m b`). Add the following code to the code block and execute it (`ctrl+enter`):
```py
collection = vx.create_collection(name="colab_collection", dimension=3)
collection.upsert(
vectors=[
(
"vec0", # the vector's identifier
[0.1, 0.2, 0.3], # the vector. list or np.array
{"year": 1973} # associated metadata
),
(
"vec1",
[0.7, 0.8, 0.9],
{"year": 2012}
)
]
)
```
This will create a table inside your database within the `vecs` schema, called `colab_collection`. You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
![Colab documents](/docs/img/ai/google-colab/colab-documents.png)
## Query your documents
Now we can search for documents based on their similarity. Create a new code block and execute the following code:
```py
collection.query(
query_vector=[0.4,0.5,0.6], # required
limit=5, # number of records to return
filters={}, # metadata filters
measure="cosine_distance", # distance measure to use
include_value=False, # should distance measure values be returned?
include_metadata=False, # should record metadata be returned?
)
```
You will see that this returns two documents in an array `['vec1', 'vec0']`:
![Colab results](/docs/img/ai/google-colab/colab-results.png)
It also returns a warning:
```
Query does not have a covering index for cosine_distance.
```
You can lean more about creating indexes in the [Vecs documentation](https://supabase.github.io/vecs/api/#create-an-index).
## 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
+172
View File
@@ -0,0 +1,172 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-hugging-face',
title: 'Hugging Face',
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.
Hugging Face exposes an [Inference API](https://huggingface.co/inference-api) you can use to execute AI tasks remotely on Hugging Face servers. This opens the doors to using Hugging Face with languages like TypeScript and can be deployed using [Edge Functions](/docs/guides/functions).
## AI Tasks
Below are some of the types of tasks you can perform with Hugging Face:
### Natural language
- [Summarization](https://huggingface.co/tasks/summarization)
- [Text classification](https://huggingface.co/tasks/text-classification)
- [Text generation](https://huggingface.co/tasks/text-generation)
- [Translation](https://huggingface.co/tasks/translation)
- [Fill in the blank](https://huggingface.co/tasks/fill-mask)
### Computer Vision
- [Image to text](https://huggingface.co/tasks/image-to-text)
- [Text to image](https://huggingface.co/tasks/text-to-image)
- [Image classification](https://huggingface.co/tasks/image-classification)
- [Video classification](https://huggingface.co/tasks/video-classification)
- [Object detection](https://huggingface.co/tasks/object-detection)
- [Image segmentation](https://huggingface.co/tasks/image-segmentation)
### Audio
- [Text to speech](https://huggingface.co/tasks/text-to-speech)
- [Speech to text](https://huggingface.co/tasks/automatic-speech-recognition)
- [Audio classification](https://huggingface.co/tasks/audio-classification)
See a [full list of tasks](https://huggingface.co/tasks).
## Access token
First generate a Hugging Face access token for your app:
https://huggingface.co/settings/tokens
Name your token based on the app its being used for and the environment. For example, if you are building an image generation app you might create 2 tokens:
- "My Image Generator (Dev)"
- "My Image Generator (Prod)"
Since we will be using this token for the inference API, choose the `read` role.
<Admonition type="info">
Though it is possible to use the Hugging Face inference API today without an access token, [you may be rate limited](https://huggingface.co/docs/huggingface.js/inference/README#usage).
To ensure you don't experience any unexpected downtime or errors, we recommend creating an access token.
</Admonition>
## Edge Functions
Edge Functions are server-side TypeScript functions that run on-demand. Since Edge Functions run on a server, you can safely give them access to your Hugging Face access token.
<Admonition type="info">
You will need the `supabase` CLI [installed](/docs/guides/cli) for the following commands to work.
</Admonition>
To create a new Edge Function, navigate to your local project and initialize Supabase if you haven't already:
```shell
supabase init
```
Then create an Edge Function:
```shell
supabase functions new text-to-image
```
Create a file called `.env.local` to store your Hugging Face access token:
```shell
HUGGING_FACE_ACCESS_TOKEN=<your-token-here>
```
Let's modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request:
```ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
const hf = new HfInference(Deno.env.get('HUGGING_FACE_ACCESS_TOKEN'))
serve(async (req) => {
const { prompt } = await req.json()
const image = await hf.textToImage(
{
inputs: prompt,
model: 'stabilityai/stable-diffusion-2',
},
{
use_cache: false,
}
)
return new Response(image)
})
```
1. This function creates a new instance of `HfInference` using the `HUGGING_FACE_ACCESS_TOKEN` environment variable.
1. It expects a POST request that includes a JSON request body. The JSON body should include a parameter called `prompt` that represents the text-to-image prompt that we will pass to Hugging Face's inference API.
1. Next we call `textToImage()`, passing in the user's prompt along with the model that we would like to use for the image generation. Today Hugging Face recommends `stabilityai/stable-diffusion-2`, but you can change this to any other text-to-image model. You can see a list of which models are supported for each task by navigating to their [models page](https://huggingface.co/models?pipeline_tag=text-to-image) and filtering by task.
1. We set `use_cache` to `false` so that repeat queries with the same prompt will produce new images. If the task and model you are using is deterministic (will always produce the same result based on the same input), consider setting `use_cache` to `true` for faster responses.
1. The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`.
Finally let's serve the Edge Function locally to test it:
```shell
supabase functions serve --env-file .env.local --no-verify-jwt
```
Remember to pass in the `.env.local` file using the `--env-file` parameter so that the Edge Function can access the `HUGGING_FACE_ACCESS_TOKEN`.
<Admonition type="info">
For demo purposes we set `--no-verify-jwt` to make it easy to test the Edge Function without passing in a JWT token. In a real application you will need to pass the JWT as a `Bearer` token in the `Authorization` header.
</Admonition>
At this point, you can make an API request to your Edge Function using your preferred frontend framework (Next.js, React, Expo, etc). We can also test from the terminal using `curl`:
```shell
curl --output result.jpg --location --request POST 'http://localhost:54321/functions/v1/text-to-image' \
--header 'Content-Type: application/json' \
--data '{"query":"Llama wearing sunglasses"}'
```
In this example, your generated image will save to `result.jpg`:
<img
src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
alt="Llama wearing sunglasses example"
width="400"
height="400"
/>
## Next steps
You can now create an Edge Function that invokes a Hugging Face task using your model of choice.
Try running some other [AI tasks](#ai-tasks).
## Resources
- 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
@@ -0,0 +1,66 @@
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',
}
This guide will walk you through a basic example using the LlamaIndex [SupabaseVectorStore](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb).
<DatabaseSetup />
## Launching a notebook
Launch our [LlamaIndex](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb) notebook in Colab:
<a
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
## Fill in your OpenAI credentials
Inside the Notebook, add your `OPENAI_API_KEY` key. Find the cell which contains this code:
```py
import os
os.environ['OPENAI_API_KEY'] = "[your_openai_api_key]"
```
## Connecting to your database
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
```python
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
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.
## Stepping through the notebook
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
![Colab documents](/docs/img/ai/google-colab/colab-documents.png)
## Resources
- 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
+215
View File
@@ -0,0 +1,215 @@
import Layout from '~/layouts/DefaultGuideLayout'
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',
}
[LangChain](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.
## Initializing your database
Prepare you database with the relevant tables:
```sql
-- Enable the pgvector extension to work with embedding vectors
create extension vector;
-- Create a table to store your documents
create table documents (
id bigserial primary key,
content text, -- corresponds to Document.pageContent
metadata jsonb, -- corresponds to Document.metadata
embedding vector(1536) -- 1536 works for OpenAI embeddings, change if needed
);
-- Create a function to search for documents
create function match_documents (
query_embedding vector(1536),
match_count int default null,
filter jsonb DEFAULT '{}'
) returns table (
id bigint,
content text,
metadata jsonb,
similarity float
)
language plpgsql
as $$
#variable_conflict use_column
begin
return query
select
id,
content,
metadata,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where metadata @> filter
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;
```
## Usage
You can now search your documents using any Node.js application. This is intended to be run on a secure server route.
```js
import { SupabaseVectorStore } from 'langchain/vectorstores/supabase'
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
import { createClient } from '@supabase/supabase-js'
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseKey) throw new Error(`Expected SUPABASE_SERVICE_ROLE_KEY`)
const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
export const run = async () => {
const client = createClient(url, supabaseKey)
const vectorStore = await SupabaseVectorStore.fromTexts(
['Hello world', 'Bye bye', "What's this?"],
[{ id: 2 }, { id: 1 }, { id: 3 }],
new OpenAIEmbeddings(),
{
client,
tableName: 'documents',
queryName: 'match_documents',
}
)
const resultOne = await vectorStore.similaritySearch('Hello world', 1)
console.log(resultOne)
}
```
### Simple Metadata Filtering
Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information.
```js
import { SupabaseVectorStore } from 'langchain/vectorstores/supabase'
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
import { createClient } from '@supabase/supabase-js'
// First, follow set-up instructions above
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
export const run = async () => {
const client = createClient(url, privateKey)
const vectorStore = await SupabaseVectorStore.fromTexts(
['Hello world', 'Hello world', 'Hello world'],
[{ user_id: 2 }, { user_id: 1 }, { user_id: 3 }],
new OpenAIEmbeddings(),
{
client,
tableName: 'documents',
queryName: 'match_documents',
}
)
const result = await vectorStore.similaritySearch('Hello world', 1, {
user_id: 3,
})
console.log(result)
}
```
### Advanced Metadata Filtering
You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](https://supabase.com/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [Postgrest API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).
```js
import { SupabaseFilterRPCCall, SupabaseVectorStore } from 'langchain/vectorstores/supabase'
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
import { createClient } from '@supabase/supabase-js'
// First, follow set-up instructions above
const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)
const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)
export const run = async () => {
const client = createClient(url, privateKey)
const embeddings = new OpenAIEmbeddings()
const store = new SupabaseVectorStore(embeddings, {
client,
tableName: 'documents',
})
const docs = [
{
pageContent:
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to expand upon the notion of quantum fluff, a theorectical concept where subatomic particles coalesce to form transient multidimensional spaces. Yet, this abstraction holds no real-world application or comprehensible meaning, reflecting a cosmic puzzle.',
metadata: { b: 1, c: 10, stuff: 'right' },
},
{
pageContent:
'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to proceed by discussing the echo of virtual tweets in the binary corridors of the digital universe. Each tweet, like a pixelated canary, hums in an unseen frequency, a fascinatingly perplexing phenomenon that, while conjuring vivid imagery, lacks any concrete implication or real-world relevance, portraying a paradox of multidimensional spaces in the age of cyber folklore.',
metadata: { b: 2, c: 9, stuff: 'right' },
},
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'right' } },
{ pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'wrong' } },
{ pageContent: 'hi', metadata: { b: 2, c: 8, stuff: 'right' } },
{ pageContent: 'bye', metadata: { b: 3, c: 7, stuff: 'right' } },
{ pageContent: "what's this", metadata: { b: 4, c: 6, stuff: 'right' } },
]
await store.addDocuments(docs)
const funcFilterA: SupabaseFilterRPCCall = (rpc) =>
rpc
.filter('metadata->b::int', 'lt', 3)
.filter('metadata->c::int', 'gt', 7)
.textSearch('content', `'multidimensional' & 'spaces'`, {
config: 'english',
})
const resultA = await store.similaritySearch('quantum', 4, funcFilterA)
const funcFilterB: SupabaseFilterRPCCall = (rpc) =>
rpc
.filter('metadata->b::int', 'lt', 3)
.filter('metadata->c::int', 'gt', 7)
.filter('metadata->>stuff', 'eq', 'right')
const resultB = await store.similaritySearch('hello', 2, funcFilterB)
console.log(resultA, resultB)
}
```
## Hybrid search
LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search).
## Resources
- 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
@@ -0,0 +1,156 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-collections',
title: 'Managing collections',
description: 'Learn how to manage groups of vector records using the vecs Python library',
sidebar_label: 'Managing collections',
}
A collection is an group of vector records managed by the `vecs` Python library. Records can be added to or updated in a collection. Collections can be queried at any time, but should be indexed for scalable query performance.
Supabase provides a [Python client](/docs/guides/ai/vecs-python-client) called `vecs` for managing unstructured vector stores in Postgres. If you come from a data science background, this unstructured data approach will feel familiar. If you are more interested in a structured data approach, see [Vector columns](/docs/guides/ai/vector-columns) or read our guide on [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured-embeddings).
Under the hood `vecs` will manage the necessary Postgres tables and columns to store and query your collections.
## API
Find the full API in the [official API docs](https://supabase.github.io/vecs/api).
### Connecting
Before you can interact with vecs, create the client to communicate with Postgres.
```python
import vecs
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
vx = vecs.create_client(DB_CONNECTION)
```
### Create collection
You can create a collection to store vectors specifying the collections name and the number of dimensions in the vectors you intend to store.
```python
docs = vx.create_collection(name="docs", dimension=3)
```
If another collection exists with the same name,
### Get an existing collection
To access a previously created collection, use `get_collection` to retrieve it by name
```python
docs = vx.get_collection(name="docs")
```
### Upserting vectors
`vecs` combines the concepts of "insert" and "update" into "upsert". Upserting records adds them to the collection if the `id` is not present, or updates the existing record if the `id` does exist.
```python
# add records to the collection
docs.upsert(
vectors=[
(
"vec0", # the vector's identifier
[0.1, 0.2, 0.3], # the vector. list or np.array
{"year": 1973} # associated metadata
),
(
"vec1",
[0.7, 0.8, 0.9],
{"year": 2012}
)
]
)
```
### Create an index
Collections can be queried immediately after being created.
However, for good performance, the collection should be indexed after records have been upserted.
Indexes should be created **after** the collection has been populated with records. Building an index on an empty collection will result in significantly reduced recall. Once the index has been created you can still upsert new documents into the collection but you should rebuild the index if the size of the collection more than doubles.
Only one index may exist per-collection. By default, creating an index will replace any existing index.
To create an index:
```python
##
# INSERT RECORDS HERE
##
# index the collection to be queried by cosine distance
docs.create_index(measure=vecs.IndexMeasure.cosine_distance)
```
Available options for query `measure` are:
- `vecs.IndexMeasure.cosine_distance`
- `vecs.IndexMeasure.l2_distance`
- `vecs.IndexMeasure.max_inner_product`
which correspond to different methods for comparing query vectors to the vectors in the database.
If you aren't sure which to use, stick with the default (cosine_distance) by omitting the parameter i.e.: `docs.create_index()`.
<Admonition type="note">
The time required to create an index grows with the number of records and size of vectors. For a few thousand records expect sub-minute a response in under a minute. It may take a few minutes for larger collections.
</Admonition>
For an in-depth guide on vector indexes, see [Managing indexes](/docs/guides/ai/managing-indexes).
### Query
Be aware that indexes are essential for good performance. If you do not create an index, every query will return a warning that includes the `IndexMeasure` you should index.
#### Basic
The simplest form of search is to provide a query vector.
```python
docs.query(
query_vector=[0.4,0.5,0.6], # required
limit=5, # number of records to return
filters={}, # metadata filters
measure="cosine_distance", # distance measure to use
include_value=False, # should distance measure values be returned?
include_metadata=False, # should record metadata be returned?
)
```
Which returns a list of vector record `ids`.
#### Metadata Filtering
The metadata that is associated with each record can also be filtered during a query.
As an example, `{"year": {"$eq": 2005}}` filters a `year` metadata key to be equal to 2005
In context:
```python
docs.query(
query_vector=[0.4,0.5,0.6],
filters={"year": {"$eq": 2012}}, # metadata filters
)
```
For a complete reference, see the [metadata guide](https://supabase.github.io/vecs/concepts_metadata/).
## Resources
- 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
@@ -0,0 +1,95 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-managing-indexes',
title: 'Managing indexes',
description: 'Understanding vector indexes',
sidebar_label: 'Managing 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.
## IVFFlat indexes
Today `pgvector` indexes use an algorithm called IVFFlat. IVF stands for 'inverted file indexes'. It works by clustering your vectors in order to reduce the similarity search scope. Rather than comparing a vector to every other vector, the vector is only compared against vectors within the same cell cluster (or nearby clusters, depending on your configuration).
### Inverted lists (cell clusters)
When you create the index, you choose the number of inverted lists (cell clusters). Increase this number to speed up queries, but at the expense of recall.
For example, to create an index with 100 lists on a column that uses the cosine operator:
```sql
create index on items using ivfflat (column_name vector_cosine_ops) with (lists = 100);
```
For more info on the different operators, see [Distance operations](#distance-operators).
For every query, you can set the number of probes (1 by default). The number of probes corresponds to the number of nearby cells to probe for a match. Increase this for better recall at the expense of speed.
To set the number of probes for the duration of the session run:
```sql
set ivfflat.probes = 10;
```
To set the number of probes only for the current transaction run:
```sql
begin;
set local ivfflat.probes = 10;
select ...
commit;
```
If the number of probes is the same as the number of lists, exact nearest neighbor search will be performed and the planner won't use the index.
### Approximate nearest neighbor
One important note with IVF indexes is that nearest neighbor search is approximate, since exact search on high dimensional data can't be indexed efficiently. This means that similarity results will change (slightly) after you add an index (trading recall for speed).
## Distance operators
The type of index required depends on the distance operator you are using. `pgvector` includes 3 distance operators:
| Operator | Description | [**Operator class**](https://www.postgresql.org/docs/current/sql-createopclass.html) |
| -------- | ---------------------- | ------------------------------------------------------------------------------------ |
| `<->` | Euclidean distance | `vector_l2_ops` |
| `<#>` | negative inner product | `vector_ip_ops` |
| `<=>` | cosine distance | `vector_cosine_ops` |
Use the following SQL commands to create an index for the operator(s) used in your queries.
### Euclidean L2 distance (`vector_l2_ops`)
```sql
create index on items using ivfflat (column_name vector_l2_ops) with (lists = 100);
```
### Inner product (`vector_ip_ops`)
```sql
create index on items using ivfflat (column_name vector_ip_ops) with (lists = 100);
```
### Cosine distance (`vector_cosine_ops`)
```sql
create index on items using ivfflat (column_name vector_cosine_ops) with (lists = 100);
```
Currently vectors with up to 2,000 dimensions can be indexed.
If you are using the `vecs` Python library, follow the instructions in [Managing collections](/docs/guides/ai/managing-collections#create-an-index) to create indexes.
## When should you add indexes?
`pgvector` recommends adding indexes only after the table has sufficient data, so that the internal IVFFlat cell clusters are based on your data's distribution. Anytime the distribution changes significantly, consider recreating indexes.
## 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
+156
View File
@@ -0,0 +1,156 @@
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import { GetStaticPaths, GetStaticProps } from 'next'
import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize'
import { relative } from 'path'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
import components from '~/components'
import Layout from '~/layouts/DefaultGuideLayout'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'vecs'
const branch = 'main'
const docsDir = 'docs'
const externalSite = 'https://supabase.github.io/vecs'
// Each external docs page is mapped to a local page
const pageMap = [
{
slug: 'api',
meta: {
title: 'API',
},
remoteFile: 'api.md',
},
{
slug: 'collections',
meta: {
title: 'Collections',
},
remoteFile: 'concepts_collections.md',
},
{
slug: 'indexes',
meta: {
title: 'Indexes',
},
remoteFile: 'concepts_indexes.md',
},
{
slug: 'metadata',
meta: {
title: 'Metadata',
},
remoteFile: 'concepts_metadata.md',
},
]
interface PythonClientDocsProps {
source: MDXRemoteSerializeResult
meta: {
title: string
description?: string
}
}
export default function PythonClientDocs({ source, meta }: PythonClientDocsProps) {
return (
<Layout meta={meta}>
<MDXRemote {...source} components={components} />
</Layout>
)
}
/**
* Fetch markdown from external repo and transform links
*/
export const getStaticProps: GetStaticProps<PythonClientDocsProps> = async ({ params }) => {
const page = pageMap.find(({ slug }) => slug === params.slug)
if (!page) {
throw new Error(`No page mapping found for slug '${params.slug}'`)
}
const { remoteFile, meta } = page
const response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const source = await response.text()
const urlTransform: UrlTransformFunction = (url) => {
try {
const externalSiteUrl = new URL(externalSite)
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const relativePage = (
pathname.endsWith('.md')
? pathname.replace(/\.md$/, '')
: relative(externalSiteUrl.pathname, pathname)
).replace(/^\//, '')
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
// If we have a mapping for this page, use the mapped path
if (page) {
return page.slug + hash
}
// If we don't have this page in our docs, link to original docs
return `${externalSite}/${relativePage}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
const mdxSource = await serialize(source, {
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [
remarkGfm,
remarkMkDocsAdmonition,
[removeTitle, meta.title],
[remarkCodeHike, codeHikeOptions],
],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
})
return { props: { source: mdxSource, meta } }
}
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: pageMap.map(({ slug }) => ({
params: {
slug,
},
})),
fallback: false,
}
}
@@ -0,0 +1,62 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'ai-vecs-python-client',
title: 'Face similarity search',
subtitle: 'Identify the celebrities you looks most similar to using Supabase Vecs.',
breadcrumb: 'AI Quickstarts',
}
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'll identify the celebrities you (or any other person) looks most similar to. You will:
1. Launch a Postgres database that uses pgvector to store embeddings
1. Launch a notebook that connects to your database
1. Load the "`ashraq/tmdb-people-image`" celebrity dataset
1. Use the `face_recognition` model to create an embedding for every celebrity photo.
1. Search for similar faces inside the dataset.
<DatabaseSetup />
## Launching a notebook
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) notebook in Colab:
<a
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
## Connecting to your database
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
```python
import vecs
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
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.
## Stepping through the notebook
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
![Colab documents](/docs/img/ai/google-colab/colab-documents.png)
## 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
@@ -0,0 +1,62 @@
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',
}
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:
1. Launch a Postgres database that uses pgvector to store embeddings
1. Launch a notebook that connects to your database
1. Create a vector collection
1. Add data to the collection
1. Query the collection
<DatabaseSetup />
## Launching a notebook
Launch our [`vector_hello_world`](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) notebook in Colab:
<a
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
## Connecting to your database
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
```python
import vecs
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
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.
## Stepping through the notebook
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
![Colab documents](/docs/img/ai/google-colab/colab-documents.png)
## 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
@@ -0,0 +1,65 @@
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',
}
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:
1. Launch a Postgres database that uses pgvector to store embeddings
1. Launch a notebook that connects to your database
1. Load the IMDB dataset
1. Use the `sentence-transformers/all-MiniLM-L6-v2` model to create an embedding representing the semantic meaning of each review.
1. Search for all duplicates.
<DatabaseSetup />
## Launching a notebook
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) notebook in Colab:
<a
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
## Connecting to your database
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
```python
import vecs
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
# create vector store client
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.
## Stepping through the notebook
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
![Colab documents](/docs/img/ai/google-colab/colab-documents.png)
<HuggingFaceDeployment />
## 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
@@ -0,0 +1,114 @@
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',
}
Most vector stores treat metadata associated with embeddings like NoSQL, unstructured data. Supabase is flexible enough to store unstructured and structured metadata.
## Structured
```sql
create table docs (
id uuid primary key,
embedding vector(3),
content text,
url string
);
insert into docs
(id, content, url, embedding)
values
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', array[0.1, 0.2, 0.3], 'Hello world', '/hello-world');
```
Notice that we've associated two pieces of metadata, `content` and `url`, with the embedding. Those fields can be filtered, constrained, indexed, and generally operated on using the full power of SQL. Structured metadata fits naturally with a traditional Supabase application, and can be managed via database [migrations](/docs/guides/getting-started/local-development#database-migrations).
## Unstructured
```sql
create table docs (
id uuid primary key,
embedding vector(3),
meta jsonb
);
insert into docs
(id, embedding, meta)
values
(
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
array[0.1, 0.2, 0.3],
'{"content": "Hello world", "url": "/hello-world"}'
);
```
An unstructured approach does not specify the metadata fields that are expected. It stores all metadata in a flexible `json`/`jsonb` column. The tradeoff is that the querying/filtering capabilities of a schemaless data type are less flexible than when each field has a dedicated column. It also pushes the burden of metadata data integrity onto application code, which is more error prone than enforcing constraints in the database.
The unstructured approach is recommended:
- for ephemeral/interactive workloads e.g. data science or scientific research
- when metadata fields are user-defined or unknown
- during rapid prototyping
Client libraries like python's [vecs](https://github.com/supabase/vecs) use this structure. For example, running:
```py
#!/usr/bin/env python3
import vecs
docs = vx.create_collection(name="docs", dimension=1536)
docs.upsert(vectors=[
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', [100, 200, 300], { url: '/hello-world' })
])
```
automatically creates the unstructured SQL table during the call to `create_collection`.
Note that when working with client libraries that emit SQL DDL, like `create table ...`, you should add that SQL to your migrations when moving to production to maintain a single source of truth for your database's schema.
## Hybrid
The structured metadata style is recommended when the fields being tracked are known in advance. If you have a combination of known and unknown metadata fields, you can accommodate the unknown fields by adding a `json`/`jsonb` column to the table. In that situation, known fields should continue to use dedicated columns for best query performance and throughput.
```sql
create table docs (
id uuid primary key,
embedding vector(3),
content text,
url string,
meta jsonb
);
insert into docs
(id, embedding, meta)
values
(
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
array[0.1, 0.2, 0.3],
'Hello world',
'/hello-world',
'{"key": "value"}'
);
```
## Choosing the right model
Both approaches create a table where you can store your embeddings and some metadata. You should choose the best approach for your use-case. In summary:
- Structured metadata is best when fields are known in advance or query patterns are predictable e.g. a production Supabase application
- 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
@@ -0,0 +1,91 @@
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',
}
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.
## Quick start
Let's see how Vecs works using a local database. Make sure you have the Supabase CLI [installed](/docs/guides/cli#installation) on your machine.
### Initialize your project
Start a local Postgres instance in any folder using the `init` and `start` commands. Make sure you have Docker running!
```bash
# Initialize your project
supabase init
# Start Postgres
supabase start
```
### Create a collection
Inside a Python shell, run the following commands to create a new collection called "docs", with 3 dimensions.
```py
import vecs
# create vector store client
vx = vecs.create_client("postgresql://postgres:postgres@localhost:54322/postgres")
# create a collection of vectors with 3 dimensions
docs = vx.create_collection(name="docs", dimension=3)
```
### Add embeddings
Now we can insert some embeddings into our "docs" collection using the `usert()` command:
```py
import vecs
# create vector store client
docs = vecs.get_collection(name="docs")
# a collection of vectors with 3 dimensions
vectors=[
("vec0", [0.1, 0.2, 0.3], {"year": 1973}),
("vec1", [0.7, 0.8, 0.9], {"year": 2012})
]
# insert our vectors
docs.upsert(vectors=vectors)
```
### Query the collection
You can now query the collection to retrieve a relevant match:
```py
import vecs
docs = vecs.get_collection(name="docs")
# query the collection filtering metadata for "year" = 2012
docs.query(
query_vector=[0.4,0.5,0.6], # required
limit=1, # number of records to return
filters={"year": {"$eq": 2012}}, # metadata filters
)
```
## Deep Dive
For a more in-depth guide on `vecs` collections, see [Managing collections](/docs/guides/ai/managing-collections).
## Resources
- 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
@@ -0,0 +1,153 @@
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',
}
Supabase offers a number of different ways to store and query vectors within Postgres. If you prefer to use Python to store and query your vectors using collections, see [Managing collections](/docs/guides/ai/managing-collections). If you want more control over vectors within your own Postgres tables or would like to interact with them using a different language like JavaScript, keep reading.
Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a PostgreSQL extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings).
## Usage
### Enable the extension
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to the [Database](https://app.supabase.com/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "vector" and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
```sql
-- Example: enable the "vector" extension.
create extension vector
with
schema extensions;
-- Example: disable the "vector" extension
drop
extension if exists vector;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension, call `drop extension`.
</TabPanel>
</Tabs>
### Create a table to store vectors
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parenthesis) represents the number of dimensions stored in that vector.
```sql
create table documents (
id serial primary key,
title text not null,
body text not null,
embedding vector(1536)
);
```
In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 1536 dimensions. Change this to the number of dimensions used in your vector application. For example, if you are generating embeddings using OpenAI's `text-embeddings-ada-002` model, you would set this number as 1536 since that model produces 1536 dimensions.
### Storing a vector / embedding
In this example we'll generate a vector using the OpenAI API client, then store it in the database using the Supabase JavaScript client.
```js
const title = 'First post!'
const body = 'Hello world!'
// Generate a vector using OpenAI
const embeddingResponse = await openai.createEmbedding({
model: 'text-embedding-ada-002',
input: body,
})
const [{ embedding }] = embeddingResponse.data.data
// Store the vector in Postgres
const { data, error } = await supabase.from('documents').insert({
title,
body,
embedding,
})
```
This example uses the JavaScript Supabase client, but you can modify it to work with any [supported language library](/docs#client-libraries).
### Querying a vector / embedding
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for performing similarity search:
| Operator | Description |
| -------- | ---------------------- |
| `<->` | Euclidean distance |
| `<#>` | negative inner product |
| `<=>` | cosine distance |
Choosing the right operator depends on your needs. If you are searching over OpenAI embeddings, OpenAI recommends using cosine similarity. For more information on how embeddings work and how they relate to each other, see [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
Supabase client libraries like `supabase-js` connect to your Postgres instance via [PostgREST](docs/guides/getting-started/architecture#postgrest-api). PostgREST does not currently support `pgvector` similarity operators, so we'll need to wrap our query in a Postgres function and call it via the `rpc()` method:
```sql
create or replace function match_documents (
query_embedding vector(1536),
match_threshold float,
match_count int
)
returns table (
id bigint,
content text,
similarity float
)
language sql stable
as $$
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where 1 - (documents.embedding <=> query_embedding) > match_threshold
order by similarity desc
limit match_count;
$$;
```
This function takes a `query_embedding` argument and compares it to all other embeddings in the `documents` table. Each comparison returns a similarity score. If the similarity is greater than the `match_threshold` argument, it is returned. The number of rows returned is limited by the `match_count` argument.
Feel free to modify this method to fit the needs of your application. The `match_threshold` ensures that only documents that have a minimum similarity to the `query_embedding` are returned. Without this, you may end up returning documents that subjectively don't match. This value will vary for each application - you will need to perform your own testing to determine the threshold that makes sense for your app.
To execute the function from your client library, call `rpc()` with the name of your Postgres function:
```ts
const { data: documents } = await supabaseClient.rpc('match_documents', {
query_embedding: embedding, // Pass the embedding you want to compare
match_threshold: 0.78, // Choose an appropriate threshold for your data
match_count: 10, // Choose the number of matches
})
```
In this example `embedding` would be another embedding you wish to compare against your table of pre-generated embedding documents. For example if you were building a search engine, every time the user submits their query you would first generate an embedding on the search query itself (using `openai.createEmbedding()`), then pass it into the above `rpc()` function to match.
Vectors and embedding can be used for much more than search. Learn more about embeddings at [What are Embeddings?](/docs/guides/ai/concepts#what-are-embeddings).
### Indexes
Once your vector table starts to grow, you will likely want to add an index to speed up queries. See [Managing indexes](/docs/guides/ai/managing-indexes) to learn how vector indexes work and how to create them.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+11
View File
@@ -74,6 +74,17 @@ Supabase provides a Realtime API using [Realtime](https://github.com/supabase/re
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
## API URL and Keys
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
<video width="99%" muted playsInline controls={true}>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
type="video/mp4"
/>
</video>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -11,10 +11,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
## Official Libraries
| `Language` | `Source Code` | `Documentation` |
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-dart](https://github.com/supabase/supabase-dart) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
| `Language` | `Source Code` | `Documentation` |
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
| Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
## Community Libraries
@@ -22,10 +22,10 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](https://supabase.com/docs/reference/csharp/introduction) |
| Go | [supabase-go](https://github.com/supabase-community/supabase-go) | |
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | |
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](https://supabase.com/docs/reference/kotlin/introduction) |
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | |
| 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} />
+1 -1
View File
@@ -90,7 +90,7 @@ const [captchaToken, setCaptchaToken] = useState()
Now lets add the HCaptcha component to the JSX section of our code
```html
```jsx
<HCaptcha />
```
+1 -1
View File
@@ -95,7 +95,7 @@ Future<void> signOut() async {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
@@ -19,6 +19,14 @@ A collection of framework-specific Auth utilities for working with Supabase.
description={'A pre-built React component for authenticating users.'}
/>
</div>
{/* Flutter Auth UI */}
<div className="col-span-6">
<ButtonCard
to={'/guides/auth/auth-helpers/flutter-auth-ui'}
title={'Flutter Auth UI'}
description={'Pre-built Flutter widgets for authenticating users.'}
/>
</div>
{/* Next.js */}
<div className="col-span-6">
<ButtonCard
@@ -25,7 +25,7 @@ npm install @supabase/supabase-js @supabase/auth-ui-react @supabase/auth-ui-shar
Pass `supabaseClient` from `@supabase/supabase-js` as a prop to the component.
```js title=/src/index.js
```js /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
@@ -38,7 +38,7 @@ This renders the Auth component without any styling.
We recommend using one of the predefined themes to style the UI.
Import the theme you want to use and pass it to the `appearance.theme` prop.
```js lines=4,16 title=/src/index.js
```js mark=4,16 /src/index.js
import { Auth } from '@supabase/auth-ui-react'
import {
// Import predefined theme
@@ -63,7 +63,7 @@ const App = () => (
The Auth component also supports login with [official social providers](../../auth#providers).
```js lines=13 title=/src/index.js
```js mark=11 /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
@@ -79,6 +79,23 @@ const App = () => (
)
```
### Options
Options are available via `queryParams`:
```jsx
<Auth
supabaseClient={supabase}
providers={['google']}
queryParams={{
access_type: 'offline',
prompt: 'consent',
hd: 'domain.com',
}}
onlyThirdPartyProviders={true}
/>
```
### Supported Views
The Auth component is currently shipped with the following views:
@@ -106,7 +123,7 @@ There are several ways to customize Auth UI:
Auth UI comes with several themes to customize the appearance. Each predefined theme comes with at least two variations, a `default` variation, and a `dark` variation. You can switch between these themes using the `theme` prop. Import the theme you want to use and pass it to the `appearance.theme` prop.
```js lines=2,13 title=/src/index.js
```js mark=3,14 /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
@@ -135,7 +152,7 @@ Currently there is only one predefined theme available, but we plan to add more.
Auth UI comes with two theme variations: `default` and `dark`. You can switch between these themes with the `theme` prop.
```js lines=14 title=/src/index.js
```js mark=15 /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
@@ -161,7 +178,7 @@ If you don't pass a value to `theme` it uses the `"default"` theme. You can pass
Auth UI themes can be overridden using variable tokens. See the [list of variable tokens](https://github.com/supabase/auth-ui/blob/main/packages/shared/src/theming/Themes.ts).
```js lines=14-21 title=/src/index.js
```js mark=12:19 /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
@@ -193,7 +210,7 @@ If you created your own theme, you may not need to override any of the them.
You can create your own theme by following the same structure within a `appearance.theme` property.
See the list of [tokens within a theme](https://github.com/supabase/auth-ui/blob/main/packages/shared/src/theming/Themes.ts).
```js title=/src/index.js
```js /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
@@ -243,7 +260,7 @@ You can swich between different variations of your theme with the ["theme" prop]
You can use custom CSS classes for the following elements:
`"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`.
```js title=/src/index.js
```js /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
@@ -271,7 +288,7 @@ const App = () => (
You can use custom CSS inline styles for the following elements:
`"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`.
```js title=/src/index.js
```js /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
@@ -295,7 +312,7 @@ const App = () => (
You can use custom labels with `localization.variables`. See the [list of labels](https://github.com/supabase/auth-ui/blob/main/packages/shared/src/localization/en.json) that can be overwritten.
```js title=/src/index.js
```js mark=10:15 /src/index.js
import { createClient } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
@@ -304,7 +321,6 @@ const supabase = createClient('<INSERT PROJECT URL>', '<INSERT PROJECT ANON API
const App = () => (
<Auth
supabaseClient={supabase}
//highlight-start
localization={{
variables: {
sign_in: {
@@ -313,7 +329,6 @@ const App = () => (
},
},
}}
//highlight-end
/>
)
```
@@ -0,0 +1,130 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
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.
<image src="https://raw.githubusercontent.com/supabase-community/flutter-auth-ui/main/assets/supabase_auth_ui.png" />
## Add Flutter Auth UI
Add the latest version of the package [supabase-auth-ui](https://pub.dev/packages/supabase_auth_ui) to pubspec.yaml:
```yaml
dependencies:
flutter:
sdk: flutter
supabase_auth_ui: ^0.1.0+2
```
### Initialize the Flutter Auth Package
```dart
import 'package:flutter/material.dart';
import 'package:supabase_auth_ui/supabase_auth_ui.dart';
void main() async {
await Supabase.initialize(
url: dotenv.get('SUPABASE_URL'),
anonKey: dotenv.get('SUPABASE_ANON_KEY'),
);
runApp(const MyApp());
}
```
### Email Auth
Use a SupaEmailAuth widget to create an email and password signin and signup form. It also contains a button to toggle to display a forgot password form.
You can pass metadataFields to add additional fields to the form to pass as metadata to Supabase.
```dart
SupaEmailAuth(
redirectTo: kIsWeb ? null : 'io.mydomain.myapp://callback',
onSignInComplete: (response) {},
onSignUpComplete: (response) {},
metadataFields: [
MetaDataField(
prefixIcon: const Icon(Icons.person),
label: 'Username',
key: 'username',
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please enter something';
}
return null;
},
),
],
)
```
### Magic Link Auth
Use SupaMagicAuth widget to create a magic link signIn form.
```dart
SupaMagicAuth(
redirectUrl: kIsWeb ? null : 'io.mydomain.myapp://callback',
onSuccess: (Session response) {},
onError: (error) {},
)
```
### Reset password
Use SupaResetPassword to create a password reset form.
```dart
SupaResetPassword(
accessToken: supabase.auth.currentSession?.accessToken,
onSuccess: (UserResponse response) {},
onError: (error) {},
)
```
### Phone Auth
Use SupaPhoneAuth to create a phone authentication form.
```dart
SupaPhoneAuth(
authAction: SupaAuthAction.signUp,
onSuccess: (AuthResponse response) {},
),
```
### Social Auth
The package supports login with [official social providers](../../auth#providers).
Use SupaSocialsAuth to create list of social login buttons.
```dart
SupaSocialsAuth(
socialProviders: [
SocialProviders.apple,
SocialProviders.google,
],
colored: true,
redirectUrl: kIsWeb
? null
: 'io.mydomain.myapp://callback',
onSuccess: (Session response) {},
onError: (error) {},
)
```
### 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
@@ -0,0 +1,935 @@
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)',
}
This submodule provides convenience helpers for implementing user authentication in Next.js applications using the pages directory.
> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the App Router has reached stable status. This is now the recommended path for new Next.js app. Check out our guide on using [Auth Helpers with the Next.js App Directory](/docs/guides/auth/auth-helpers/nextjs).
## Install the Next.js helper library
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm"
>
<TabPanel id="npm" label="npm">
```sh
npm install @supabase/auth-helpers-nextjs
```
This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
npm install @supabase/auth-helpers-react
```
</TabPanel>
<TabPanel id="yarn" label="Yarn">
```sh
yarn add @supabase/auth-helpers-nextjs
```
This library supports the following tooling versions:
- Node.js: `^10.13.0 || >=12.0.0`
- Next.js: `>=10`
Additionally, install the **React Auth Helpers** for components and hooks that can be used across all React-based frameworks.
```sh
yarn add @supabase/auth-helpers-react
```
</TabPanel>
</Tabs>
## Set up environment variables
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env.local` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/nextjs/.env.local.example).
```bash .env.local
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
```
## Basic Setup
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
```jsx pages/_app.js
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { useState } from 'react'
function MyApp({ Component, pageProps }) {
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createPagesBrowserClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component:
```tsx mark=2,8 pages/_app.tsx
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
import { useState } from 'react'
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session
}>) {
// Create a new supabase browser client on every first render.
const [supabaseClient] = useState(() => createPagesBrowserClient())
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
)
}
```
</TabPanel>
</Tabs>
You can now determine if a user is authenticated by checking that the `user` object returned by the `useUser()` hook is defined.
### Code Exchange API Route
The `Code Exchange` API route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) implemented by the Next.js Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Create a new file at `pages/api/auth/callback.js` and populate with the following:
```jsx pages/api/auth/callback.js
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const handler = async (req, res) => {
const { code } = req.query
if (code) {
const supabase = createPagesServerClient({ req, res })
await supabase.auth.exchangeCodeForSession(String(code))
}
res.redirect('/')
}
export default handler
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `pages/api/auth/callback.ts` and populate with the following:
```tsx pages/api/auth/callback.ts
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const handler: NextApiHandler = async (req, res) => {
const { code } = req.query
if (code) {
const supabase = createPagesServerClient({ req, res })
await supabase.auth.exchangeCodeForSession(String(code))
}
res.redirect('/')
}
export default handler
```
</TabPanel>
</Tabs>
## Usage with TypeScript
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
### Browser client
Creating a new supabase client object:
```tsx
import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '../database.types'
const supabaseClient = createPagesBrowserClient<Database>()
```
Retrieving a supabase client object from the SessionContext:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>()
```
### Server client
```tsx
// Creating a new supabase server client object (e.g. in API route):
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { Database } from 'types_db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const supabaseServerClient = createPagesServerClient<Database>({
req,
res,
})
const {
data: { user },
} = await supabaseServerClient.auth.getUser()
res.status(200).json({ name: user?.name ?? '' })
}
```
## Client-side data fetching with RLS
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to use the `supabaseClient` from the `useSupabaseClient` hook and only run your query once the user is defined client-side in the `useUser()` hook:
```jsx mark=10:17
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
const LoginPage = () => {
const supabaseClient = useSupabaseClient()
const user = useUser()
const [data, setData] = useState()
useEffect(() => {
async function loadData() {
const { data } = await supabaseClient.from('test').select('*')
setData(data)
}
// Only run query once user is logged in.
if (user) loadData()
}, [user])
if (!user)
return (
<Auth
redirectTo="http://localhost:3000/"
appearance={{ theme: ThemeSupa }}
supabaseClient={supabaseClient}
providers={['google', 'github']}
socialLayout="horizontal"
/>
)
return (
<>
<button onClick={() => supabaseClient.auth.signOut()}>Sign out</button>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
<p>client-side data fetching with RLS</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
)
}
export default LoginPage
```
## Server-side rendering (SSR)
Create a server supabase client to retrieve the logged in user's session:
```jsx pages/profile.js
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
```
## Server-side data fetching with RLS
You can use the server supabase client to run [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) authenticated queries server-side:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function ProtectedPage({ user, data }) {
return (
<>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Run queries with RLS on the server
const { data } = await supabase.from('users').select('*')
return {
props: {
initialSession: session,
user: session.user,
data: data ?? [],
},
}
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function ProtectedPage({ user, data }: { user: User; data: any }) {
return (
<>
<div>Protected content for {user.email}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Run queries with RLS on the server
const { data } = await supabase.from('users').select('*')
return {
props: {
initialSession: session,
user: session.user,
data: data ?? [],
},
}
}
```
</TabPanel>
</Tabs>
## Server-side data fetching to OAuth APIs using `provider token` {`#oauth-provider-token`}
When using third-party auth providers, sessions are initiated with an additional `provider_token` field which is persisted in the auth cookie and can be accessed within the session object. The `provider_token` can be used to make API requests to the OAuth provider's API endpoints on behalf of the logged-in user.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
export default function ProtectedPage({ user, allRepos }) {
return (
<>
<div>Protected content for {user.email}</div>
<p>Data fetched with provider token:</p>
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Retrieve provider_token & logged in user's third-party id from metadata
const { provider_token, user } = session
const userId = user.user_metadata.user_name
const allRepos = await (
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
})
).json()
return { props: { user, allRepos } }
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx
import { User, createPagesServerClient } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function ProtectedPage({ user, allRepos }: { user: User; allRepos: any }) {
return (
<>
<div>Protected content for {user.email}</div>
<p>Data fetched with provider token:</p>
<pre>{JSON.stringify(allRepos, null, 2)}</pre>
<p>user:</p>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
)
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
// Retrieve provider_token & logged in user's third-party id from metadata
const { provider_token, user } = session
const userId = user.user_metadata.user_name
const allRepos = await (
await fetch(`https://api.github.com/search/repositories?q=user:${userId}`, {
method: 'GET',
headers: {
Authorization: `token ${provider_token}`,
},
})
).json()
return { props: { user, allRepos } }
}
```
</TabPanel>
</Tabs>
## Protecting API routes
Create a server supabase client to retrieve the logged in user's session:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx pages/api/protected-route.js
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx pages/api/protected-route.ts
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute: NextApiHandler = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
</Tabs>
## Auth with Next.js Middleware
As an alternative to protecting individual pages you can use a [Next.js Middleware](https://nextjs.org/docs/middleware) to protect the entire directory or those that match the config object. In the following example, all requests to `/middleware-protected/*` will check whether a user is signed in, if successful the request will be forwarded to the destination route, otherwise the user will be redirected:
```ts middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// Check auth condition
if (session?.user.email?.endsWith('@gmail.com')) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: '/middleware-protected/:path*',
}
```
## Migration Guide
### Migrating to v0.7.X
#### PKCE Auth Flow
PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new API route for `/api/auth/callback` that exchanges an auth `code` for the user's `session`.
Check the [Code Exchange API Route steps](/docs/guides/auth/auth-helpers/nextjs-pages#code-exchange-api-route) above to implement this route.
#### Authentication
For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function:
```jsx
supabase.auth.signUp({
email: 'jon@example.com',
password: 'sup3rs3cur3',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})
```
#### Deprecated Functions
With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The `createBrowserSupabaseClient` and `createServerSupabaseClient` functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.
- `createBrowserSupabaseClient` has been replaced with `createPagesBrowserClient`
- `createServerSupabaseClient` has been replaced with `createPagesServerClient`
### Migrating to v0.5.X
To make these helpers more flexible as well as more maintainable and easier to upgrade for new versions of Next.js, we're stripping them down to the most useful part which is managing the cookies and giving you an authenticated supabase-js client in any environment (client, server, middleware/edge).
Therefore we're marking the `withApiAuth`, `withPageAuth`, and `withMiddlewareAuth` higher order functions as deprecated and they will be removed in the next **minor** release (v0.6.X).
Please follow the steps below to update your API routes, pages, and middleware handlers. Thanks!
#### `withApiAuth` deprecated!
Use `createPagesServerClient` within your `NextApiHandler`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx pages/api/protected-route.ts
import { withApiAuth } from '@supabase/auth-helpers-nextjs'
export default withApiAuth(async function ProtectedRoute(req, res, supabase) {
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
})
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx pages/api/protected-route.ts
import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'
const ProtectedRoute: NextApiHandler = async (req, res) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return res.status(401).json({
error: 'not_authenticated',
description: 'The user does not have an active session or is not authenticated',
})
// Run queries with RLS on the server
const { data } = await supabase.from('test').select('*')
res.json(data)
}
export default ProtectedRoute
```
</TabPanel>
</Tabs>
#### `withPageAuth` deprecated!
Use `createPagesServerClient` within `getServerSideProps`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx pages/profile.tsx
import { withPageAuth, User } from '@supabase/auth-helpers-nextjs'
export default function Profile({ user }: { user: User }) {
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
export const getServerSideProps = withPageAuth({ redirectTo: '/' })
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx pages/profile.js
import { createPagesServerClient, User } from '@supabase/auth-helpers-nextjs'
import { GetServerSidePropsContext } from 'next'
export default function Profile({ user }: { user: User }) {
return <pre>{JSON.stringify(user, null, 2)}</pre>
}
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// Create authenticated Supabase Client
const supabase = createPagesServerClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
```
</TabPanel>
</Tabs>
#### `withMiddlewareAuth` deprecated!
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="before"
>
<TabPanel id="before" label="Before">
```tsx middleware.ts
import { withMiddlewareAuth } from '@supabase/auth-helpers-nextjs'
export const middleware = withMiddlewareAuth({
redirectTo: '/',
authGuard: {
isPermitted: async (user) => {
return user.email?.endsWith('@gmail.com') ?? false
},
redirectTo: '/insufficient-permissions',
},
})
export const config = {
matcher: '/middleware-protected',
}
```
</TabPanel>
<TabPanel id="after" label="After">
```tsx middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// Check auth condition
if (session?.user.email?.endsWith('@gmail.com')) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: '/middleware-protected',
}
```
</TabPanel>
</Tabs>
### Migrating to v0.4.X and supabase-js v2
With the update to `supabase-js` v2 the `auth` API routes are no longer required, therefore you can go ahead and delete your `auth` directory under the `/pages/api/` directory. Please refer to the [v2 migration guide](/docs/reference/javascript/v1/upgrade-guide) for the full set of changes within supabase-js.
The `/api/auth/logout` API route has been removed, please use the `signout` method instead:
```jsx
<button
onClick={async () => {
await supabaseClient.auth.signOut()
router.push('/')
}}
>
Logout
</button>
```
The `supabaseClient` and `supabaseServerClient` have been removed in favor of the `createPagesBrowserClient` and `createPagesServerClient` methods. This allows you to provide the CLI-generated types to the client:
```tsx
// client-side
import type { Database } from 'types_db'
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
// server-side API route
import type { NextApiRequest, NextApiResponse } from 'next'
import type { Database } from 'types_db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const supabaseServerClient = createPagesServerClient<Database>({
req,
res,
})
const {
data: { user },
} = await supabaseServerClient.auth.getUser()
res.status(200).json({ name: user?.name ?? '' })
}
```
- The `UserProvider` has been replaced by the `SessionContextProvider`. Make sure to wrap your `pages/_app.js` componenent with the `SessionContextProvider`. Then, throughout your application you can use the `useSessionContext` hook to get the `session` and the `useSupabaseClient` hook to get an authenticated `supabaseClient`.
- The `useUser` hook now returns the `user` object or `null`.
- Usage with TypeScript: You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion:
Creating a new supabase client object:
```tsx
import { Database } from '../database.types'
const [supabaseClient] = useState(() => createPagesBrowserClient<Database>())
```
Retrieving a supabase client object from the SessionContext:
```tsx
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { Database } from '../database.types'
const supabaseClient = useSupabaseClient<Database>()
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -1,500 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'nextjs-server-components',
title: 'Supabase Auth with Next.js app directory',
description:
'Authentication helpers for creating an authenticated Supabase client in Next.js 13 app directory Server Components and Route Handlers.',
sidebar_label: 'Next.js (app)',
}
The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than `localStorage`. This makes the users's session available server-side - in Server Components and Route Handlers - and is automatically sent along with any requests to Supabase.
> Note: If you are using the `pages` directory, check out [Auth Helpers in Next.js](/docs/guides/auth/auth-helpers/nextjs).
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Bh1TOpOcGJQ"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
> To learn more about Supabase and the Next.js 13 app directory, check out [this playlist](https://youtube.com/playlist?list=PL5S4mPUpp4OtwG-qCxm8gA_hjaBq0OPdz).
## Install the Next.js helper library
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="npm"
>
<TabPanel id="npm" label="npm">
```sh
npm install @supabase/auth-helpers-nextjs
```
</TabPanel>
<TabPanel id="yarn" label="Yarn">
```sh
yarn add @supabase/auth-helpers-nextjs
```
</TabPanel>
</Tabs>
## Set up environment variables
Retrieve your project's URL and anon key from your [API settings](https://app.supabase.com/project/_/settings/api) in the dashboard, and create a `.env.local` file with the following environment variables:
```bash title=".env.local"
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
## Configure Middleware
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.js` in order to use a Supabase client in Server Components or Route Handlers.
Create a new `middleware.js` file in the root of your project and populate with the following:
```jsx title="middleware.js"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
export async function middleware(req) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient({ req, res })
await supabase.auth.getSession()
return res
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Middleware runs immediately before each route in rendered. Next.js only provides read access to headers and cookies in Server Components and Route Handlers, however, Supabase needs to be able to set cookies and headers to refresh expired access tokens. Therefore, you must call the `getSession` function in `middleware.ts` in order to use a Supabase client in Server Components or Route Handlers.
Create a new `middleware.ts` file in the root of your project and populate with the following:
```tsx title="middleware.ts"
import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import type { Database } from '@/lib/database.types'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareSupabaseClient<Database>({ req, res })
await supabase.auth.getSession()
return res
}
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createMiddlewareSupabaseClient` to add type support to the Supabase client.
</TabPanel>
</Tabs>
## Supabase Provider
All Client Components need to share a single instance of the Supabase client. We can wrap our application in a `<SupabaseProvider />` and use React Context to create a global Supabase instance.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Create a new file at `/app/supabase-provider.jsx` and populate with the following:
```jsx title="app/supabase-provider.jsx"
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
const Context = createContext(undefined)
export default function SupabaseProvider({ children, session }) {
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase, session }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `/app/supabase-provider.tsx` and populate with the following:
```tsx title="app/supabase-provider.tsx"
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { Session, createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'
import type { SupabaseClient } from '@supabase/auth-helpers-nextjs'
import type { Database } from '@/lib/database.types'
type MaybeSession = Session | null
type SupabaseContext = {
supabase: SupabaseClient<Database>
session: MaybeSession
}
const Context = createContext<SupabaseContext | undefined>(undefined)
export default function SupabaseProvider({
children,
session,
}: {
children: React.ReactNode
session: MaybeSession
}) {
const [supabase] = useState(() => createBrowserSupabaseClient())
const router = useRouter()
useEffect(() => {
const {
data: { subscription },
} = supabase.auth.onAuthStateChange(() => {
router.refresh()
})
return () => {
subscription.unsubscribe()
}
}, [router, supabase])
return (
<Context.Provider value={{ supabase, session }}>
<>{children}</>
</Context.Provider>
)
}
export const useSupabase = () => {
const context = useContext(Context)
if (context === undefined) {
throw new Error('useSupabase must be used inside SupabaseProvider')
}
return context
}
```
> TypeScript types can be [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support) and passed to `createBrowserSupabaseClient` to add type support to the Supabase client.
</TabPanel>
</Tabs>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` component:
```jsx title="app/layout.jsx"
import './globals.css'
import SupabaseProvider from './supabase-provider'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
</body>
</html>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` component:
```tsx title="app/layout.tsx"
import './globals.css'
import SupabaseProvider from './supabase-provider'
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
</body>
</html>
)
}
```
</TabPanel>
</Tabs>
Now any of our Client Components can use the `useSupabase` hook to ensure they are using the same instance of a Supabase client.
## Creating a Supabase Client
### Client Components
While Server Components are great for data fetching, we still need to use Supabase client-side for [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx) and [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
As mentioned above, it is important that all Client Components share a single instance of the Supabase client. We can use the `useSupabase` hook we created above to ensure this is the case.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/new-post.jsx"
'use client'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```jsx title="app/new-post.tsx"
'use client'
import { useState } from 'react'
import { useSupabase } from './supabase-provider'
export default function NewPost() {
const [content, setContent] = useState('')
const { supabase } = useSupabase()
const handleSave = async () => {
const { data } = await supabase.from('posts').insert({ content }).select()
}
return (
<>
<input onChange={(e) => setContent(e.target.value)} value={content} />
<button onClick={handleSave}>Save</button>
</>
)
}
```
</TabPanel>
</Tabs>
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/supabase-provider.tsx) for making the user's session available to all Client Components.
### Server Components
In order to use Supabase in Server Components, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/page.jsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="app/page.tsx"
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { headers, cookies } from 'next/headers'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export default async function ServerComponent() {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
</TabPanel>
</Tabs>
> check out [this example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx) for redirecting unauthenticated users - protected pages.
### Route Handlers
In order to use Supabase in Route Handlers, you need to have implemented the `middleware.ts` steps above 👆
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
```jsx title="app/api/posts/route.jsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title="app/api/posts/route.tsx"
import { createRouteHandlerSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import { headers, cookies } from 'next/headers'
import type { Database } from '@/lib/database.types'
// do not cache this page
export const revalidate = 0
export async function GET() {
const supabase = createRouteHandlerSupabaseClient<Database>({
headers,
cookies,
})
const { data } = await supabase.from('posts').select('*')
return NextResponse.json(data)
}
```
</TabPanel>
</Tabs>
> Check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for a full example including [authentication](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/login.tsx), [realtime](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx) and [protected pages](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/page.tsx).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
File diff suppressed because it is too large. Load diff
@@ -56,11 +56,87 @@ This library supports the following tooling versions:
Retrieve your project URL and anon key in your project's [API settings](https://app.supabase.com/project/_/settings/api) in the Dashboard to set up the following environment variables. For local development you can set them in a `.env` file. See an [example](https://github.com/supabase/auth-helpers/blob/main/examples/remix/.env.example).
```bash title=.env
```bash .env
SUPABASE_URL=YOUR_SUPABASE_URL
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
### Code Exchange Route
The `Code Exchange` route is required for the [server-side auth flow](https://supabase.com/docs/guides/auth/server-side-rendering) implemented by the Remix Auth Helpers. It exchanges an auth `code` for the user's `session`, which is set as a cookie for future requests made to Supabase.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
>
<TabPanel id="js" label="JavaScript">
Create a new file at `app/routes/auth.callback.jsx` and populate with the following:
```jsx app/routes/auth.callback.jsx
import { redirect } from '@remix-run/node'
import { createServerClient } from '@supabase/auth-helpers-remix'
export const loader = async ({ request }) => {
const response = new Response()
const url = new URL(request.url)
const code = url.searchParams.get('code')
if (code) {
const supabaseClient = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{ request, response }
)
await supabaseClient.auth.exchangeCodeForSession(code)
}
return redirect('/', {
headers: response.headers,
})
}
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new file at `app/routes/auth.callback.tsx` and populate with the following:
```tsx app/routes/auth.callback.tsx
import { redirect } from '@remix-run/node'
import { createServerClient } from '@supabase/auth-helpers-remix'
import type { Database } from 'db_types'
import type { LoaderArgs } from '@remix-run/node'
export const loader = async ({ request }: LoaderArgs) => {
const response = new Response()
const url = new URL(request.url)
const code = url.searchParams.get('code')
if (code) {
const supabaseClient = createServerClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ request, response }
)
await supabaseClient.auth.exchangeCodeForSession(code)
}
return redirect('/', {
headers: response.headers,
})
}
```
> `Database` is a TypeScript definitions file [generated by the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types).
</TabPanel>
</Tabs>
## Server-side
The Supabase client can now be used server-side - in loaders and actions - by calling the `createServerClient` function.
@@ -252,7 +328,7 @@ Since our environment variables are not available client-side, we need to plumb
>
<TabPanel id="js" label="JavaScript">
```jsx title=app/root.jsx
```jsx app/root.jsx
export const loader = () => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
@@ -267,26 +343,26 @@ export const loader = () => {
Next, we call the `useLoaderData` hook in our component to get the `env` object.
```jsx title=app/root.jsx
```jsx app/root.jsx
const { env } = useLoaderData()
```
We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.
```jsx title=app/root.jsx
```jsx app/root.jsx
const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))
```
And then we can share this instance across our application with Outlet Context.
```jsx title=app/root.jsx
```jsx app/root.jsx
<Outlet context={{ supabase }} />
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title=app/root.tsx
```tsx app/root.tsx
export const loader = ({}: LoaderArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
@@ -301,13 +377,13 @@ export const loader = ({}: LoaderArgs) => {
Next, we call the `useLoaderData` hook in our component to get the `env` object.
```tsx title=app/root.tsx
```tsx app/root.tsx
const { env } = useLoaderData<typeof loader>()
```
We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.
```tsx title=app/root.tsx
```tsx app/root.tsx
const [supabase] = useState(() =>
createBrowserClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
)
@@ -315,7 +391,7 @@ const [supabase] = useState(() =>
And then we can share this instance across our application with Outlet Context.
```tsx title=app/root.tsx
```tsx app/root.tsx
<Outlet context={{ supabase }} />
```
@@ -341,7 +417,7 @@ Let's pipe that through from our loader.
<TabPanel id="js" label="JavaScript">
```jsx title=app/root.jsx
```jsx app/root.jsx
export const loader = async ({ request }) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL,
@@ -375,7 +451,7 @@ export const loader = async ({ request }) => {
<TabPanel id="ts" label="TypeScript">
```tsx title=app/root.tsx
```tsx app/root.tsx
export const loader = async ({ request }: LoaderArgs) => {
const env = {
SUPABASE_URL: process.env.SUPABASE_URL!,
@@ -420,7 +496,7 @@ And then use the revalidator, inside the `onAuthStateChange` hook.
<TabPanel id="js" label="JavaScript">
```jsx title=app/root.jsx
```jsx app/root.jsx
const { env, session } = useLoaderData()
const { revalidate } = useRevalidator()
@@ -441,14 +517,14 @@ useEffect(() => {
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, supabase, fetcher])
}, [serverAccessToken, supabase, revalidate])
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```tsx title=app/root.tsx
```tsx app/root.tsx
const { env, session } = useLoaderData<typeof loader>()
const { revalidate } = useRevalidator()
@@ -471,7 +547,7 @@ useEffect(() => {
return () => {
subscription.unsubscribe()
}
}, [serverAccessToken, supabase, fetcher])
}, [serverAccessToken, supabase, revalidate])
```
</TabPanel>
@@ -493,7 +569,7 @@ Now we can use our outlet context to access our single instance of Supabase and
<TabPanel id="js" label="JavaScript">
```jsx title=app/components/login.jsx
```jsx app/components/login.jsx
export default function Login() {
const { supabase } = useOutletContext()
@@ -507,6 +583,9 @@ export default function Login() {
const handleGitHubLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
},
})
}
@@ -528,7 +607,7 @@ export default function Login() {
<TabPanel id="ts" label="TypeScript">
```tsx title=app/components/login.tsx
```tsx app/components/login.tsx
export default function Login() {
const { supabase } = useOutletContext<{ supabase: SupabaseClient<Database> }>()
@@ -542,6 +621,9 @@ export default function Login() {
const handleGitHubLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
},
})
}
@@ -574,7 +656,7 @@ export default function Login() {
<TabPanel id="js" label="JavaScript">
```jsx title=app/routes/realtime.jsx
```jsx app/routes/realtime.jsx
import { useLoaderData, useOutletContext } from '@remix-run/react'
import { createServerClient } from '@supabase/auth-helpers-remix'
import { json } from '@remix-run/node'
@@ -622,7 +704,7 @@ export default function Index() {
<TabPanel id="ts" label="TypeScript">
```tsx title=app/routes/realtime.tsx
```tsx app/routes/realtime.tsx
import { useLoaderData, useOutletContext } from '@remix-run/react'
import { createServerClient } from '@supabase/auth-helpers-remix'
import { json } from '@remix-run/node'
@@ -685,6 +767,30 @@ export default function Index() {
> Ensure you have [enabled replication](https://app.supabase.com/project/_/database/replication) on the table you are subscribing to.
## Migration Guide
### Migrating to v0.2.0
#### PKCE Auth Flow
PKCE is the new server-side auth flow implemented by the Remix Auth Helpers. It requires a new `loader` route for `/auth/callback` that exchanges an auth `code` for the user's `session`.
Check the [Code Exchange Route steps](/docs/guides/auth/auth-helpers/remix#code-exchange-route) above to implement this route.
#### Authentication
For authentication methods that have a `redirectTo` or `emailRedirectTo`, this must be set to this new code exchange API Route - `/api/auth/callback`. This is an example with the `signUp` function:
```jsx
supabaseClient.auth.signUp({
email: 'jon@example.com',
password: 'sup3rs3cur3',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback',
},
})
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
File diff suppressed because it is too large. Load diff
@@ -13,8 +13,7 @@ Magic links only work with email addresses. By default, a user can only request
Setting up Magic Link logins for your Supabase application.
- Add Magic Link authenticator to your [Supabase Project](https://app.supabase.com)
- Add the login code to your application - [JavaScript](https://github.com/supabase/supabase-js) | [Flutter](https://github.com/supabase/supabase-flutter)
- Add the login code to your application - [JavaScript](/docs/reference/javascript/introduction) | [Flutter](/docs/reference/dart/installing)
## Add Magic Link into your Supabase Project
@@ -88,7 +87,7 @@ Future<void> signOut() async {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
@@ -65,7 +65,7 @@ We are using `next` as our query parameter, but this can name whatever you like.
The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code.
### Exchange authorization code
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function.
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeCodeForSession` function.
```ts
// api/auth/callback.ts
@@ -44,6 +44,17 @@ You cannot delete a user if they are the owner of any objects in Supabase Storag
You will encounter an error when you try to delete an Auth user that owns any Storage objects. If this happens, try deleting all the objects for that user, or reassign ownership to another user.
</Admonition>
## Exporting Users
As Supabase is built on top of PostgreSQL, you can query the `auth.users` and `auth.identities` table via the `SQL Editor` tab to extract all users:
```sql
select * from auth.users;
```
You can also opt to export the results via CSV through the dashboard if you wish:
![export_users.png](/docs/img/guides/auth-managing-user-data/export_users.png)
## Public access
Since Row Level Security is enabled, this table is accessible via the API but no data will be returned unless we set up some Policies.
@@ -104,6 +104,8 @@ create policy "Users can update their own profiles."
2. Enables RLS.
3. Creates a policy which allows logged in users to update their own data.
**Note:** If you want to use upsert operations, the user needs to have `INSERT`, `UPDATE`, and `SELECT` permissions.
### Only anon or authenticated access
You can add a Postgres role
@@ -286,43 +288,24 @@ using (
## Tips
### Enable Realtime for database tables
Realtime server broadcasts database changes to authorized users depending on your Row Level Security (RLS) policies.
We recommend that you enable row level security and set row security policies on tables that you add to the publication.
However, you may choose to disable RLS on a table and have changes broadcast to all connected clients.
```sql
/**
* REALTIME SUBSCRIPTIONS
* Realtime enables listening to any table in your public schema.
*/
begin;
-- remove the realtime publication
drop publication if exists supabase_realtime;
-- re-create the publication but don't enable it for any tables
create publication supabase_realtime;
commit;
-- add a table to the publication
alter publication supabase_realtime add table products;
-- add other tables to the publication
alter publication supabase_realtime add table posts;
```
### You don't have to use policies
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture.
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel.
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all.
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser.
However, if you want to use another authorization method for your applications, that's also fine. Supabase is "just Postgres", so if your application
works with Postgres, then it also works with Supabase.
That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase.
Tip: Make sure to enable RLS for all your tables, so that your tables are inaccessible. Then use the "Service" which we provide, which is designed to bypass RLS.
If you plan to use this approach make sure to enable RLS for your tables. Then use the `service_role` key (for our client libraries) or the `postgres` role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient:
```sql
create table profiles (
id serial primary key,
email text
);
alter table profiles enable row level security;
```
### Never use a service key on the client
@@ -306,3 +306,7 @@ export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
### Which authentication flows have PKCE support?
At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful.
@@ -1,4 +1,5 @@
import Layout from '~/layouts/DefaultGuideLayout'
import AppleSecretGenerator from '~/components/AppleSecretGenerator'
export const meta = {
id: 'auth-apple',
@@ -89,69 +90,15 @@ Now you'll need to download a `secret key` file from Apple that will be used to
- Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`.
- Click `Done` at the top right.
## Generate a `client_secret`
## Generate a client secret
The `secret key` you downloaded is used to create the `client_secret` string you'll need to authenticate your users.
You need to configure a client secret when using Sign in with Apple for Web. This is a specially crafted [JWT signed with a secret key downloaded from Apple's Developer Center](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens).
According to the [Apple Docs](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) it needs to be a JWT
token encrypted using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm.
<Admonition>
Use this tool to generate a new Apple client secret. No keys leave your browser!
</Admonition>
At this time, the easiest way to generate this JWT token is with [Ruby](https://www.ruby-lang.org/en/).
If you don't have Ruby installed, you can [Download Ruby Here](https://www.ruby-lang.org/en/downloads).
- Install Ruby (or check to make sure it's installed on your system).
- Install [ruby-jwt](https://github.com/jwt/ruby-jwt).
- From the command line, run: `sudo gem install jwt`.
Create the script below using a text editor: `secret_gen.rb`
```ruby
require "jwt"
key_file = "Path to the private key"
team_id = "Your Team ID"
client_id = "The Service ID of the service you created"
key_id = "The Key ID of the private key"
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.
private_key = OpenSSL::PKey::EC.new IO.read key_file
token = JWT.encode(
{
iss: team_id,
iat: Time.now.to_i,
exp: Time.now.to_i + 86400 * validity_period,
aud: "https://appleid.apple.com",
sub: client_id
},
private_key,
"ES256",
header_fields=
{
kid: key_id
}
)
puts token
```
1. Edit the `secret_gen.rb` file:
- `key_file` = "Path to the private key you downloaded from Apple". It should look like this: `AuthKey_XXXXXXXXXX.p8`.
- `team_id` = "Your Team ID". This is found at the Apple Developer website, under Membership details. This is a 10-character alphanumeric string called "Team ID". Alternatively, this can be seen next to your name in the upper right when viewing your Certificates, Identifiers & Profiles.
- `client_id` = "The Service ID of the service you created". This is the `Services ID` you created in the above step `Obtain a Services ID`. If you've lost this ID, you can find it in the Apple Developer Site:
- Go to `Certificates, Identifiers & Profiles`.
- Click `Identifiers` at the left.
- At the top right drop-down, select `Services IDs`.
- Find your Identifier in the list (i.e. app.com.acme.roadrunner).
- `key_id` = "The Key ID of the private key". This can be found in the name of your downloaded secret file (For a file named `AuthKey_XXXXXXXXXX.p8` your key_id is `XXXXXXXXXX`). If you've lost this ID, you can find it in the Apple Developer Site:
- Go to `Certificates, Identifiers & Profiles`.
- Click `Keys` at the left.
- Click on your newly-created key in the list.
- Look under `Key ID` to find your key_id.
2. From the command line, run: `ruby secret_gen.rb > client_secret.txt`.
3. Your `client_secret` is now stored in this `client_secret.txt` file.
<AppleSecretGenerator />
## Add your OAuth credentials to Supabase
@@ -180,8 +127,6 @@ async function signout() {
## Resources
- [Apple Developer Account](https://developer.apple.com).
- [Ruby](https://www.ruby-lang.org/en/) Docs.
- [ruby-jwt](https://github.com/jwt/ruby-jwt) library.
- Thanks to [Janak Amarasena](https://medium.com/@janakda) who did all the heavy lifting in [How to configure Sign In with Apple](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003).
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -2,11 +2,11 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-azure',
title: 'Login with Azure',
description: 'Add Azure OAuth to your Supabase project',
title: 'Login with Azure (Microsoft)',
description: 'Add Azure (Microsoft) OAuth to your Supabase project',
}
To enable Azure Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
To enable Azure (Microsoft) Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard.
## Overview
@@ -91,6 +91,21 @@ async function signout() {
}
```
## Obtain the provider refresh token
Azure OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to include the following scope:
```js
async function signInWithAzure() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'azure',
options: {
scopes: 'offline_access',
},
})
}
```
## Resources
- [Azure Developer Account](https://portal.azure.com)
@@ -68,7 +68,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Bitbucket Account](https://bitbucket.org)
@@ -69,7 +69,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Discord Account](https://discord.com)
- [Discord Developer Portal](https://discord.com/developers)
@@ -83,7 +83,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Facebook Developers Dashboard](https://developers.facebook.com/)
@@ -79,7 +79,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [GitHub Developer Settings](https://github.com/settings/developers)
@@ -65,7 +65,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [GitLab Account](https://gitlab.com)
@@ -83,22 +83,6 @@ async function signInWithGoogle() {
}
```
Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters:
```js
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
}
```
You can view the full list of query parameters and their descriptions [here](https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient).
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
@@ -109,9 +93,28 @@ async function signout() {
}
```
## Obtain the provider refresh token
Google OAuth2.0 doesn't return the `provider_refresh_token` by default. If you need the `provider_refresh_token` returned, you will need to add additional query parameters:
```js
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent',
hd: 'domain.com', // google will also allow OAuth logins to be restricted to a specified domain using the 'hd' parameter
},
},
})
}
```
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard)
@@ -64,7 +64,7 @@ async function signout() {
## Resources
- [Supabase Account - Free Tier OK](https://supabase.com)
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
Loaded 100 of 1101 files, more files were not shown because too many files have changed in this diff. Show more